Skip to main content

Containerization

Now that we have a working program, the next step is to containerize it. This is helpful since simplified, think of containers like a small version of Linux that runs your application.

Requirements

Make sure you have Docker Desktop installed

Git

To continue on, branch develop to feature/docker

Docker

To containerize this app, go to the root folder and create a Dockerfile file. It should contain the following:

FROM node:23-alpine3.19
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
CMD [ "npm", "start"]
warning

Using this Dockerfile will run the image as root user which is not very secure. In general, the image should be ran under a user that is configured on the host machine, in this case kubernetes host machines. Say you have a user called 'app' on the kubernetes server and agent virtual machines. Run command

id

This will get you the id and group of the user. Modify your Dockerfile as follows:

FROM node:23-alpine3.19
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
USER 1000:1000
CMD [ "npm", "start"]

Note the USER is now the uid and gid of the host machine user you want to run under. This step is important in securing your image as well as achieving a higher Docker Scout score.

When building Docker, sometimes you may have files that you don't necessarily want. For this, we should add a .dockerignore file and fill it with the following:

.github
.env
/node_modules
/lib

To test if it can be containerized, run the following:

docker build .

So we have containerized the application and it sits on our local Docker repository. Go ahead and commit your changes to feature.

note

Even though you have a container now, if it were to be ran, it doesn't know anything about your environment variables unless you tell it. We will do this later through within Kubernetes.

GitActions

As much as I don't mind building containers, I love automating them. For this, we get into GitActions. These are stored in your git repository under the folder .github/workflows.

GitHub

Before we continue, we have to set up a few things in our repository to get it working. Head over to the repository and go to settings. Under Actions > General, set Read and write permission as well as Allow GitHub Actions to create and approve pull requests.

GitActions Settings

Next go to Secrets and variables > Actions. We need to add a few environment variables within the repository.

  • DOCKER_USERNAME (docker username)
  • DOCKER_PASSWORD (docker api token, not your login password)
  • PAT (github developer personal access token)

Code

Before we move on, branch off develop and create feature/gitActions.

Now within our code repo, add a .github/workflows folder and we will make three files:

docker-pull-request-image.yml

name: Docker Pull Request Image CI
on:
pull_request:
types: [opened]
jobs:
build:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v3
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
-
name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v3
with:
images: ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }}
-
name: Build and push
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64,linux/arm64
push: false
tags: ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }}:pr-${{ github.event.number }}

docker-staging-image.yml

name: Docker Staging Image CI
on:
push:
tags: ["v*"]
jobs:
build:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v3
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }}
-
name: Build and push
uses: docker/build-push-action@v6
with:
context: .
sbom: true
platforms: linux/amd64
push: true
provenance: mode=max
tags: ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }}:${{ github.ref_name }}
-
name: Repository Dispatch
uses: peter-evans/repository-dispatch@v1
with:
token: ${{ secrets.PAT }}
repository: ${{ github.actor }}/argocd-example
event-type: clock-staging
client-payload: '{"image": "${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }}:${{ github.ref_name }}"}'
tip

Notice in the last step for Repository Dispatch, we have an event-type called clock-staging, this will be used later to update argocd manifests on version updates

docker-production-image.yml

name: Docker Production Image CI
on:
push:
tags: ['[0-9]+.[0-9]+.[0-9]+']
jobs:
build:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v3
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }}
-
name: Build and push
uses: docker/build-push-action@v6
with:
context: .
sbom: true
platforms: linux/amd64
push: true
provenance: mode=max
tags: ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }}:${{ github.ref_name }}
-
name: Repository Dispatch
uses: peter-evans/repository-dispatch@v1
with:
token: ${{ secrets.PAT }}
repository: ${{ github.actor }}/argocd-example
event-type: clock-production
client-payload: '{"image": "${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }}:${{ github.ref_name }}"}'
tip

Notice in the last step for Repository Dispatch, we have an event-type called clock-production, this will be used later to update argocd manifests on version updates

note

Notice on Build and push, we have sbom and provenance set, these parameters help secure your base docker images with Docker Scout

Now push your git feature and make pull requests to develop and into main. Go ahead and open up GitHub and select the 'Actions' tab. You will now notice that two builds were ran, one for Added Actions, and another for Develop. These were ran because of the pull request git action we just created.

GitActions Ran

Docker Hub

Now, no image was actually pushed to Docker at this point because we don't have a repository for clock on Docker Hub. Go ahead and create one.

Docker Repo

Versioning and Tags

Now here comes the fun part, the git actions we added actually have settings so when we create tags in git, they will automatically build. Are you ready for this? Head over to GitKraken and right click on develop > Create tag here.

Create Tag

We will tag this v0.1.0. Think beta version 0, minor change 1, attempt 0. Now right click on the tag and select 'Push v0.1.0 to origin'.

Push Develop Tag

You may see a notification.

Push Tag Notification

Head back over on GitHub > Actions and there it is, our image is automatically building!

develop Build

Head over to Docker Hub and open our repository. There it is, build v0.1.0.

Docker build

Now, let's make a production build. For this right click on main and tag it 0.1.0 and push it.

Thoughts on Versioning

Versioning can be a complicated matter but the way I see it, it's a good idea to think of it as the following:

  • 1st digit is release number
  • 2nd digit is feature/bug/hotfix number
  • 3rd digit is iterations of feature/bug/hotfix