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.
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 firstname.lastname@example.org # 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.
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.
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.
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: $ GH_USERNAME: $ 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.
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.
By combining Dockerfile, docker-compose, and GitHub Actions, you can create an efficient and secure workflow for deploying and managing