On this page
What you are building
The service exposes a few endpoints for course data and depends on PostgreSQL. The goals are a small image that builds quickly, starts the same way everywhere, reads configuration from the environment and can be checked by the platform for health. The same principles apply to Flask, Django or any other Python web framework.
Write the Dockerfile
Order instructions so slow, rarely changing steps come first. Copy only the dependency file before installing, then copy the source. Run the application as a non-root user.
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
RUN useradd --create-home appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]PYTHONUNBUFFERED makes logs appear immediately in container output. Choose the Python version your project is tested against and pin it in the tag.
Exclude what does not belong
Create a .dockerignore listing .git, .venv, __pycache__, test data, local environment files and editor folders. This speeds up builds by shrinking the context sent to Docker and keeps secrets and personal files out of the image.
Run locally with Compose
Docker Compose describes the API and database together. Give the database a named volume so data survives restarts, and pass configuration through environment variables.
services:
api:
build: .
ports: ["8000:8000"]
environment:
DATABASE_URL: postgresql://app:app@db:5432/app
depends_on: [db]
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
volumes: ["pgdata:/var/lib/postgresql/data"]
volumes:
pgdata:These credentials are for local development only. In production, secrets come from the platform's secret store, never from a committed file. Note that depends_on controls start order, not readiness, so the API should retry its database connection briefly at startup.
Add a meaningful health check
Expose two endpoints: a liveness endpoint that returns success if the process is running, and a readiness endpoint that also checks the database connection. Platforms use readiness to decide when to send traffic, so a container that cannot reach its database is kept out of rotation instead of returning errors to users.
Configure through the environment
Read every setting, including the database URL, allowed origins and log level, from environment variables, validated once at startup. The same image then moves unchanged from development to staging to production, with only configuration differing. Rebuilding the image per environment makes it impossible to be sure that what you tested is what you deployed.
Verify before deploying
- Build from a clean checkout and confirm the image size is reasonable.
- Run the container with production-like environment variables and call the readiness endpoint.
- Confirm the process runs as the non-root user.
- Scan the image for known vulnerabilities with your registry or a scanning tool.
- Check that logs go to standard output and no secrets appear in them.
- Tag the image with the commit SHA or release version, not only latest.
Checklist
- Dependencies install before the source is copied
- A .dockerignore keeps the context small and clean
- The container runs as a non-root user
- Data lives in volumes or managed services
- Liveness and readiness are separate checks
- One image is promoted through environments by configuration
Share or cite this guide
Innovate with Ajay Editorial (2026-10-07). Containerise a Python API with Docker, from First Build to Deployment. https://innovatewithajay.com/articles/containerise-a-python-api-with-docker
