Write a Dockerfile so that you can run the DotnetTemplate web app in a Docker container.
You might already have a Dockerfile in your repository from workshop 7, but that should be deleted or moved. It was for running a Jenkins build server locally, not for running this application.
There are different approaches to writing the Dockerfile but we'd recommend starting from an official dotnet image and then scripting the install of node/NPM.
Then use the setup commands in the README to install dependencies and build the app.
Finally, add an ENTRYPOINT
that will start the app.
Once you've done that try building and running the Dockerfile locally to check it works.
Troubleshooting:
- If you are seeing a Node Sass error, try adding the
DotnetTemplate.Web/node_modules
folder to a.dockerignore
file to avoid copying local build artefacts/dependencies into the image.- If you get errors from node-gyp while running
npm install
, try installing build tools withapt-get update && apt-get install -y build-essential
- To build the dotnet code you'll need the correct version of the SDK (Software Development Kit) dotnet Docker image.
- Note that you won't need to run
sudo
when building the image (as the default user is root).- Some instructions, like installing Node, might depend on the OS of an image - for a Linux image it might not be immediately obvious which distribution you have. Running the image and accessing a terminal can provide one approach to explore for an answer but Docker Hub might offer clues too - can you spot any on the dotnet SDK page?
- For many images, starting the container with
docker run -it --entrypoint /bin/bash <image>
will give us access to a terminal, from where we can explore further. A command likecat /etc/*-release
might provide us with an answer from there
- Create a personal free account on Docker Hub (if you haven't already).
- Create a public repository in Docker Hub: https://hub.docker.com/repository/create. Don't connect it to GitHub, and name it dotnettemplate.
- Build your Docker image locally and push it to Docker Hub. See https://docs.docker.com/docker-hub/repos/ for instructions.
You should already have a pipeline which builds and tests the app. You will now extend it to automatically build the Docker image and publish it to Docker Hub.
You could add new steps to your existing job, but let's create a new job to handle this. Make sure your new job only runs after the testing job completes successfully, by using the "needs" option.
You can search the GitHub Actions marketplace for actions to publish a Docker image, or you could script it yourself. Either way, make sure to store credentials securely, not directly in the yaml file.
Try tagging your published image with the name of the branch that triggered the build. You can use the github
context to find out the branch name. See here for details.
Note that default environment variables won't be available in a
with:
section because that's evaluated during workflow processing, before it is sent to the runner.
Add a new job to your .gitlab-ci.yml file. It should belong to a new stage so that it only runs after all tests have completed successfully.
To have access to the Docker command line, your new job needs to use the docker
image and a docker:dind
"service" (which means a container running alongside your job's container)
image: docker
services: [ docker:dind ]
In the new job, run the correct Docker CLI commands to build the image and publish it to Docker Hub.
Make sure to store credentials securely, not directly in the yaml file. You do this via the GitLab website (Settings -> CI/CD -> Variables). CI/CD Variables are available to your pipeline script as environment variables.
Try tagging your published image with the name of the branch that triggered the build. Find the appropriate environment variable from GitLab's documentation.
To test that publishing to Docker Hub is working:
- Make some change to the application code. Don't worry if you don't know anything about C#, find some visible text to modify in DotnetTemplate.Web/Views/Home/FirstPage.cshtml.
- Commit your changes to git, and push them.
- Check your pipeline completes successfully.
- Download and run your new image from Docker Hub (or you could also get someone else to).
Modify the workflow so that it will only publish to Docker Hub when run on certain branches, for example only when the main branch is updated.
In one of the workshop 7 goals you were asked to set up a Jenkins job for the app (if you haven't done that yet it's worth going back to it now). Modify the Jenkinsfile so that it will publish to Docker Hub.
- Create a free Heroku account: https://signup.heroku.com/.
- Create a new Heroku app: https://dashboard.heroku.com/new-app. Do not click the button to integrate with a GitHub repository.
- Build your docker image locally and deploy it to Heroku. See https://devcenter.heroku.com/articles/container-registry-and-runtime for instructions. In particular you want to push an existing image then release the image. The first steps will push the Docker image to Heroku's Docker Hub registry. Then the last step will deploy that image to your Heroku app.
- The docs mention a "process-type". You want to use
web
- If you are using
ENTRYPOINT dotnet run
, that will not work on Heroku because of how it runs containers. You can use the "exec" syntax instead:ENTRYPOINT ["dotnet", "run"]
- You should now see a log of the deployment on your Heroku app's dashboard: https://dashboard.heroku.com/apps/<HEROKU_APP_NAME> (replace
<HEROKU_APP_NAME>
with the name you gave your Heroku app when you created it). - You can see the app running by clicking the "Open app" button on the app's dashboard, or by going to <HEROKU_APP_NAME>.herokuapp.com.
You will need an API key so that your pipeline can access Heroku. Manually run the command heroku authorizations:create
to generate it.
Add your API key as a GitHub Actions secret.
Add new steps to your publishing job in order to deploy to Heroku. You should be able to find an existing action to do this for you, or again, you could script it yourself. As with publishing to Docker Hub, this should only run on the "main" branch.
Hint
You might want to look at this action.
Hint
See the example "Deploy with Docker" section, and don't forget the usedocker
flag
Set your API key as an environment variable called "HEROKU_API_KEY".
Add a new job to your .gitlab-ci.yml
file. You need to:
-
Set up pre-requisites:
- Run the job in a "Docker in Docker" image by setting
image: docker
- Add "Docker" service to your job by setting
services: [ docker:dind ]
. For both of these, you could pick a specific tag from the repository to control the version of Docker. - Install Heroku CLI and its dependencies with:
apk add --update-cache curl bash npm` curl https://cli-assets.heroku.com/install.sh | sh
- Run the job in a "Docker in Docker" image by setting
-
Add all the commands that you ran manually before. Namely:
- Build your image
- Log in to the Heroku container registry
- Push your image
- Trigger a "release"
Make a small, visible change again, push it to your repository and check that it automatically shows up on your Heroku website.
You may have noticed that the image our Dockerfile builds is pretty sizeable (~1.5GB) and, apart from taking up space, it slows our pipeline down during the upload step. .NET allows us to separate the dependencies needed to build the code (part of the SDK) from those needed to run the compiled binary (the runtime), with the latter being much smaller. This offers a good opportunity to optimise the speed of our deployment pipeline.
Try writing your Dockerfile as a multistage build. The structure of your Dockerfile will look like this:
FROM <parent-image-1> as build-stage
# Some commands
FROM <parent-image-2>
# Some commands
In this way you use a large parent (dotnet/sdk
) to build the app and then use a smaller parent (dotnet/aspnet
) for your final image that will run the application. The second stage just needs to copy the build artefact from the earlier stage with a COPY command of the form: COPY --from=build-stage ./source ./destination
.
For an example that closely matches this project see here - or see the Docker docs for another approach.
To make the first example linked above work:
- Replace any mention of "aspnetapp" with "DotnetTemplate.Web".
- Remove the
dotnet restore
line - Remove the "--no-restore" option from the publish command
- Keep your instructions that install node, but you no longer need the "npm ..." commands (they are included in DotnetTemplate.Web.csproj and run as part of
dotnet publish
).
Check that you can still run the app locally using your new image, and then push your changes. You should see a decrease in the image size locally from ~1.5GB to a few hundred MB - does your pipeline speed up?
Sometimes the build, tests and deployment will all succeed, however the app won't actually run. In this case it can be useful if your workflow can tell you if this has happened. Modify your workflow so that it does a healthcheck.
As part of this it can be useful to add a new healthcheck endpoint to the app, see this microsoft guide for an example of how to do this. This article is long and detailed, and that can make it look intimidating - but everything we need to know is just in the "Basic health probe" section. Try working through it! You should find that we can add this healthcheck endpoint with just two lines of code.
At the end of your workflow, check that the response from the healthcheck endpoint is correct.
How would you handle failure, for example if the healthcheck in the previous step fails? Write a custom action that will automatically roll-back a failed Heroku deployment. Make sure it sends an appropriate alert! Find a way to break your application and check this works.
Failures don't always happen immediately after a deployment. Sometimes runtime issues will only emerge after minutes, hours or days in production. Set up a separate workflow which will use your healthcheck endpoint and send a notification if the healthcheck fails. Make sure this workflow runs every 5 minutes. Hint: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#onschedule.
Try making your workflow release to a different Heroku app environment for each branch of your repository.
Currently we'll deploy every time a change is pushed to the main branch. However you might want to have more control over when deployments happen. Modify your Heroku and workflow setup so your main branch releases to a staging environment, and you instead manually trigger a workflow to release to production.
In one of the workshop 7 goals you were asked to set up a Jenkins job for the app (if you haven't done that yet it's worth going back to it now). Now modify the Jenkinsfile so that it will deploy to Heroku.