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"]
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.
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.
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 }}"}'
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 }}"}'
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
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.
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.
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.
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'.
You may see a notification.
Head back over on GitHub > Actions and there it is, our image is automatically building!
Head over to Docker Hub and open our repository. There it is, build v0.1.0.
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