Your container builds and runs. The image is optimized with multi-stage builds. But production environments demand more than a working container—they require security, observability, and resilience.
Consider what happens when your containerized FastAPI agent service goes to production. Kubernetes needs to know if your service is healthy before routing traffic to it. If your container runs as root, a security vulnerability could give an attacker full control of the host. If configuration is hardcoded, you'll need to rebuild images for every environment (dev, staging, production).
Production hardening addresses these concerns through three patterns: environment variable configuration (flexibility), health checks (observability), and non-root users (security). These aren't optional extras—they're requirements for any serious deployment. In this lesson, you'll add each pattern to your Dockerfile, understand why it matters, and end up with a production-ready container template you'll use for every AI service you build.
Before diving into implementation, understand what we're solving:
Each pillar is independent but together they form the foundation of production-ready containers. Let's implement each one.
Hardcoded configuration creates fragile containers. If your Dockerfile specifies LOG_LEVEL=INFO, you need a new image for debug logging. If API_HOST=production.example.com, you can't run locally.
Docker provides two instructions for configuration:
The ENV instruction sets environment variables that persist into the running container:
Your application reads these values at runtime.
Update main.py:
Output:
The -e flag overrides environment variables when starting a container:
Output:
The container uses DEBUG instead of the default INFO without rebuilding the image.
ARG defines build-time variables. They're available during docker build but not when the container runs:
Build with different Python versions:
Output:
Key distinction: If you need the value when the container RUNS, use ENV. If you only need it during BUILD, use ARG.
Orchestrators like Kubernetes need to know if your container is healthy. A container can be "running" but completely broken—the process exists but crashes on every request. Health checks detect this.
First, ensure your FastAPI service has a health endpoint.
Update main.py:
Test the endpoint:
Output:
The HEALTHCHECK instruction tells Docker how to verify container health:
Let's break down each component:
Why wget instead of curl?
Alpine images include wget by default but not curl. Using wget avoids adding dependencies. For slim-based images, use curl:
Build and run a container with the health check:
Wait 30 seconds for the first health check, then inspect:
Output:
The "Status": "healthy" confirms the health check passed. If the endpoint fails, status becomes "unhealthy" and FailingStreak increments.
Output:
Notice (healthy) in the STATUS column. This is how you quickly verify container health.
Clean up:
By default, Docker containers run as root. If an attacker exploits a vulnerability in your application, they have root access to the container—and potentially the host.
Running as a non-root user limits damage. Even if compromised, the attacker has limited privileges.
Add a dedicated user in your Dockerfile:
The flags:
After creating the user, switch to it with USER:
Files copied into the container are owned by root by default. Use --chown to set ownership:
Without --chown, the non-root user can't read the files:
Build and run, then check who's running the process:
Output:
Not root. The container runs with limited privileges.
Clean up:
Combining all three pillars—configuration, health checks, and non-root user—creates a production-ready template:
This template includes:
Create the required files.
Create requirements.txt:
Create main.py:
Build and run:
Output:
Verify all three pillars:
Output:
Output:
Output:
All three pillars verified. Clean up:
Placing USER appuser before COPY commands can cause permission errors:
Adding HEALTHCHECK without implementing the endpoint:
Result: Container marked unhealthy immediately.
Fix: Always implement the health endpoint in your application code.
The health check runs INSIDE the container. Use the container's internal port:
Never put sensitive data in Dockerfile ENV instructions:
Use -e flag at runtime or Docker secrets for sensitive configuration.
Before deploying any container to production, verify: