Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 59

Container Smoke Testing

Recommended resources

Hello! Welcome to the next lesson in our journey to master Docker for professional use.

In our last session, we dissected the two primary methods for running Docker commands within a CI/CD pipeline: Docker-in-Docker (DinD) and socket mounting (DooD). We established that while socket mounting is fast, it poses a significant security risk, making DinD the safer choice for most production environments.

This brings us to a crucial question: now that we have the means to build images in a secure and isolated CI job, what should we do immediately after a docker build command successfully completes? A successful build doesn't guarantee a working application.

Today, we'll address this by implementing a critical quality gate. You will learn how to perform post-build smoke tests on a running container to verify its health. This is a fundamental practice in DevOps to catch packaging errors early, long before an image is deployed to production.

Why a Post-Build Test is Essential

You might have a comprehensive suite of unit and integration tests that run before you even start the docker build process. So why add another test? The problem is that many things can go wrong during the packaging of the application into an image.

To understand the gap that smoke testing fills, let's start with a short article that clearly explains the problem.

Your Docker build needs a smoke test

This article, "Your Docker build needs a smoke test" by Itamar Turner-Trauring, explains why docker build and unit tests are not enough. It introduces the concept of a smoke test as a simple, effective solution.

Please read the sections "Building a broken image", "End-to-end tests are too late", and "The solution: a smoke test". Pay close attention to the failure modes that only a running container can reveal, and how a smoke test fits into the build process.

As the article highlights, a smoke test is not meant to be exhaustive. Its purpose is to catch catastrophic failures quickly. It answers the basic question: "Does the container even start and exhibit signs of life?"

Common issues that smoke tests catch include:

  • Crashing on startup: The application fails immediately due to a missing configuration file, an incorrect path, or a permissions issue.
  • Incorrect CMD or ENTRYPOINT: A typo in the Dockerfile's execution instruction.
  • Wrong network port: The application listens on port 8000, but the EXPOSE instruction says 3000.
  • Listening on the wrong interface: The server binds to 127.0.0.1 (localhost) instead of 0.0.0.0, making it inaccessible from outside the container.

Implementing a Smoke Test

There are several ways to run a smoke test. Let's explore two common patterns, from a simple script to a more robust setup using Docker Compose.

Pattern 1: The "Run, Test, Kill" Script

The most straightforward approach is a simple shell script within your CI job. This pattern is great for simple applications.

  1. Build the image: docker build -t my-app:test .
  2. Run the container in the background: docker run -d --name test-container -p 8080:80 my-app:test
  3. Wait for it to start: A sleep 5 is a naive but simple way. A better approach is a loop that polls the health endpoint.
  4. Test the endpoint: Use a tool like curl to hit a health check or status endpoint. curl --fail http://localhost:8080/api/health
  5. Clean up: Stop and remove the container. A great way to do this automatically is with the --rm flag.

Here's an example of what the core test might look like in a CI script:

# Build the image
docker build -t my-app:ci-test .

# Run the container with auto-cleanup, and fail the script if it doesn't start
echo "Starting container for smoke test..."
docker run -d --rm --name smoke-test-app my-app:ci-test

# Give the app a moment to initialize
sleep 5

# Test the health endpoint. If curl fails, the script will exit with an error.
echo "Running smoke test..."
curl --fail http://localhost:8080/health || (docker logs smoke-test-app && exit 1)

# If the test succeeds, stop the container
echo "Smoke test passed. Cleaning up..."
docker stop smoke-test-app

This works, but managing multiple services (like an app and a database) can become clumsy. That's where Docker Compose shines.

Pattern 2: Using Docker Compose for Integrated Testing

Since you're familiar with Docker Compose, you can leverage it to create a dedicated testing environment. This is a very common and powerful pattern in CI. The key is to have a separate Compose file for your CI tests, e.g., docker-compose.ci.yml.

To see this in action, the following article provides a clear, practical example within a GitHub Actions pipeline.

Smoke test your Docker Image in GitHub Actions

This guide, "Smoke test your Docker Image in GitHub Actions", demonstrates a clean way to use Docker Compose for smoke testing.

Read through the article to see how a docker-compose.ci.yml file is created. Focus on the docker-compose up command and its flags, especially --exit-code-from, which is the key to making this work in CI.

