DEV Community

Cover image for Automatic Deployment using Docker and GitHub Actions
Fabian Reinders
Fabian Reinders

Posted on • Updated on • Originally published at fabiancdng.com

Automatic Deployment using Docker and GitHub Actions

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 default GITHUB_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:

Scope selection for a personal access token

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":

Screenshot of a GitHub repository's secrets tab

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Screenshot of GitHub container registry

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
Enter fullscreen mode Exit fullscreen mode

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)