Django project template for deployment behind Traefik.
Next step: The steps from "Create your Django app" onwards:
https://brntn.me/blog/six-things-i-do-every-time-i-start-a-django-project/
Use python src/manage.py runserver — no Docker needed. Copy src/conf/local_settings.py.template to src/conf/local_settings.py and customise. Copy src/.env.template to src/.env and customise. Run migrations, create superuser.
This guide walks through deploying a Django app behind Traefik on a VPS. Traefik handles routing and automatic TLS certificates via Let's Encrypt. The Django app runs in a container with uWSGI serving static files, authenticated media, and the app itself.
User vs root: Git commands (clone, pull, etc.) must be run as the non-privileged user that owns the app files. Docker commands must be run as root (e.g. via sudo or a root shell).
Go to GitHub and create a PAT (Personal Access Token) to clone docker-traefik with. Give it read-only access to the contents and metadata permissions, and restrict it to the docker-traefik repo.
Now, clone (as the non-privileged user):
cd /home/user
git clone https://CapnKernel:PAT@github.com/CapnKernel/docker-traefik
cd docker-traefikThen start Traefik (as root):
docker compose up -dVerify Traefik is running:
docker ps
# Look for the traefik container, ports 80, 443, and 8080Traefik will auto-discover other containers on the shared_docker_network network via Docker labels created by apps.
(Run all of these from ~user/docker-traefik)
# See Traefik logs
docker compose logs -f
# See Traefik status
docker compose ps
# Run / start Traefik
docker compose up -d
# Restart Traefik
docker compose restart
# Stop Traefik
docker compose down
# Rebuild and run Traefik (e.g. after config changes)
docker compose up -d --force-recreateCreate a new GitHub repository using django-fresh as a template:
-
Click "Use this template" → "Create a new repository"
-
Name it (e.g.
myapp) -
Create a PAT for authentication, with read-only access to the content and metadata permissions, and restricted to the
myapprepo. -
Clone it onto the VPS:
# On the VPS, as the user who will own the app files: git clone https://CapnKernel:PAT@github.com/CapnKernel/myapp.git cd ~/myapp
Tip: To make copying commands from this readme easier, search/replace myapp with the name of your app.
Edit docker-compose.yml in your app repo. Set the environment variables and Traefik labels for your deployment.
Choose the example that matches your deployment pattern.
Example A: top-level site at https://myapp.afork.com/
name: myapp
services:
myapp:
build: .
image: myapp:latest
pull_policy: build
container_name: myapp_web
volumes:
- sqlite:/data/db
- media:/data/media
- static:/app/static
- env:/data/env
environment:
- DJANGO_SETTINGS_MODULE=conf.settings
- SITE_NAME=MyApp
- ALLOWED_HOSTS=myapp.afork.com,localhost
- SCRIPT_NAME=
- CSRF_TRUSTED_ORIGINS=https://myapp.afork.com
- EMAIL_DEFAULT_FROM=noreply@afork.com
- EMAIL_HOST=
- EMAIL_PORT=
- EMAIL_HOST_USER=
- EMAIL_HOST_PASSWORD=
- DBBACKUP_HOSTNAME=myapp
networks:
- shared_docker_network
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`myapp.afork.com`)"
- "traefik.http.routers.myapp.entrypoints=websecure"
- "traefik.http.routers.myapp.tls=true"
- "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
- "traefik.http.services.myapp.loadbalancer.server.port=8000"
volumes:
sqlite:
media:
static:
env:
networks:
shared_docker_network:
external: trueExample B: subpath site at https://vps.afork.com/myapp
name: myapp
services:
myapp:
build: .
image: myapp:latest
pull_policy: build
container_name: myapp_web
volumes:
- sqlite:/data/db
- media:/data/media
- static:/app/static
- env:/data/env
environment:
- DJANGO_SETTINGS_MODULE=conf.settings
- SITE_NAME=MyApp
- ALLOWED_HOSTS=vps.afork.com,localhost
- SCRIPT_NAME=/myapp
- CSRF_TRUSTED_ORIGINS=https://vps.afork.com
- EMAIL_DEFAULT_FROM=noreply@afork.com
- EMAIL_HOST=
- EMAIL_PORT=
- EMAIL_HOST_USER=
- EMAIL_HOST_PASSWORD=
- DBBACKUP_HOSTNAME=myapp
networks:
- shared_docker_network
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`vps.afork.com`) && PathPrefix(`/myapp`)"
- "traefik.http.routers.myapp.entrypoints=websecure"
- "traefik.http.routers.myapp.tls=true"
- "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
- "traefik.http.services.myapp.loadbalancer.server.port=8000"
volumes:
sqlite:
media:
static:
env:
networks:
shared_docker_network:
external: trueKey configuration points:
| Setting | Top-level | Subpath | Reason |
|---|---|---|---|
SCRIPT_NAME |
(empty) | /myapp |
Tells Django the URL prefix; uWSGI strips it from PATH_INFO so Django sees bare paths |
ALLOWED_HOSTS |
myapp.afork.com,localhost |
vps.afork.com,localhost |
Must match the Host header Traefik forwards |
CSRF_TRUSTED_ORIGINS |
https://myapp.afork.com |
https://vps.afork.com |
Required for POST requests over HTTPS |
| Traefik rule | Host(\myapp.afork.com`)` |
Host(\vps.afork.com`) && PathPrefix(`/myapp`)` |
Routes matching requests to this container |
(as root):
cd ~/myapp
docker compose up -dThis will:
- Build the Docker image (install Python deps, copy code, set up uWSGI)
- Create the named volumes (
sqlite,media,static,env) - Start the container on
shared_docker_network - Run migrations and collect static files on startup
- Traefik detects the new container via labels and starts routing traffic
docker compose exec myapp python manage.py createsuperuser --email admin@afork.comUse curl to check the site responds:
| Page | Top-level site | Subpath site |
|---|---|---|
| Main | curl -sI https://myapp.afork.com/ | head -5 |
curl -sI https://vps.afork.com/myapp/ | head -5 |
| Admin | curl -sI https://myapp.afork.com/office/ | head -5 |
curl -sI https://vps.afork.com/myapp/office/ | head -5 |
Expected: a 200 or 302 (redirect to login) response.
With examples:
- App (top-level):
https://myapp.afork.com/ - Admin (top-level):
https://myapp.afork.com/office/ - App (subpath):
https://vps.afork.com/myapp/ - Admin (subpath):
https://vps.afork.com/myapp/office/ - Traefik dashboard:
http://localhost:8080/(use ssh port forwarding)
| Pattern | SCRIPT_NAME |
Traefik rule |
|---|---|---|
| Top-level site | (empty) | Host(\myapp.afork.com`)` |
| Subpath site | /myapp |
Host(\vps.afork.com`) && PathPrefix(`/myapp`)` |
For a top-level site (e.g. https://myapp.afork.com/), set SCRIPT_NAME= (empty) and use a simple Host() rule.
For a subpath site (e.g. https://vps.afork.com/myapp), set SCRIPT_NAME=/myapp and use Host() && PathPrefix(/myapp). uWSGI's --mount + --manage-script-name handles prefix stripping.
| Variable | Description |
|---|---|
DJANGO_SETTINGS_MODULE |
Always conf.settings |
SITE_NAME |
Display name in templates |
ALLOWED_HOSTS |
Comma-separated hostnames |
SCRIPT_NAME |
URL prefix (empty for root, /myapp for subpath) |
CSRF_TRUSTED_ORIGINS |
Comma-separated origins |
EMAIL_DEFAULT_FROM |
From address for emails |
EMAIL_HOST |
SMTP server |
EMAIL_PORT |
SMTP port |
EMAIL_HOST_USER |
SMTP username |
EMAIL_HOST_PASSWORD |
SMTP password |
DBBACKUP_HOSTNAME |
Instance identifier in backup filenames |
All docker commands below must be run as root (e.g. via sudo or a root shell). Git commands (clone, pull, etc.) should be run as the non-privileged user.
The app uses four named volumes:
| Volume name | Mount point | Access | Purpose |
|---|---|---|---|
static |
/app/static/ |
ro | Collected static files (CSS, JS, images) |
sqlite |
/data/db/ |
rw | SQLite database file (db.sqlite3) |
media |
/data/media/ |
rw | User-uploaded files |
env |
/data/env/ |
rw | Persistent secrets (e.g. secret_key.txt) |
To browse the volumes, open a shell inside the container:
docker compose exec myapp bashUse standard commands to inspect:
# List files in a volume
ls -la /data/db/
# Read a file
cat /data/env/secret_key.txt
# Check database size
du -sh /data/db/# Copy a file into the container (e.g. restore a database backup)
docker cp ./backup.sql myapp_web:/data/db/
# Copy a file out of the container (e.g. download a media file)
docker cp myapp_web:/data/media/photo.jpg ./photo.jpg
# Copy the entire database out
docker cp myapp_web:/data/db/db.sqlite3 ./db.sqlite3# See app logs
docker compose logs -f
# See app status
docker compose ps
# Stop the app (keeps volumes, network)
docker compose down
# Restart the app
docker compose restart
# Run / start the app (builds image if needed)
docker compose up -d
# Rebuild the image and start (after code changes)
docker compose up -d --build
# Run Django management commands
docker compose exec myapp python manage.py check --deploy
docker compose exec myapp python manage.py createsuperuser --email admin@afork.com
docker compose exec myapp python manage.py dumpdata --indent 2 > backup.json
# Backup the SQLite database
docker compose exec myapp sh -c "cp /data/db/db.sqlite3 /data/db/db.sqlite3.bak"
# Restore the SQLite database from a local backup
docker cp ./db.sqlite3 myapp_web:/data/db/db.sqlite3
docker compose restart myapp
# Remove everything (volumes too — deletes database, media, secrets)
docker compose down -vThis is the nuke everything from orbit option. It wipes out all images, containers, volumes and build objects for all app, and traefik too. Run as root, works from any directory.
# Nuke
docker stop $(docker ps -aq); docker rm $(docker ps -aq); docker rmi -f $(docker images -q); docker volume rm $(docker volume ls -q); docker system prune -af --volumes
# Verify
docker ps -a; echo '---'; docker images; echo '---'; docker volume ls; echo '---'; docker system df