The core idea from the article is the command:
docker-compose -f docker-compose.ci.yml up --build --exit-code-from test

  • --build: Ensures the image is built from your latest code.
  • --exit-code-from test: This is the magic. Docker Compose will start all services, wait for the test service to exit, and then shut everything down. The exit code of the entire docker-compose command will be the exit code of your test service. If your tests pass (exit 0), the CI step succeeds. If they fail (exit non-zero), the CI step fails.

Putting It All Together in a CI Pipeline

Now let's see how these concepts play out in a real CI environment like GitLab CI. One of the trickiest parts is container networking.

The following video is excellent because it starts with a common mistake related to networking and then evolves the solution into a very clean, modern CI pattern. This directly builds on our previous lesson about DinD and container isolation.

How to test a Docker container in GitLab CI

This video, "How to test a Docker container in GitLab CI" by Valentin Despa, walks through the process of setting up a smoke test, hitting a common networking pitfall, and resolving it using CI services.

Watch from the start of the implementation (02:44) to the end of the improved curl test (12:09). Pay special attention to: The initial attempt using curl localhost and why it fails (04:39). The explanation of DinD networking and the fix using the service name (07:08). The more advanced pattern where the application itself is a service, and a separate curl image is used for testing (07:42).

As the video demonstrates, when you run containers in a CI system (or with Docker Compose), they are on a shared network. They do not communicate via localhost. They communicate using the service name as the hostname. This is a critical takeaway for any DevOps work.

The final pattern shown in the video is a best practice:

  • Define your newly built application container as a service in the CI pipeline, giving it an alias (e.g., my-app).
  • Your main CI job runs in a different, lightweight container that has the necessary testing tools (e.g., an image with curl or postman/newman).
  • The test script then runs commands against the service alias (e.g., curl http://my-app:80/health).

This separates the application under test from the test runner itself, leading to cleaner, more modular CI jobs.

Docker Run Command for Playwright Tests
This image shows the output of a `docker run` command executing a test suite (Playwright) inside a container. This is a concrete example of what a smoke or component test execution looks like in a CI log. The `--rm` flag ensures the container is cleaned up after the test run, a crucial practice for keeping CI runners tidy.
Test your understanding!

You are designing a CI pipeline for a microservice application that consists of a Node.js API and a Redis database. After building the API's Docker image, you need to run a smoke test. The test involves checking if the API can successfully connect to Redis and if the /health endpoint returns a 200 OK status.

Which smoke testing pattern would you choose and why? Briefly outline the components of your chosen solution.

Show answer

The Docker Compose pattern is the ideal choice here because the test has a dependency (the Redis database). Managing multiple services with simple shell scripts is complex and error-prone.

Here's how the solution would be structured:

  1. docker-compose.ci.yml file: This file would define three services:

    • api: The Node.js application service, configured to be built from the local Dockerfile.
    • redis: A service using the standard redis:alpine image.
    • tester: A service using a lightweight image like node:alpine or appropriate/curl. This service would contain the script to run the smoke test. Its command would be something like sh -c "curl --fail http://api:3000/health".
  2. CI Job Command: The CI job would execute a single command:
    docker-compose -f docker-compose.ci.yml up --build --abort-on-container-exit --exit-code-from tester

This setup ensures that a fresh, isolated environment with the API and its Redis dependency is created for every test run. The --exit-code-from tester flag ensures the entire CI step passes or fails based on the test result.


Conclusion

You now have a solid understanding of how to implement a post-build smoke test, a crucial step in creating professional, production-grade CI/CD pipelines. This simple check acts as a powerful safety net, saving you from deploying broken images and reducing the time spent debugging issues that should have been caught much earlier.

Key Takeaways:

  • Test After Building: A successful docker build is not enough. Always run a smoke test on the resulting image.
  • Catch Packaging Errors: Smoke tests are designed to find runtime configuration and packaging errors that unit tests miss.
  • Choose the Right Pattern: Simple docker run scripts work for simple cases, but using Docker Compose with --exit-code-from provides a robust and scalable solution for testing applications with dependencies.
  • Network by Service Name: In multi-container environments like Docker Compose or CI platforms, containers communicate using service names as hostnames, not localhost.

In this module, we've focused heavily on building, optimizing, and testing images to prepare them for deployment. We've built a CI pipeline that creates lean, secure, and now verified container images.

In our next lesson, we will begin our final module, "Preparing Applications for Kubernetes." We'll start by taking a step back to understand the fundamental relationship between the tools you've mastered and the world's leading container orchestrator. Our first outcome will be to explain the role of Docker as a container runtime within the Kubernetes ecosystem.