Deploy Django With Portainer Stacks
Deploy a Django app with Portainer: define a Compose stack with web, worker, Postgres, and Redis, pull images from a private registry, set environment variables and secrets, enable Git-based redeploys or webhooks, and add a reverse proxy for HTTPS.
Problem statement
You run Docker on a server and want a web interface to deploy and manage the Django app instead of SSH and docker compose commands: see container status and logs, update to a new image with a click or a webhook, and let teammates redeploy without shell access. Portainer manages Docker hosts and Swarm clusters through a UI. Its stacks are Docker Compose files, so a Django Compose setup moves over directly; the work is in image distribution, secrets, persistent data, and HTTPS.
- Install Portainer CE on the Docker host and protect it (HTTPS, strong admin password, not exposed publicly if possible).
- Add your container registry (GHCR, Docker Hub, GitLab) under Registries.
- Create a stack from a Compose file with
web,worker,db,redis, and a reverse proxy. - Set environment variables in the stack editor or load them from a
.env; use Docker secrets in Swarm mode. - Enable the stack webhook or Git repository polling so CI can trigger redeploys.
Step-by-step solution
1. Install Portainer
docker volume create portainer_data
docker run -d --name portainer --restart=always \
-p 127.0.0.1:9443:9443 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:lts
Binding to 127.0.0.1 keeps the UI off the internet; reach it with an SSH tunnel (ssh -L 9443:localhost:9443 server) or through a VPN such as Tailscale. Portainer controls the Docker socket, which is root-equivalent access to the host.
2. Define the stack
services:
web:
image: ghcr.io/acme/shop:${TAG:-latest}
command: gunicorn config.wsgi --bind 0.0.0.0:8000 --workers 3
env_file: stack.env
depends_on: [db, redis]
restart: unless-stopped
worker:
image: ghcr.io/acme/shop:${TAG:-latest}
command: celery -A config worker -l info
env_file: stack.env
restart: unless-stopped
db:
image: postgres:17
volumes: [pgdata:/var/lib/postgresql/data]
environment:
POSTGRES_DB: shop
POSTGRES_USER: shop
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
restart: unless-stopped
redis:
image: redis:7
restart: unless-stopped
proxy:
image: caddy:2
ports: ["80:80", "443:443"]
volumes: [./Caddyfile:/etc/caddy/Caddyfile:ro, caddy_data:/data]
restart: unless-stopped
volumes: { pgdata: {}, caddy_data: {} }
In Portainer, open Stacks, Add stack, paste the file or point it at a Git repository, and add environment variables in the editor. Portainer writes them to stack.env for env_file references.
3. Pull private images
Under Registries, add GHCR with a token that has read:packages. Stacks referencing ghcr.io/acme/shop then pull with those credentials. When redeploying, enable Re-pull image so Portainer fetches the new tag instead of reusing the cached one.
4. Run migrations on deploy
Add a one-off service that runs before web, or run migrations from the web container's entrypoint guarded by a lock. A simple, explicit pattern:
migrate:
image: ghcr.io/acme/shop:${TAG:-latest}
command: python manage.py migrate --noinput
env_file: stack.env
depends_on: [db]
restart: "no"
Make web depend on it with condition: service_completed_successfully so new code never starts against an old schema.
5. Automate redeploys
Portainer can redeploy a stack when:
- a webhook is called (available for stacks and services in recent versions; the URL is shown in the stack settings), or
- a Git repository changes, using polling with an interval or a webhook from your Git host.
From CI, after pushing the image:
curl -fsS -X POST "$PORTAINER_WEBHOOK_URL"
Keep the webhook URL secret; anyone with it can trigger a redeploy.
Explanation
Portainer does not change how containers run; it is a management layer over the Docker API. A stack in standalone mode is deployed with Compose, and in Swarm mode as a Swarm stack, which adds secrets, configs, and rolling updates. That means everything you know about running Django in Docker Compose applies, and the stack file can still be used from the command line if Portainer is unavailable.
Edge cases / notes
- Swarm vs standalone. Docker secrets and
deploy:settings require Swarm mode; on a standalone host use environment variables or mounted files. - Relative paths. Stacks created from the web editor have no repository checkout, so bind mounts like
./Caddyfileonly work with Git-based stacks or absolute paths. - Agent security. Managing remote hosts uses the Portainer Agent on port 9001; restrict it with firewall rules.
Next steps
- Review the Compose setup with Deploy Django With Docker Compose in Production.
- Route several apps with Deploy Django With Traefik and Docker.
- Check the container setup against the Django Docker Production Checklist.
FAQ
Is Portainer required to run Django in Docker?
No. It is a convenience layer. The same Compose file works with docker compose up -d.
Is it safe to expose Portainer to the internet?
Avoid it. Portainer controls the Docker socket. Keep it behind a VPN or SSH tunnel.
How do I run migrations with Portainer?
Add a one-off migrate service that web depends on, or run a command in a container from the UI before switching tags.
Can Portainer redeploy automatically from CI?
Yes, through stack webhooks or Git polling, after CI has pushed a new image.