← Back to Home
πŸ“¦

Project 1: Containerise an App & Ship it with CI/CD

From source code to an automatically built, tested, and published container image.

⏱12 min readπŸ“šDevOps Fundamentals

Project 1 takes your tiny app and gives it the first two pillars of production: it’s containerised so it runs anywhere, and it’s shipped by a CI/CD pipeline so every push is automatically built, tested, and published. No manual steps.


🎯 Learning Objectives


Step 1 β€” A Production Dockerfile

A good image is small, secure, and cached well. Use a multi-stage build (build tools stay out of the final image) and run as a non-root user:

dockerfile
# ---- build stage ----
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci                 # install deps (cached unless package files change)
COPY . .
RUN npm run build

# ---- runtime stage ----
FROM node:20-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER node                  # never run as root
EXPOSE 3000
HEALTHCHECK CMD curl -f http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]

πŸ’‘ Order layers by change frequency

Copy package*.json and install before copying the rest of the source. Dependencies change rarely, so Docker reuses that cached layer on most builds β€” dramatically faster CI.

Verify it locally before automating:

bash
docker build -t myapp:dev .
docker run -p 3000:3000 myapp:dev

Step 2 β€” The CI Pipeline (Test on Every Push)

Using GitHub Actions, the first job runs your tests on every push and pull request β€” the safety net that lets you move fast:

yaml
# .github/workflows/ci.yml
name: CI
on:
push:
  branches: [main]
pull_request:

jobs:
test:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: "20"
    - run: npm ci
    - run: npm test

Now a red X on a pull request means β€œdon’t merge this” β€” automated, every time.


Step 3 β€” Build & Publish the Image

Add a second job that builds the Docker image and pushes it to a registry (here, GitHub Container Registry) β€” but only on main, and only after tests pass:

yaml
  build-and-push:
  needs: test                      # only if tests pass
  if: github.ref == 'refs/heads/main'
  runs-on: ubuntu-latest
  permissions:
    contents: read
    packages: write
  steps:
    - uses: actions/checkout@v4
    - uses: docker/login-action@v3
      with:
        registry: ghcr.io
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
    - uses: docker/build-push-action@v6
      with:
        push: true
        tags: |
          ghcr.io/${{ github.repository }}:latest
          ghcr.io/${{ github.repository }}:${{ github.sha }}

⚠ Never hardcode credentials

The registry password comes from a secret (the built-in GITHUB_TOKEN here), never from plaintext in the file. Committing credentials is one of the most common β€” and most damaging β€” mistakes. Use your CI’s secret store.


Step 4 β€” Tag for Traceability

Notice the two tags above: latest and the commit SHA. This matters more than it looks:

Tag Purpose
latest Convenience β€” the newest main build
:<git-sha> Immutable, traceable β€” every image maps to exact source code

Deploying by SHA (not latest) means you always know exactly what code is running, and rollback is just deploying a previous SHA. This is the foundation Project 3’s GitOps will build on.

bash β€” 80Γ—24
student@devops:~$git push origin main

πŸ§ͺ Hands-on Lab

πŸ“

Harden the Pipeline

Your pipeline builds and pushes on every push to any branch, using only a latest tag, and runs the container as root. List three concrete improvements and why each matters.


🧠 Knowledge Check

Knowledge Check

Why use a multi-stage Docker build?

Knowledge Check

Why tag images with the Git commit SHA rather than only 'latest'?


πŸ’Ό Interview Preparation

Interview Q&A

Describe a good CI/CD pipeline for a containerised app.


Summary

Project 1 is done: your app is containerised with a solid Dockerfile and shipped by a CI/CD pipeline that tests, builds, and publishes traceable images automatically. Next, Project 2 provisions real infrastructure with Terraform and deploys the app to Kubernetes.

Up Next

Project 2: Provision with Terraform & Deploy to Kubernetes

You've mastered this lesson. Continue your journey to becoming a DevOps Engineer.

Start Next Lesson→