Back to Blog
Lesson 14 of the CI/CD: Continuous Integration from Scratch course
DevOpsJuly 14, 20264 min read

Dockerizing the Pipeline: Running CI Jobs in Containers

Stop worrying about "it works on my machine." Learn how to use Docker in GitHub Actions to achieve perfect environment parity and consistent CI test results.

DockerGitHub ActionsCI/CDDevOpsenvironment parity

Previously in this course, we covered Introduction to Docker to create local images, and before that, we mastered Running Tests in CI using standard GitHub-hosted runners.

While standard runners are convenient, they come pre-installed with a massive array of software. This can lead to "environment drift," where your code passes in CI because of a hidden system dependency, but fails in production. By defining a container in your GitHub Actions job, you ensure your tests execute in an exact, isolated environment.

Why Containerize Your CI Pipeline?

When you run a job on a default Ubuntu runner, you are at the mercy of whatever version of Python, Node.js, or system libraries GitHub has pre-installed. If your production environment uses a specific, minimal base image, your CI tests might be missing a dependency that only exists on the GitHub runner.

Environment parity is the practice of ensuring your development, testing, and production environments are identical. By running your CI job inside the same Docker image you use for deployment, you guarantee that if the tests pass, the environment is ready for your code.

Defining a Container in Your Workflow

To run a job inside a Docker container, we use the container key at the job level in our YAML workflow file. GitHub Actions will pull the specified image and execute all steps in that job inside a container created from that image.

Worked Example: Running Tests in a Python Container

Assume you have a Dockerfile in your repository that defines your application's environment. We want to ensure our tests run inside that specific environment.

Update your workflow file (e.g., .github/workflows/ci.yml) to include the container key:

YAML
name: CI Pipeline

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    # This defines the environment for the entire job
    container:
      image: python:3.11-slim
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Run tests
        run: pytest

When this job runs, GitHub Actions performs these steps:

  1. Provisions an ubuntu-latest runner.
  2. Pulls the python:3.11-slim image from Docker Hub.
  3. Starts a container from that image.
  4. Executes your checkout, pip install, and pytest commands inside that container.

Hands-on Exercise

  1. Open your repository's .github/workflows/ci.yml file.
  2. Identify the test job.
  3. Add the container: block as shown in the example above. You can use node:18-slim if you are working with a JavaScript project, or python:3.11-slim for Python.
  4. Commit and push the changes.
  5. Watch the "Actions" tab in your repository. You will notice a new "Initialize containers" step appearing in the job execution logs.

Common Pitfalls

  • Missing Dependencies: Since you are using a base image (like python:3.11-slim), it is much smaller than the standard GitHub runner. You might find that basic tools like git or curl are missing. If a step fails with "command not found," you may need to install it in the container or use a more comprehensive base image.
  • Docker Hub Rate Limiting: If you use a very popular base image, GitHub Actions might hit Docker Hub's pull rate limits. Using a digest (the unique SHA hash of the image) instead of a tag (like :latest) makes your pipeline more reliable and secure.
  • Permissions Issues: By default, the container runs as root. If your application logic expects a non-privileged user, you may need to add a user configuration to your container definition in the YAML.

FAQ

Q: Do I still need to use actions/checkout? A: Yes. Even though the job runs in a container, the container starts empty. You still need the checkout action to mount your repository code into the container's workspace.

Q: Can I use a private image? A: Yes, but you must configure credentials in your workflow to allow GitHub Actions to pull from your private registry (e.g., Amazon ECR or Docker Hub).

Q: Does this replace my local Docker setup? A: No, it complements it. You should still be able to build and run your Dockerfile locally to verify parity before pushing code.

Recap

By moving your job execution into a container, you achieve true environment parity, ensuring your CI environment matches your production requirements. We've defined the container key in our workflow and observed how it isolates our test suite from the host runner's pre-installed software.

Up next: We will explore Building Images in CI, where we'll learn how to package our code into a Docker image automatically every time we push to the repository.

Similar Posts