Skip to content

Building, Deploying, and Managing Docker Images with GitHub Actions

Updated: at 02:12 PM

Building, Deploying, and Managing Docker Images with GitHub Actions

Hello, I’m Peter Kellner, and today we’re going to dive deep into how Docker files and docker-compose work together, how they integrate with GitHub actions, and why they are important for your deployment workflow. I’ve brought in files from a current website I’m working on to show you how it all works. There may be extra lines of code here you don’t need, but I wanted to show you a real-world example.

Dockerfile & docker-compose.yml

Our journey begins with two files: the Dockerfile and the docker-compose.yml. These are the heart and soul of containerizing an application. The Dockerfile builds our image, and docker-compose.yml sets up the services that use this image.

Let’s dissect our Dockerfile first.

FROM node:16-alpine AS deps

# Installing npm
RUN npm install -g npm@9.8.1

# Define an ARG variable passed in as --build-arg
ARG DATABASE_URL

# Use the ARG value in the environment variable
ENV DATABASE_URL=$DATABASE_URL

# Create app directory
RUN mkdir -p /usr/src
WORKDIR /usr/src

# Install app dependencies
COPY package.json /usr/src/
COPY package-lock.json /usr/src/
RUN npm install

# Bundle app source
COPY . /usr/src

# Run Prisma generate
RUN npx prisma generate

RUN npm run build
EXPOSE 3100

CMD ["npm", "start"]

This Dockerfile begins by defining the base image (node:16-alpine) and installing npm. Then, an argument DATABASE_URL is declared.

The ARG instruction defines a variable that users can pass at build-time to the builder with the docker build command. This DATABASE_URL will be different when you are building the app and when you are running it. The value of this argument will be passed into the ENV instruction, setting up the DATABASE_URL environment variable inside the Docker container.

After that, the necessary directories are created, and the app’s dependencies are installed. The app’s source code is then copied into the Docker container, followed by the generation of Prisma client using npx prisma generate. Lastly, the application is built, and the port 3100 is exposed for the app to be accessible.

The docker-compose.yml file, on the other hand, configures your application’s services, in this case, the newsfairness service. It specifies how Docker containers should behave in production.

networks:
  web:
    external: true
        
services:
  newsfairness:
    image: ghcr.io/pkellner/newsfairness:latest
    restart: always
    container_name: newsfairness
    environment:
      DATABASE_URL: mysql://root:Abc123@mysqlsecure:3306/mydb?sslaccept=accept_invalid_certs
      MINUTES_TO_CACHE_RESPONSES: 3
      MINUTES_TO_GO_BACK_AND_DELETE_RECORDS: 2880
    ports:
      - 6747:3000
    networks:
      - web
    expose:
      - 6747
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=web"
      - "traefik.http.routers.newsfairness.entrypoints=websecure"
      - "traefik.http.routers.newsfairness.rule=Host(`news-fairness.peterkellner.net`)"
      - "com.centurylinklabs.watchtower.enable=true"

In this file, a network named web is defined, and it’s declared as external. The newsfairness service uses this network, restarts always in case of failure, and exposes the port 6747. This service uses the image built from our Dockerfile and sets up certain environment variables, such as the DATABASE_URL. Note that this DATABASE_URL is different from the one used at build time.

GitHub Actions

Now, let’s introduce the third component of our application deployment flow - GitHub Actions. GitHub Actions help you automate your software development workflows in the same place you store code and collaborate on pull requests and issues.

name: news-fairness

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: web

    name: docker build if docker file changed
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: Get changed files using defaults
        id: changed-files
        uses: tj-actions/changed-files@v35

      - name: Get changed files using a comma separator
        id: changed-files-comma
        uses: tj-actions/changed-files@v35
        with:
          separator: ","

      - name: Get changed action file
        id: changed-files-specific
        uses: tj-actions/changed-files@v35
        with:
          files: |
            .github/workflows/docker-build-push.yml

      - name: Run step if any of the listed files above change
        if: steps.changed-files-specific.outputs.any_changed == 'true'
        env:
          GH_TOKEN: ${{secrets.GH_TOKEN}}
          GH_USERNAME: ${{secrets.GH_USERNAME}}
          DATABASE_URL_EXTERNAL: ${{secrets.DATABASE_URL_EXTERNAL}}
        run: |
          RELEASEVERSION=0.101
          RELEASEDATE1=$(date +"%m/%d/%YT%H:%M:%S%p")
          RELEASEDATE=$(TZ=":US/Pacific" date +%c)
          RELEASEDATEISO=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
          perl -ni -e 'next if /^RELEASE(?:VERSION|DATE)=/;print' .env.production
          (
          echo "RELEASEVERSION=$RELEASEVERSION"
          echo "RELEASEDATE=$RELEASEDATE"
          echo "RELEASEDATEISO=$RELEASEDATEISO"
          ) >> .env.production
          cat .env.production
          docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
          echo $GH_TOKEN | docker login ghcr.io -u $GH_USERNAME --password-stdin &
          docker build --build-arg DATABASE_URL=$DATABASE_URL_EXTERNAL . --file Dockerfile --tag ghcr.io/pkellner/newsfairness:latest --tag ghcr.io/pkellner/newsfairness:$RELEASEVERSION 
          docker push ghcr.io/pkellner/newsfairness --all-tags

This GitHub Actions script, triggered when you push to the main branch, will build a Docker image if the Dockerfile has changed. The secrets in the script are sensitive pieces of information that you don’t want to expose in your code. They are stored securely in GitHub and can be accessed in GitHub Actions.

For instance, DOCKER_USER and DOCKER_PASSWORD are your DockerHub username and password, respectively. GH_TOKEN is your GitHub token used for authenticating with GitHub Container Registry (GHCR), and GH_USERNAME is your GitHub username. The DATABASE_URL_EXTERNAL is the database connection string used when building the Docker image.

Conclusion

By combining Dockerfile, docker-compose, and GitHub Actions, you can create an efficient and secure workflow for deploying and managing

Check out the ORM (Object Relational Mapper) PRISMA. The database access method I use in all my projects