Packaging your app in a container means the server runs exactly what you tested, with the same Python version and libraries. This guide shows how to dockerize a Python app (Flask or Django) and run it with PostgreSQL and Redis using Docker Compose on a VPS.
Before you start
- Docker Engine with the Compose plugin on your VPS and, ideally, on your computer for testing.
- An app with a
requirements.txtthat includesgunicorn(andpsycopg[binary]for PostgreSQL). - Settings read from environment variables (database URL, secret key, debug flag).
Write the Dockerfile
Create Dockerfile in the project root:
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 . .
RUN useradd -m appuser
USER appuser
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "myproject.wsgi:application"]- Copying
requirements.txtfirst lets Docker cache the dependency layer, so code changes rebuild in seconds. PYTHONUNBUFFEREDsends logs straight todocker logs.- Running as a non-root user limits the damage of a compromised app.
- For Flask, use
app:appinstead of the Django WSGI path.
Add a .dockerignore so secrets and junk stay out of the image:
.git
.env
__pycache__/
*.pyc
venv/
media/Write docker-compose.yml
services:
web:
build: .
restart: unless-stopped
env_file: .env
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
ports:
- "127.0.0.1:8000:8000"
volumes:
- static:/app/staticfiles
- media:/app/media
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
retries: 10
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
pgdata:
static:
media:The matching .env (never committed):
POSTGRES_PASSWORD=change-me
DATABASE_URL=postgresql://app:change-me@db:5432/app
REDIS_URL=redis://redis:6379/0
SECRET_KEY=long-random-string
DEBUG=0
ALLOWED_HOSTS=example.comServices reach each other by name (db, redis) on Compose's private network; neither is published to the internet.
Build, migrate and start
docker compose up -d --builddocker compose exec web python manage.py migratedocker compose exec web python manage.py collectstatic --noinputFor Flask with Flask-Migrate, run flask db upgrade instead. Create a Django admin user with docker compose exec web python manage.py createsuperuser.
Serve it over HTTPS
Put Nginx (on the host or as another container) in front of 127.0.0.1:8000 with Certbot for SSL. Serve static files directly from Nginx or with WhiteNoise inside the app. Pass X-Forwarded-Proto and, in Django, set SECURE_PROXY_SSL_HEADER so the app knows requests are HTTPS.
Deploy updates
git pull && docker compose up -d --build && docker compose exec web python manage.py migrateCompose only recreates containers whose image or config changed. Clean old images occasionally with docker image prune.
Check that it works
docker compose ps: all services running, db healthy.docker compose logs -f web: Gunicorn booted workers without errors.curl -I http://127.0.0.1:8000returns 200 or a redirect.
Troubleshooting
- could not translate host name "db": the app runs outside Compose or uses a different network. Run it as a Compose service.
- Connection refused to db on startup: the app started before PostgreSQL was ready; use the health check condition shown above.
- Static files 404: run
collectstaticand make sure Nginx or WhiteNoise servesSTATIC_ROOT. - Permission denied writing media: the volume is owned by root while the app runs as
appuser. Fix ownership once withdocker compose exec -u root web chown -R appuser /app/media.
Python Docker FAQ
Should I use Alpine for Python images?
Usually not: many Python wheels are built for glibc, so slim images install faster and avoid compile problems.
Do I still need Gunicorn inside Docker?
Yes. Docker only runs the process; Gunicorn is the production server that handles concurrent requests.
How do I back up the database?
Run docker compose exec -T db pg_dump -U app -Fc app > app.dump on a schedule and copy the file off the server.
