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.

Django with PortainerPush imageto registryPortainer stackcompose fileRedeploywebhook or GitProxy + TLSCaddy or TraefikPortainer runs your Compose file and adds a UI and redeploy triggers on top.
  1. Install Portainer CE on the Docker host and protect it (HTTPS, strong admin password, not exposed publicly if possible).
  2. Add your container registry (GHCR, Docker Hub, GitLab) under Registries.
  3. Create a stack from a Compose file with web, worker, db, redis, and a reverse proxy.
  4. Set environment variables in the stack editor or load them from a .env; use Docker secrets in Swarm mode.
  5. 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.

stack services and their roleImagePersistent datawebyour app imagenoneworkersame app imagenonedbpostgres:17!pgdata volumeredisredis:7optionalproxycaddy:2certificates volumeOnly the database and proxy hold data that must survive redeploysdjango-deployment.com
Back up the pgdata volume; Portainer does not do it for you.

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.

CI to running containers1CI builds and pushesghcr.io/acme/shop:SHA2Update TAG variableor Git commit3Webhook triggers redeployre-pull image4migrate service runsthen web starts5Check logs in Portainerroll back tag if neededThe stack definition stays in Git; Portainer applies itdjango-deployment.com
Pinning TAG to a SHA makes rollback a one-field change.

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 ./Caddyfile only 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

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.

Keep going