32.10. Dockerizing the Application
Table of Contents
Why Dockerize Your Final Project
For your final project, you want your backend to run the same way:
- On your laptop
- On any teammateβs laptop
- On CI servers
- On production servers
Docker gives you this consistency by packaging:
- Your code
- Your runtime (Python)
- System libraries
- Configuration (environment variables, ports, commands)
into a single image that you can run as a container anywhere Docker is installed.
In this chapter you will:
- Write a Dockerfile for your backend
- Build and run the image locally
- Add environment-based configuration
- Connect your app container to Postgres and Redis containers
- Prepare images for production
You already learned Docker earlier, so here you focus on applying it to a real, multi-service backend.
Designing the Container Layout
What will run in containers?
In previous chapters, your final project got these pieces:
- FastAPI (application)
- PostgreSQL (database)
- Redis (cache / background jobs)
- A background worker (Celery / RQ / custom script)
- Possibly a reverse proxy (Nginx) for production
A simple layout for now:
| Container | Purpose |
|---|---|
app | FastAPI backend |
worker | Background jobs |
db | PostgreSQL database |
redis | Redis for cache / tasks |
nginx (later) | Reverse proxy, HTTPS, static |
In this chapter you focus on app and worker images, and wiring them to db and redis using Docker Compose.
Creating a Production-Friendly Dockerfile
Basic Dockerfile structure
Here is a typical Dockerfile for a FastAPI / Uvicorn app in your project:
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_DEFAULT_TIMEOUT=100
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --upgrade pip \
&& pip install -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "project.main:app", "--host", "0.0.0.0", "--port", "8000"]Important parts:
FROM python:3.12-slimuses a small base imageWORKDIR /appsets the working directoryCOPY requirements.txtthenpip installfor dependenciesCOPY . .copies your codeEXPOSE 8000documents the app portCMD [...]runs Uvicorn with your FastAPI application
RULE: Always use a specific Python version tag like python:3.12-slim, not just python:latest. This keeps your builds reproducible.
Aligning the Dockerfile with your project structure
Assume your final project has a structure like:
/app
βββ project
β βββ __init__.py
β βββ main.py # FastAPI app
β βββ config.py
β βββ db.py
β βββ ...
βββ requirements.txt
βββ alembic.ini
βββ alembic
βββ scripts
βββ worker.py
Then your CMD should reference the correct module path:
- If FastAPI instance is
appinproject/main.py, use
project.main:app
You must match the module path exactly:
| File path | FastAPI instance | Correct Uvicorn target |
|---|---|---|
project/main.py | app = FastAPI() | project.main:app |
backend/app.py | api = FastAPI() | backend.app:api |
src/api/main.py | api = FastAPI() | src.api.main:api |
If you get an error like ImportError: No module named project, your module path or WORKDIR is probably wrong.
Using `.dockerignore`
Without a .dockerignore file, Docker sends everything in your project directory into the build context. This is slow and may copy secrets or build artifacts.
Create .dockerignore next to your Dockerfile:
.git
.gitignore
__pycache__
*.pyc
*.pyo
*.pyd
.env
.env.*
.vscode
.idea
.mypy_cache
.pytest_cache
.dist
build
htmlcov
.coverage
node_modules
RULE: Never copy .env or secret files into the image. Keep secrets outside images and pass them as environment variables at runtime.
Building and Running Locally
Building the image
From your project root, build the image:
docker build -t my-final-backend:dev .Explanation:
-t my-final-backend:devsets the image name and tag.means βuse this directory as the build contextβ
You can list images:
docker imagesRunning the container
Run the application:
docker run --rm -p 8000:8000 my-final-backend:devFlags:
-p 8000:8000mapslocalhost:8000on your machine to port8000in the container--rmremoves the container when it exits
Open in a browser:
- http://localhost:8000/docs
If your app uses environment variables for DB or Redis, the container might fail because those services are not running yet. That is where Docker Compose helps.
Environment-Based Configuration
Your app should already read configuration from environment variables, for example in project/config.py:
from pydantic import BaseSettings, AnyUrl
class Settings(BaseSettings):
env: str = "development"
database_url: AnyUrl
redis_url: AnyUrl
secret_key: str
debug: bool = False
class Config:
env_file = ".env" # for local dev only
settings = Settings()
In Docker, do not rely on .env inside the image. Instead pass variables from the host or compose file.
Example environment variables
ENV=development
DATABASE_URL=postgresql+psycopg2://user:password@db:5432/app_db
REDIS_URL=redis://redis:6379/0
SECRET_KEY=super-secret-key-change-me
DEBUG=true
Notice db and redis in the URLs. Those will be the service names in Docker Compose.
Using Docker Compose for the Full Stack
Basic `docker-compose.yml` for the final project
Create docker-compose.yml in your project root:
version: "3.9"
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: final-backend-app
command: >
uvicorn project.main:app
--host 0.0.0.0
--port 8000
--reload
ports:
- "8000:8000"
env_file:
- .env.docker
depends_on:
- db
- redis
worker:
build:
context: .
dockerfile: Dockerfile
container_name: final-backend-worker
command: ["python", "-m", "scripts.worker"]
env_file:
- .env.docker
depends_on:
- db
- redis
db:
image: postgres:16-alpine
container_name: final-backend-db
environment:
POSTGRES_USER: app_user
POSTGRES_PASSWORD: app_password
POSTGRES_DB: app_db
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
container_name: final-backend-redis
ports:
- "6379:6379"
volumes:
postgres_data:Key points:
appandworkerare built from the same project Dockerfileworkeruses a different command, here a Python module for background tasksdbandredisuse official images- All services share a default network created by Compose, so they can talk via
dbandredishostnames
`.env.docker` for compose
Create .env.docker:
ENV=development
DATABASE_URL=postgresql+psycopg2://app_user:app_password@db:5432/app_db
REDIS_URL=redis://redis:6379/0
SECRET_KEY=dev-secret-key-change-me
DEBUG=trueRun the stack:
docker compose up --build
Use docker compose or docker-compose depending on your system.
Visit:
- http://localhost:8000/docs
To stop:
docker compose downDatabase Migrations Inside Containers
Your project likely uses Alembic. You must decide when and where to run migrations.
Option 1: run migrations manually inside app container
After docker compose up -d, run:
docker compose exec app alembic upgrade head
This uses the app container, your DATABASE_URL from .env.docker, and your Alembic config.
Option 2: run migrations at startup
You can wrap migration logic in a small script, for example:
# scripts/run_with_migrations.py
import subprocess
from project.main import app # ensure app imports correctly
def run_migrations():
subprocess.check_call(["alembic", "upgrade", "head"])
if __name__ == "__main__":
run_migrations()
Then change command in compose for app:
command: >
sh -c "alembic upgrade head &&
uvicorn project.main:app --host 0.0.0.0 --port 8000"RULE: Never run destructive migrations (like dropping tables) automatically in production startup without backups or approvals. Prefer safe, forward-only migrations.
Developing With Hot Reload in Docker
For development you may want Uvicorn reload and local code changes reflected without rebuilding the image.
Update docker-compose.yml:
services:
app:
build:
context: .
dockerfile: Dockerfile
volumes:
- .:/app
command: >
uvicorn project.main:app
--host 0.0.0.0
--port 8000
--reload
# ...
Now the code on your host is mounted into the container at /app. Changes trigger --reload.
Use this only for development. For production, do not mount source code from the host.
Multi-Stage Builds for Lean Production Images
Development images are often bigger, because they include build tools and caches. For production you want smaller and safer images.
Use a multi-stage Dockerfile:
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --upgrade pip \
&& pip install --prefix=/install -r requirements.txt
FROM python:3.12-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .
EXPOSE 8000
CMD ["uvicorn", "project.main:app", "--host", "0.0.0.0", "--port", "8000"]Explanation:
builderstage uses compilers to build wheels, then installs packages into/installruntimestage copies only installed packages and your code, but not compilers or apt tools
Result is a smaller, more secure image.
Separate Images for App and Worker (Optional)
You can use one image for both app and worker, as shown. Sometimes you want different Python dependencies for your worker or a different base image.
Example Dockerfile.worker:
FROM python:3.12-slim
WORKDIR /app
COPY requirements-worker.txt requirements.txt
RUN pip install --upgrade pip \
&& pip install -r requirements.txt
COPY . .
CMD ["python", "-m", "scripts.worker"]
Then in docker-compose.yml:
worker:
build:
context: .
dockerfile: Dockerfile.worker
env_file:
- .env.docker
depends_on:
- db
- redisIf your worker needs image libraries or other system packages, you add them only to the worker image.
Logging and Observability in Containers
Your application should log to stdout and stderr. Docker captures these streams and you can view logs with:
docker compose logs app
docker compose logs worker
docker compose logs dbTo follow logs:
docker compose logs -f app workerFor production, structured logs (JSON) are easier for log aggregation tools.
Example FastAPI logging config snippet:
import logging
import sys
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s %(message)s",
stream=sys.stdout,
)Security Considerations for Dockerizing a Backend
A few important rules for your final project:
RULES FOR SECURE DOCKERIZED BACKENDS
- Do not bake secrets into Docker images. Use environment variables or secret managers.
- Run your app as a non-root user in the container when possible.
- Expose only needed ports.
- Use official base images and keep them updated.
- Do not mount host directories in production, except where absolutely necessary.
- Keep your Dockerfiles small and clear, no unused packages.
Example of running as a non-root user:
RUN adduser --disabled-password --gecos "" appuser
USER appuser
Place USER appuser near the end of your Dockerfile, before CMD.
Building Images for Production and Pushing to a Registry
For production you will:
- Build a production-tagged image
- Push to a registry (like Docker Hub or GitHub Container Registry)
- Pull and run on your production server
Example:
# build with a version tag
docker build -t myorg/final-backend:1.0.0 .
# login to Docker Hub
docker login
# push
docker push myorg/final-backend:1.0.0In your production deployment chapter you will wire this image into your CI/CD pipeline and server setup.
Checklist for Your Final Project Dockerization
Use this checklist to verify your project is ready:
| Item | Done? |
|---|---|
| Dockerfile builds and runs the FastAPI app | [ ] |
.dockerignore is present and excludes secrets | [ ] |
docker-compose.yml runs app, db, and Redis | [ ] |
worker container is configured and running | [ ] |
Env vars in .env.docker match your config module | [ ] |
App can connect to Postgres (db) and Redis | [ ] |
| Database migrations run correctly in containers | [ ] |
Dev setup supports hot reload (--reload) | [ ] |
| Production image uses a fixed Python version | [ ] |
| No secrets are baked into images | [ ] |
Once you complete this, your final project backend can be started with a single command:
docker compose up --buildand is ready to be deployed in production using the same images.
Views: 9
KAHIBARO