Deploy Django with CapRover

Deploy Django with CapRover: captain-definition, a Gunicorn Dockerfile, one-click PostgreSQL, container port and HTTPS settings, persistent media, and safe migrations.

Problem statement

CapRover is a self-hosted PaaS built on Docker Swarm and Nginx. You install it on a server, and it gives you a dashboard, a CLI, one-click databases, automatic Let's Encrypt certificates, and deploys from a tarball, a Git webhook, or a prebuilt image.

A Django app deployed to CapRover without adjustment usually hits one of four problems: the container listens on a different port from the one CapRover proxies to, HTTPS redirects loop because Django does not trust the proxy, uploaded files vanish on redeploy because the app was created without persistent data, or migrations never run. This guide deploys Django with each of these handled.

Django on CapRovercaprover deploytarball or imageBuildcaptain-definitionNginx proxyHTTPS, domainsSwarm serviceGunicorn replicasCapRover proxies to the container HTTP port you set; Gunicorn must listen on that same port.
  1. Install CapRover on a server with Docker and point a wildcard DNS record (*.apps.example.com) at it.
  2. Add a captain-definition file pointing at your Dockerfile.
  3. Create the app in the dashboard, ticking Has Persistent Data if it stores uploads on disk.
  4. Set Container HTTP Port to the port Gunicorn binds (for example 8000).
  5. Install PostgreSQL from one-click apps and connect with its internal srv-captain-- hostname.
  6. Add environment variables, enable HTTPS, then force HTTPS.
  7. Deploy with caprover deploy and run migrations in the start script (single instance) or as a one-off step.

Step-by-step solution

1. Install CapRover

On a server with Docker installed and ports 80, 443, and 3000 open:

docker run -p 80:80 -p 443:443 -p 3000:3000 \
  -e ACCEPTED_TERMS=true \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /captain:/captain \
  caprover/caprover

Create a wildcard A record, for example *.apps.example.com, pointing at the server. Then run the setup from your machine:

npm install -g caprover
caprover serverSetup

The setup changes the default dashboard password, sets the root domain, and enables HTTPS for the dashboard. After that, close port 3000 in your firewall because the dashboard is served on captain.apps.example.com over HTTPS.

2. Add captain-definition and a Dockerfile

At the repository root:

{
  "schemaVersion": 2,
  "dockerfilePath": "./Dockerfile"
}
captain-definition points at your Dockerfile{"schemaVersion":2,"dockerfilePath": "./Dockerfile"}121The current schema; version 1 is legacy.2Your own production Dockerfile, the same one you test locally.CapRover builds the image on the server from this filedjango-deployment.com
Building on a small server can run out of memory; build in CI and deploy an image instead.
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 DJANGO_SETTINGS_MODULE=config.settings.build python manage.py collectstatic --noinput
RUN useradd --create-home app && mkdir -p /app/media && chown -R app:app /app/media
USER app
EXPOSE 8000
CMD ["sh", "-c", "python manage.py migrate --noinput && exec gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 3 --access-logfile -"]

The build runs on the CapRover server, so it needs enough memory for pip install. On small servers, build the image in CI and deploy it by image name instead.

3. Create the app

In Apps, create myapp. Tick Has Persistent Data if uploaded media will be stored on the server's disk. This cannot be changed later without recreating the app.

In the app's App Configs:

  • Container HTTP Port: 8000.
  • Persistent Directories: map /app/media to a named volume (only available for apps created with persistent data).
  • Instance count: keep 1 while migrations run in the start command.

4. Create PostgreSQL

From One-Click Apps, install PostgreSQL with a strong password. CapRover runs it as a service named srv-captain--<app-name> on its overlay network, reachable only from other CapRover apps. Do not expose its port publicly.

5. Set environment variables

In the Django app's App Configs, add:

DJANGO_SETTINGS_MODULE=config.settings.production
DJANGO_SECRET_KEY=long-random-value
DATABASE_URL=postgres://postgres:strong-password@srv-captain--myapp-db:5432/postgres
DJANGO_ALLOWED_HOSTS=example.com,myapp.apps.example.com
DJANGO_CSRF_TRUSTED_ORIGINS=https://example.com

Saving the configuration restarts the service with the new values.

6. Configure Django for CapRover's proxy

CapRover's Nginx terminates TLS and passes X-Forwarded-Proto to the container:

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
ALLOWED_HOSTS = os.environ["DJANGO_ALLOWED_HOSTS"].split(",")
CSRF_TRUSTED_ORIGINS = os.environ["DJANGO_CSRF_TRUSTED_ORIGINS"].split(",")
CapRover sits in front of your containerCapRover NginxTLS, domains, X-Forwarded-ProtoDocker Swarm servicesrv-captain--myappContainer on port 80Gunicorn, as set in the appDjangoSECURE_PROXY_SSL_HEADER,ALLOWED_HOSTSThe container port setting must match the port Gunicorn bindsdjango-deployment.com
Other services reach the database by its srv-captain-- name on the overlay network.

You can let CapRover handle the HTTP-to-HTTPS redirect with Force HTTPS and leave SECURE_SSL_REDIRECT off, or enable both. Without SECURE_PROXY_SSL_HEADER, turning on SECURE_SSL_REDIRECT causes an infinite redirect loop, because Django sees every proxied request as plain HTTP.

7. Domains and HTTPS

In HTTP Settings, add example.com as a custom domain (after pointing its A record at the server), click Enable HTTPS for each domain, and then enable Force HTTPS. CapRover renews the certificates.

For large uploads, raise the proxy body size by editing the app's Nginx configuration template in the same screen (client_max_body_size).

8. Deploy

From the project directory:

caprover deploy --appName myapp
a CapRover deploy, start to finishcaprover deploytarball uploadedImage builtfrom the DockerfileNew container startsservice updatedOld container removedtraffic movesMigrate runone-off, if neededWithout a health check, a crashing container can still replace a working onedjango-deployment.com
Keep previous image versions so the dashboard can roll back to one.

This uploads a tarball of the committed files and builds on the server. To deploy a prebuilt image instead:

caprover deploy --appName myapp --imageName ghcr.io/example/myapp:1.4.0

Verification check:

  • The deployment log shows a successful build and the service starting.
  • curl -sI https://example.com/ returns 200 (or your expected redirect) with a valid certificate.
  • curl -sI http://example.com/ redirects to HTTPS exactly once.
  • An uploaded file survives a second deploy.

Explanation

CapRover wraps Docker Swarm services with an Nginx front end. Each deploy builds or pulls an image and updates the Swarm service; Nginx routes each domain to the service's container HTTP port. Because Swarm replaces containers on every update, only volumes declared through persistent directories survive, and only the settings in App Configs define the runtime environment. Django sits behind two proxies' worth of conventions, so the forwarded-protocol header and allowed hosts are what tie it together.

Edge cases / notes

Scaling beyond one instance

Persistent-data apps are pinned to a node, and concurrent migrations are unsafe. Before raising the instance count, move media to object storage and run migrations as a separate step, for example a one-off container using the same image, before updating the service.

Rollback

Redeploy the previous image tag with caprover deploy --imageName. The dashboard's deployment section also lists previous versions. Migrations are not reversed automatically.

Celery workers

Create a second app without HTTP (tick Do not expose as web-app), deploy the same image, and override the command through a different captain-definition or a Dockerfile CMD selected by an environment variable.

When to automate this

Replace manual caprover deploy runs with a CI job that builds an image, pushes it to a registry, and calls caprover deploy --imageName with an app token. The one-time pieces (wildcard DNS, one-click database, persistent directories) are worth documenting in a short runbook.

Next steps

FAQ

What port should Django use on CapRover?

Any port, as long as the app's Container HTTP Port setting matches the port Gunicorn binds inside the container. The default setting is 80.

Why do my Django uploads disappear after a CapRover deploy?

The app was created without persistent data, or the media path is not mapped to a persistent directory. Recreate the app with persistent data or use object storage.

How do I connect Django to a CapRover PostgreSQL one-click app?

Use the service hostname srv-captain--<database-app-name> and the password you set during installation. It is reachable only from apps on the CapRover network.

Can CapRover deploy from GitHub automatically?

Yes. Configure the repository in the app's deployment settings and add the webhook URL CapRover provides to the repository, or deploy from CI with the CLI.

Keep going