Stop Deploying Broken Docker Images: A Guide to Container Structure Tests

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

The 2 AM Pager Call: When Dockerfiles Lie

At 2:15 AM on a Tuesday, my phone started screaming. A critical microservice was stuck in a CrashLoopBackOff in production. The logs were infuriatingly simple: /usr/bin/python3: not found. I was baffled. I had just swapped a 450MB Ubuntu-based image for a 50MB ‘slim’ version, and it worked perfectly on my laptop. Or so I thought.

After twenty minutes of frantic digging, I found the culprit. The new base image had moved the Python binary to a different path. My entrypoint script was pointing to a ghost. We often trust our Dockerfiles blindly, but we rarely verify the actual artifacts until they hit a cluster. Manually inspecting layers for dozens of services is impossible and error-prone.

To solve this, you need Container Structure Tests (CST). Think of this as the missing unit testing layer for your infrastructure. This framework, open-sourced by Google, lets you run assertions against the container itself. You can check for specific files, command outputs, and metadata without spinning up a heavy integration environment.

Why Your Build Process is Currently Lying to You

Most developers assume that if docker build returns an exit code 0, the image is healthy. That’s a dangerous assumption. A successful build only proves your syntax was valid. It doesn’t guarantee the following:

  • The nginx.conf you injected has the correct 644 permissions.
  • Your node binary is actually in the $PATH.
  • Required environment variables like API_ENDPOINT are set.
  • The image isn’t running as root, which is a major security vulnerability.

CST allows you to codify these requirements into a simple YAML file. If the image doesn’t meet your exact specs, the CI/CD pipeline stops cold. No more 2 AM surprises.

Installing the Framework

The tool is a single, lightweight binary. It won’t bloat your CI runner or require a complex dependency chain. On Linux, you can install the 15MB binary with a few commands:

# Download the binary
curl -LO https://storage.googleapis.com/container-structure-test/latest/container-structure-test-linux-amd64

# Make it executable and move to your path
chmod +x container-structure-test-linux-amd64
sudo mv container-structure-test-linux-amd64 /usr/local/bin/container-structure-test

# Verify it works
container-structure-test version

Mac users can simply run brew install container-structure-test. Once installed, you can move from guessing to verifying.

Defining Your Test Suite

CST uses YAML to define expectations. I typically categorize my tests into three main areas: Commands, Files, and Metadata. Let’s look at a configuration for a standard Python API.

1. Command Tests: Verification of Binaries

This is my most-used test type. It ensures the environment is functional. It’s not enough for a file to exist; it must execute and return the expected output.

schemaVersion: "2.0.0"
commandTests:
  - name: "Verify Python version"
    command: "python"
    args: ["--version"]
    expectedOutput: ["Python 3.11.*"]
  - name: "Ensure pip is available"
    command: "pip"
    args: ["--version"]
    exitCode: 0

2. File Existence and Permissions

This would have caught my 2 AM incident. I also use this to enforce security policies, like making sure sensitive directories aren’t world-writable.

fileExistenceTests:
  - name: "App entrypoint check"
    path: "/app/main.py"
    shouldExist: true
    permissions: "-rw-r--r--"
  - name: "Security check: No SSH keys"
    path: "/root/.ssh/id_rsa"
    shouldExist: false

3. Metadata Validation

This section checks the “labels” and “environment” of the container. It’s perfect for ensuring your WORKDIR is consistent across all microservices.

metadataTest:
  envVars:
    - key: "PYTHONUNBUFFERED"
      value: "1"
  exposedPorts: ["8080"]
  workdir: "/app"
  user: "nonroot"

Execution: Running the Tests

With your config.yaml ready, run the test against your local image. I usually trigger this immediately after the build step. It takes less than 5 seconds to run a dozen tests.

container-structure-test test \
  --image my-python-app:v1.0.2 \
  --config tests.yaml

The output is concise. If a test fails, CST tells you exactly why—whether it was a regex mismatch in the output or a permission error. This feedback loop is what saves hours of debugging.

CI/CD Pipeline Integration

Automating this is where you get the most value. In my projects, CST acts as a quality gate. If the metadata test fails because someone forgot to set the USER to non-root, the build dies before it ever hits the registry.

Here is a GitHub Actions snippet for this workflow:

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Build Docker Image
        run: docker build -t my-app:${{ github.sha }} .

      - name: Run Container Structure Tests
        uses: sudo-bot/[email protected]
        with:
          image: my-app:${{ github.sha }}
          config: tests.yaml

      - name: Push Image
        if: success()
        run: docker push my-app:${{ github.sha }}

Common Pitfalls and Pro Tips

While the tool is straightforward, keep the execution environment in mind. Command tests run inside the container. If you use distroless images, you won’t have a shell. In those cases, you’ll need to rely almost entirely on fileExistenceTests and metadataTest.

Don’t try to cram everything into one giant file. For complex projects, I split tests into security.yaml and runtime.yaml. You can pass multiple --config flags to the tool, making your test suite much easier to maintain as your project grows.

Final Thoughts

Container Structure Tests aren’t meant to test your app’s business logic. That’s what unit tests are for. These tests validate the delivery vehicle itself. In a fast-paced environment where teams deploy dozens of times a day, we can’t afford infrastructure failures caused by simple typos.

Spend twenty minutes setting up these tests today. It’s a small investment that guarantees better system reliability and, more importantly, a full night’s sleep.

Share: