Automatic Deployment using Docker and GitHub Actions
Tags: tutorial, docker, devops, webdev
Published: 2023-05-13 09:15:40

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](http://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](https://nextjs.org/docs/pages/api-reference/next-config-js/output)) Dockerfile looks like this:

```dockerfile
# 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](https://docs.github.com/en/actions/reference/events-that-trigger-workflows).

### 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):

```yaml
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](https://docs.github.com/en/actions/reference/events-that-trigger-workflows).

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 ❗ ):

```yaml
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:

```yaml
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:

```yaml
    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:

```yaml
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 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](https://github.com/settings/tokens) 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](https://a.storyblok.com/f/213297/883x663/3b0e4970eb/screenshot-2021-07-22-at-14-25-45.png "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](https://a.storyblok.com/f/213297/1405x682/955729d7d8/screenshot-2021-07-18-at-16-32-21.png "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](https://containrrr.dev/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:

```bash
$ 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](https://containrrr.dev/watchtower/container-selection/) if you wish to do so.

You can even set up [notifications via Email, Discord, Telegram, etc.](https://containrrr.dev/watchtower/notifications/).

_Alternatively_, you can create a Docker Compose stack for watchtower:

```yaml
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.

![Screenshot of GitHub container registry](https://a.storyblok.com/f/213297/981x333/7de6aa349c/screenshot-2021-07-22-at-14-27-44.png "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.

```yaml
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](https://fabiancdng.com/blog/automatic-deployment-using-docker-and-github-actions) on July 18, 2021.