Module 09Containers~1 hour

Containers in a DevOps Workflow

Connect Docker to your CI pipeline and trace the complete journey from a git push to a verified, containerised application.

Covers:MLO5MLO6MLO7
92

The complete pipeline picture

Over the last eight modules you have built the individual components of a DevOps pipeline. This module connects them. Here is the full picture:

Developer push
CI: checkout
CI: test
CI: lint
CI: docker build
CI: smoke test
PR green

The addition of Docker in the pipeline serves two purposes:

  • Environment verification — if the image builds in CI, it will build anywhere. Dockerfile errors are caught immediately.
  • Artefact production — the image built in CI is the exact artefact that would be deployed to production. CI is the point where the application is packaged.
93

Docker in CI

GitHub's hosted runners have Docker pre-installed. You do not need any special setup. Add the following steps after your tests:

.github/workflows/ci.yml — docker steps
      - name: Build Docker image
        run: docker build -t my-app:${{ github.sha }} .

      - name: Run smoke test
        run: |
          docker run --rm \
            -e APP_ENV=test \
            my-app:${{ github.sha }} \
            python3 -c 'import app; print("OK")'

${{ github.sha }} is a built-in GitHub Actions variable containing the commit hash. Using it as the image tag means every build produces a uniquely-identified image — you can always trace a running container back to the exact commit that built it.

94

Tagging images correctly

Image tags are version labels. Good tagging makes it possible to deploy any specific version and to understand which version is running where:

bash — tagging strategies
# By commit SHA — unique per build
docker build -t my-app:a1b2c3d .

# By semantic version — human readable
docker build -t my-app:1.4.2 .

# By branch name — useful for staging
docker build -t my-app:main .

# Multiple tags on the same image
docker build  -t my-app:1.4.2 -t my-app:latest .

latest is a special convention: it refers to the most recently pushed version. Many tools pull latest by default. Be careful with it — it changes with every push, making it hard to reproduce a specific deployment. For serious deployments, always use a specific tag.

95

Health checks

A container can be running but not actually serving requests correctly — the process started but is stuck, or the database connection failed. Health checks provide a way to verify the container is genuinely working:

Dockerfile — health check
FROM python:3.11-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt

# Run this command to check health
# --interval: how often to check
# --timeout: how long to wait for a response
# --retries: how many failures before marking unhealthy
HEALTHCHECK --interval=30s --timeout=5s --retries=3
  CMD curl -f http://localhost:8000/health || exit 1

CMD ["uvicorn", "app:app", "--host", "0.0.0.0"]
bash — checking health status
docker ps
CONTAINER ID  IMAGE         STATUS
a1b2c3d4e5f6  my-app:latest Up 2 mins (healthy)

# An unhealthy container
b2c3d4e5f6a7  my-app:latest Up 5 mins (unhealthy)
96

Environment-specific configuration

The same Docker image should run in development, staging, and production. The difference is configuration — database URLs, log levels, API endpoints. Pass these as environment variables, never bake them into the image:

bash — runtime configuration
# Development: connect to local database, verbose logging
docker run  -e DATABASE_URL=postgresql://localhost/devdb -e LOG_LEVEL=DEBUG my-app:latest

# Production: connect to production database, minimal logging
docker run  -e DATABASE_URL=postgresql://prod-db.example.com/proddb -e LOG_LEVEL=WARNING -e SECRET_KEY=very_long_random_string my-app:latest

In CI, pass test-specific configuration:

.github/workflows — environment variables in CI
      - name: Run smoke test
        env:
          DATABASE_URL: sqlite:///:memory:
          APP_ENV: test
        run: docker run --rm -e DATABASE_URL -e APP_ENV my-app:latest
97

Container registries

A container registry is a storage service for Docker images. When CI builds an image, it can push it to a registry. When a server deploys, it pulls from the registry.

