-
Notifications
You must be signed in to change notification settings - Fork 0
Deployment
This setup runs Django with Gunicorn, serves static files through WhiteNoise, and stores application data in PostgreSQL.
Choose Docker Compose or a native host deployment.
-
compose.yamldefines shared Django and PostgreSQL services. -
compose.local.yamladds self-signed HTTPS on local port 8000. -
compose.prod.yamladds domain-based Nginx on public ports 80 and 443.
Always combine the shared file with exactly one environment-specific file.
Run infrastructure in containers while Django runs on the host with live reload:
docker compose -f compose.yaml -f compose.local.yaml up -d db redis mailpit adminer
make migrate
make runPostgreSQL is available at 127.0.0.1:5432, Redis at 127.0.0.1:6379, Mailpit at http://localhost:8025, and Adminer at http://localhost:8080.
Use 127.0.0.1 as POSTGRES_HOST in .env.
The containerized web service overrides that value with db automatically.
Stop local infrastructure with:
docker compose -f compose.yaml -f compose.local.yaml stop db redis mailpit adminerThis mode runs the production image, Gunicorn, PostgreSQL, Nginx, HTTPS redirects, and Django production security settings. It does not require Nginx or OpenSSL on the host.
Ensure .env trusts both local HTTPS origins:
CSRF_TRUSTED_ORIGINS=https://localhost:8000,https://127.0.0.1:8000Start the stack:
docker compose -f compose.yaml -f compose.local.yaml up --build -dOpen https://localhost:8000 and accept the self-signed certificate warning.
Nginx terminates TLS and proxies requests to Gunicorn without requiring Nginx or OpenSSL on the host.
Adminer is available at http://localhost:8080 with db as the database server.
Use the PostgreSQL credentials from .env to sign in.
View logs or stop the local stack:
docker compose -f compose.yaml -f compose.local.yaml logs -f nginx web
docker compose -f compose.yaml -f compose.local.yaml down- Docker Engine
- Docker Compose v2
- A server with public ports 80 and 443
- A domain whose DNS records point to the server
- A trusted TLS certificate for that domain
Create the production environment file:
cp .env.example .envSet these values before deployment:
DEBUG=False
SECRET_KEY=<generate-a-long-random-value>
DOMAIN=example.com
TLS_CERT_DIR=/etc/learn-django/certs
ALLOWED_HOSTS=example.com,www.example.com
CSRF_TRUSTED_ORIGINS=https://example.com,https://www.example.com
POSTGRES_DB_NAME=learn_django
POSTGRES_USER=learn_django
POSTGRES_PASSWORD=<generate-a-long-random-value>
POSTGRES_HOST=db
POSTGRES_PORT=5432
STORAGE_PROVIDER=localKeep .env private and never commit it.
Place the trusted certificate files from your certificate provider in TLS_CERT_DIR on the server:
/etc/learn-django/certs/fullchain.pem
/etc/learn-django/certs/privkey.pem
Certificate files are ignored by Git. Renew them through the certificate provider or deployment platform before they expire.
Build and start the services:
docker compose -f compose.yaml -f compose.prod.yaml up --build -dCompose waits for PostgreSQL, applies migrations, starts Gunicorn on internal port 8001, and exposes Nginx on ports 80 and 443.
Verify HTTPS and application readiness:
curl --fail https://example.com/health/Create an administrator:
docker compose -f compose.yaml -f compose.prod.yaml exec web .venv/bin/python manage.py createsuperuserRun Django's production checks:
docker compose -f compose.yaml -f compose.prod.yaml exec web .venv/bin/python manage.py check --deployThe production override terminates TLS in Nginx and forwards the original Host and HTTPS scheme to Django.
Only ports 80 and 443 are published.
Gunicorn and PostgreSQL remain private inside the Compose network.
Pull the new code and recreate the web service:
git pull
docker compose -f compose.yaml -f compose.prod.yaml up --build -dMigrations run automatically before Gunicorn starts.
View logs:
docker compose -f compose.yaml -f compose.prod.yaml logs -f nginx webStop services without deleting data:
docker compose -f compose.yaml -f compose.prod.yaml downBack up PostgreSQL:
docker compose -f compose.yaml -f compose.prod.yaml exec -T db sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' > backup.sqlNamed volumes retain PostgreSQL and local uploaded media.
Back up both volumes, or use STORAGE_PROVIDER=s3 for durable uploaded media.
Use this option when Docker and Docker Compose are unavailable or unwanted.
- Python 3.14
uv- PostgreSQL
- systemd or another process supervisor
- A server with ports 80 and 443 available for a reverse proxy
Create a dedicated operating-system user and clone the repository into a stable location such as /srv/learn-django.
The examples below use user learn-django and that directory.
Create a PostgreSQL database and role using your provider's control panel or PostgreSQL administration tools. Do not run Django with a PostgreSQL superuser.
Copy and configure the environment file:
cp .env.example .envUse production values and point Django at the PostgreSQL host:
DEBUG=False
SECRET_KEY=<generate-a-long-random-value>
ALLOWED_HOSTS=example.com,www.example.com
CSRF_TRUSTED_ORIGINS=https://example.com,https://www.example.com
POSTGRES_DB_NAME=learn_django
POSTGRES_USER=learn_django
POSTGRES_PASSWORD=<generate-a-long-random-value>
POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=5432
STORAGE_PROVIDER=localSet POSTGRES_HOST to the provider hostname when PostgreSQL runs on another server.
Keep .env readable only by the application user and never commit it.
Install locked production dependencies:
uv sync --frozen --no-dev --group prodApply migrations, collect static files, and run deployment checks:
uv run python manage.py migrate
uv run python manage.py collectstatic --noinput
uv run python manage.py check --deployCreate an administrator:
uv run python manage.py createsuperuserDo not use Django's development server in production.
Create /etc/systemd/system/learn-django.service:
[Unit]
Description=Learn Django Gunicorn service
After=network.target postgresql.service
[Service]
Type=simple
User=learn-django
Group=learn-django
WorkingDirectory=/srv/learn-django
ExecStart=/srv/learn-django/.venv/bin/gunicorn --config gunicorn.conf.py app.wsgi:application --bind 127.0.0.1:8000
Restart=on-failure
[Install]
WantedBy=multi-user.targetIf PostgreSQL is remote, remove postgresql.service from After.
Enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable --now learn-django
sudo systemctl status learn-djangoVerify application and database readiness:
curl --fail http://127.0.0.1:8000/health/Place Caddy, Nginx, or another TLS-terminating reverse proxy in front of 127.0.0.1:8000.
Forward the original Host and X-Forwarded-Proto headers.
Keep Gunicorn bound to localhost so it is not exposed publicly.
Pull code, synchronize dependencies, apply migrations, collect static files, then restart Gunicorn:
git pull
uv sync --frozen --no-dev --group prod
uv run python manage.py migrate
uv run python manage.py collectstatic --noinput
uv run python manage.py check --deploy
sudo systemctl restart learn-django
curl --fail http://127.0.0.1:8000/health/View logs:
sudo journalctl -u learn-django -fBack up PostgreSQL:
pg_dump --host="$POSTGRES_HOST" --username="$POSTGRES_USER" "$POSTGRES_DB_NAME" > backup.sqlBack up the media/ directory when STORAGE_PROVIDER=local.
When STORAGE_PROVIDER=s3, configure AWS S3 or an S3-compatible service such as Cloudflare R2 in .env and include the bucket in the backup policy.
Django Boilerplate