Introduction
In modern software development, automating repetitive tasks like building Docker images and pushing them to a registry is a game-changer. Not only does it streamline the development process, but it also ensures consistency and speed. In this post, I’ll walk you through two GitHub Actions workflows I've set up for my Angular frontend and backend applications. These workflows automate the entire process of building Docker images and pushing them to GitHub Container Registry (GHCR) whenever there are updates to the relevant code.
Why Automate Docker Image Builds?
As applications grow, the manual process of building and deploying Docker images becomes more error-prone and time-consuming. Automating this process with GitHub Actions ensures that every time changes are pushed to the repository, the Docker images are automatically rebuilt and deployed. This is especially useful for continuous integration and deployment (CI/CD) pipelines.
The workflows I'll explain below are set up for:
- Frontend (Angular) - For building and pushing the Docker image of the Angular app.
- Backend - For building and pushing the Docker image of the backend API (in my case, it’s a Node.js app).
Let's dive into the individual workflows.
Workflow 1: Building and Pushing Angular Docker Image
The Goal
This workflow automatically builds a Docker image for my Angular frontend and pushes it to GitHub Container Registry (GHCR). It gets triggered whenever changes are made to the web/ directory in the repository. Specifically, this workflow:
- Installs dependencies using yarn
- Builds the Angular app in production mode
- Logs into GHCR
- Builds the Docker image
- Pushes the image to GHCR
The Workflow File
name: Build & Push Angular Docker Image
on:
push:
branches: [master]
paths:
- "web/**"
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
node-version: 22
- name: Install dependencies
run: yarn install --frozen-lockfile
working-directory: ./web
- name: Build Angular app
run: yarn build --configuration production
working-directory: ./web
- name: Log in to GHCR
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
run: |
IMAGE=ghcr.io/${{ github.repository_owner }}/pet-web
IMAGE=$(echo $IMAGE | tr '[:upper:]' '[:lower:]')
TAG=${{ github.sha }}
docker build -t $IMAGE:latest -t $IMAGE:$TAG ./web
- name: Push Docker image
run: |
IMAGE=ghcr.io/${{ github.repository_owner }}/pet-web
IMAGE=$(echo $IMAGE | tr '[:upper:]' '[:lower:]')
TAG=${{ github.sha }}
docker push $IMAGE:latest
docker push $IMAGE:$TAG
Breakdown of the Workflow Steps
- Checkout Repo: We first use actions/checkout to pull the repository’s code into the workflow.
- Setup Node.js: Then, we set up Node.js using actions/setup-node. I’m using version 22 here, but you can adjust this according to your project requirements.
- Install Dependencies: The next step installs the required dependencies using yarn install within the web/ directory.
- Build the Angular App: We use yarn build to create a production build of the Angular app.
- Login to GHCR: I use the docker/login-action to authenticate with GitHub Container Registry using the GitHub Token.
- Build the Docker Image: The Docker image is built and tagged with the latest tag and the commit SHA for versioning.
- Push the Docker Image: Finally, the image is pushed to the registry. Both the latest and the commit-specific tags are pushed to ensure proper versioning.
Workflow 2: Building and Pushing Backend Docker Image
The Goal
For the backend, the workflow is similar, but it includes an additional step: passing a build argument (DATABASE_URL). This is useful if the backend Docker image requires dynamic configuration based on the environment. Here’s what this workflow does:
- Logs into GHCR
- Builds the backend Docker image with a specific DATABASE_URL
- Pushes the image to GHCR
The Workflow File
name: Backend CI - Build Image
on:
push:
branches: [master]
paths:
- "backend/**"
jobs:
build-backend:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
DOCKER_BUILDKIT: 1
steps:
- uses: actions/checkout@v4
- name: Login to GHCR
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | \
docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build & tag backend image
run: |
IMAGE=ghcr.io/${{ github.repository_owner }}/pet-backend
IMAGE=$(echo $IMAGE | tr '[:upper:]' '[:lower:]')
TAG=${{ github.sha }}
docker build \
--build-arg DATABASE_URL="file:./dev.db" \
-t $IMAGE:latest \
-t $IMAGE:$TAG \
-f backend/Dockerfile \
backend
- name: Push backend image
run: |
IMAGE=ghcr.io/${{ github.repository_owner }}/pet-backend
IMAGE=$(echo $IMAGE | tr '[:upper:]' '[:lower:]')
docker push $IMAGE:latest
docker push $IMAGE:${{ github.sha }}
Breakdown of the Backend Workflow
- Checkout Repo: The repository is checked out using actions/checkout.
- Login to GHCR: Just like the frontend workflow, the backend workflow logs in to GHCR using the GITHUB_TOKEN.
- Build the Docker Image: The backend image is built using the Dockerfile in the backend/ directory. We’re passing the DATABASE_URL as a build argument, which is useful for setting up different environments.
- Push the Docker Image: The built image is then pushed to GHCR with both latest and commit SHA tags.
How These Workflows Benefit Your Development Process
Here’s why I love having these automated workflows in place:
- Consistency: Every push to the master branch will trigger these workflows, ensuring that your Docker images are always built the same way.
- Versioning: Using the commit SHA as a tag ensures that you can always trace which version of the code corresponds to a given image.
- Speed: Automating the build and push process saves developers time, allowing them to focus on writing code rather than dealing with the logistics of deploying Docker images.
- Security: By using the GitHub token and ensuring all steps are run with the proper permissions, you avoid the need for managing sensitive credentials manually.
Conclusion
In this post, I’ve shared how I’ve automated Docker image builds for both the Angular frontend and the backend of my app using GitHub Actions. This setup not only simplifies the deployment process but also ensures that every change made to the codebase results in a corresponding update to the Docker image, all while ensuring version control and security. If you haven’t set up CI/CD for your own applications, this is a great place to start!