RegistryUse caseNotes
Docker HubPublic images, open-source projectsFree for public images. Official images (python:3.11) live here.
GitHub Container Registry (GHCR)Private images linked to a GitHub repoFree for public repos. Integrates naturally with GitHub Actions.
AWS ECR / GCP Artifact RegistryEnterprise/cloud deploymentsPaid, but tightly integrated with cloud infrastructure.
.github/workflows — push image to GHCR
      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        run: |
          IMAGE=ghcr.io/${{ github.repository }}:${{ github.sha }}
          docker build -t $IMAGE .
          docker push $IMAGE
98

The deployment gap

This module brings the pipeline to a verified, packaged container image. Deploying that image to a real server — pulling it, stopping the old version, starting the new one — is the next step, and it is outside the scope of this module.

This is an intentional boundary. Real deployment involves infrastructure decisions (where are the servers? how many replicas?), orchestration (Kubernetes, AWS ECS), secrets management in production, rolling deployments with zero downtime, and monitoring. These are Year 2 topics.

What this module teaches is the foundation: building a pipeline that produces a verified, tagged, registry-stored image is the CI/CD team's output. The deployment infrastructure consumes that output.

99

Common production challenges

These challenges will appear in your exam and in your career. Understanding them at a conceptual level now prepares you for solving them later:

Image size bloat
Installing too many packages increases image size. Use --no-cache-dir with pip, use slim base images, and remove package manager caches after installation.
Secret leakage
Secrets passed as build ARGs can be extracted from image history. Always pass secrets at runtime as environment variables, never at build time.
Wrong CMD for the environment
A CMD designed for development (e.g. Flask's debug server) is not suitable for production. Use Gunicorn or Uvicorn with worker processes in production.
Missing env vars at startup
If a required environment variable is not set, the application crashes immediately. Write startup checks that fail loudly with a useful message.
Container can't connect to database
In production, the database is a separate service. The application needs the correct DATABASE_URL and the network must allow the connection.
100

Putting it all together

Here is the complete CI workflow integrating everything from Modules 6, 7, 8, and 9:

.github/workflows/ci.yml — complete
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  ci:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install and test
        run: |
          chmod +x run.sh
          ./run.sh install
          ./run.sh lint
          ./run.sh test

      - name: Build Docker image
        run: docker build -t my-app:${{ github.sha }} .

      - name: Smoke test container
        run: |
          docker run --rm \
            -e APP_ENV=test \
            my-app:${{ github.sha }} \
            python3 -c 'import app; print("Image OK")'
101

Key terms

smoke test
A minimal check that the application starts and runs without immediately crashing.
image tag
A label for a specific version of an image. Commit SHA is a common choice in CI.
container registry
A storage service for Docker images. GitHub Container Registry (GHCR) is free.
health check
A command run periodically inside a container to verify it is working correctly.
github.sha
A built-in GitHub Actions variable containing the current commit's hash.
--rm flag
Tells docker run to automatically delete the container when it exits.
deployment
The act of taking a verified image and running it on a production server. Covered in Year 2.
pipeline artefact
The output of a CI pipeline — in this case, a tagged, verified Docker image.
102

Exercises

✎ Lab exercises — approximately 55 minutes

Part A: Docker in the pipeline

  1. Take your working pipeline from Module 6.
  2. Write a Dockerfile for the Python application.
  3. Add a docker build step after the tests.
  4. Add a smoke test step: docker run --rm my-app:latest python3 -c "import app"
  5. Push and verify all four steps pass: install, test, build, smoke.

Part B: Tagging with commit SHA

  1. Update the build command to use ${{{{ github.sha }}}} as the tag.
  2. Push and check what tag appears in the Docker build log.
  3. Run docker images locally — can you see the image with its SHA tag?

Part C: Environment variables

  1. Add an APP_ENV environment variable to the smoke test step.
  2. Modify your Python code to print the value of APP_ENV on startup.
  3. Check the pipeline log — can you see it printing test?

Part D: Health check

  1. Add a simple /health endpoint to your Flask app that returns {"status": "ok"}.
  2. Add a HEALTHCHECK to the Dockerfile.
  3. Run the container detached: docker run -d -p 5000:5000 my-app:latest
  4. Check health: docker inspect | grep Health
  5. Check health via curl: curl localhost:5000/health