In this article, I'll show you how I automatically deploy my web apps from GitHub on my server using the new GitHub container registry, Docker, GitHub Actions, and watchtower.
What even is "Automatic Deployment"?
Well- Have you ever been annoyed by rebuilding your code, reuploading the files to your server, restarting your app, and maybe even rebuilding the Docker image?
That's the exact problem CD ("Continuous Deployment") tries to solve. With a proper CD pipeline set up, you don't have to worry about any of these things anymore.
You can have a tool like GitHub Actions (or any other CI/CD tool) automatically build your code when you, for instance, push to a certain branch or create a new release.
Then, some magic ✨ is automatically going to deploy the built code to your server.
You can even make GitHub Actions build your code as a Docker image and push it to a container registry like Docker Hub, GitLab, or GitHub for a continuous build of your application.
The app we'll be deploying
To give you a (more or less) real-world example, I'll go through how the automatic deployment of my homepage (fabiancdng.com) works, which is a Next.js app.
Note that this blog post is from 2021 and my architecture might have changed by now. The concept should remain the same, however.
But you can do this for literally any web app as long as you have a functional Dockerfile for building an image from your code.
You'll have to create that yourself for your app but in this example, my Next.js (standalone mode) Dockerfile looks like this:
# Production image, copy all the files and run next
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# You only need to copy next.config.js if you are NOT using the default configuration
# COPY --from=builder /app/next.config.js ./
# COPY --from=builder /app/public ./public
# COPY --from=builder /app/package.json ./package.json
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
# COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
# COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --chown=nextjs:nodejs .next/standalone ./
COPY --chown=nextjs:nodejs .next/static ./.next/static
COPY --chown=nextjs:nodejs public ./public
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]
Step 1: Using GitHub Actions to build and push the image
First, we set up a GitHub Actions workflow that is going to build a Docker image from our code, whenever we push to the master branch.
In case you want a different event to trigger your deployment (like a tag or something), you can take a look at the according page in the GitHub Actions docs.
Creating the workflow file
GitHub Actions are stored as YAML files in the repository under the path .github/workflows
. So you have to create that folder first.
Then, simply create a new file in the workflows folder (the name can be anything). Just make sure it has the .yml
ending.
Alrighty, let's go through the workflow file!
1. Specifying the name of the workflow
Start by giving your workflow a name. Enter the following in your workflow file (you can of course choose anything for the name):
name: Deploy Docker image
2. Specifying the event triggering the workflow
There are multiple options for an event, which you can find in the docs.
In this case, my event is a push to the master branch.
Add the event in the following format to your workflow file (indentation is key ❗ ):
on:
push:
branches:
- master
3. Creating a job
A workflow consists of one or more jobs, which can be something like test, build, deploy.
For the sake of simplicity, I just create one job called "Deploy" that is going to build the code into a Docker image and push it to the registry:
jobs:
Deploy:
runs-on: ubuntu-latest
As you can see, the job also has a key runs-on
which you can use to specify the OS your GitHub Actions container is supposed to run.
4. Creating the steps for the "Deploy" job
Each job consists of one or more steps. For our deployment job, we want to set up 3 steps:
One for copying the code in the repo
One for logging into the container registry
One for both building and pushing the docker image
We can use a pre-made action for all of these steps that we just have to pass some data to work:
steps:
- name: Checkout Code
uses: actions/checkout@v1
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push Docker Image
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: |
ghcr.io/fabiancdng/fabiancdng-homepage:latest
Make sure to change the username and the name of your image down in the "tags" section.
The finished script should then look something like this:
name: Deploy Docker image
on:
push:
branches:
- master
jobs:
Deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v1
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push Docker Image
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: |
ghcr.io/fabiancdng/fabiancdng-homepage:latest
5. Creating an access token
Update: You don't need to create an access token yourself anymore.
GitHub Actions automatically provides a secret
GITHUB_TOKEN
now that contains a token to authorize the push to the registry in the workflow. You can still create a custom access token though to overwrite the scopes/permissions of the defaultGITHUB_TOKEN
.If you don't need to do that, simply skip this step.
To allow GitHub Actions to push images to the package registry on our behalf, we need to supply it with a GitHub access token that has write access to the package registry.
Go to the Developer settings on GitHub.com and navigate to the option "Personal Access Tokens" in the sidebar.
Then hit "Generate new token" and specify the permissions. I recommend just checking the "packages" settings like this:
6. Putting the access token into the "Repository Secrets"
Update: Only follow this step if you want to overwrite the default
GITHUB_TOKEN
provided by GitHub Actions (for example to change scopes/permissions).If you don't need to do that, simply skip this step.
If you don't need to do that, simply skip this step.
I highly recommend putting the token in the repository's secrets section to prevent it from showing up in the logs or even worse in the file tree of your public repository.
To do so, go to your repository's settings and then to "Secrets":
Click "New repository secret" and give it the name of GITHUB_TOKEN
and paste your access token as the value.
Step 2: Setting up "watchtower" on your server
Watchtower is an open-source software that automatically recreates your containers with the new version of the image as soon as it is available on the registry.
You can start watchtower without Docker Compose like this:
$ docker run -d \
--name watchtower \
-v /var/run/docker.sock:/var/run/docker.sock \
-v <PATH_TO_HOME_DIR>/.docker/config.json:/config.json \
containrrr/watchtower
Since it binds to the Docker socket, it's going to update ALL of your running containers, you can use container labeling to exclude or only specifically include containers.
Check the docs if you wish to do so.
You can even set up notifications via Email, Discord, Telegram, etc..
Alternatively, you can create a Docker Compose stack for watchtower:
version: '3'
services:
watchtower:
image: containrrr/watchtower
restart: unless-stopped
container_name: watchtower
environment:
- WATCHTOWER_POLL_INTERVAL=1800
- WATCHTOWER_CLEANUP=true
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /root/.docker/config.json:/config.json
The environment variable WATCHTOWER_POLL_INTERVAL
defines the interval (in seconds) watchtower is supposed to check for updates.
WATCHTOWER_CLEANUP
can automatically delete the old and unused images when set to true.
Step 3: Deploying the image
We need to push to the master branch at least once now in order to kick off the action so there's an image to begin with.
When done so, the image should be showing up in the "Packages" tab of your GitHub profile.
You can also associate the image with a repository here.
Creating a docker-compose.yml
for your app
Now, create a container running your image either by using the docker run
command or, like me, by adding it to a docker-compose.yml
file.
version: "3.7"
services:
homepage:
image: ghcr.io/fabiancdng/fabiancdng-homepage:latest
container_name: homepage
restart: unless-stopped
Note: The name of your image should follow this format (if you use the GitHub Container Registry): ghcr.io/<YOUR USERNAME>/<IMAGE NAME>:<TAG>
Step 4: Lean back and let the magic happen 🧙♀
As soon as you've set up GitHub Actions, started your containerized application, and started watchtower, your automatic deployment is set up and ready to go! 🥳
Cheers.
📣 This post was originally published on my website on July 18, 2021.
Top comments (0)