Troubleshooting
Platforms & PaaS

Fix DigitalOcean App Platform Health Check Failed for Django

Fix DigitalOcean App Platform deploys that fail with health check failed or container exited for Django: bind Gunicorn to the right port, configure the HTTP health check path, allow the internal host, give migrations a pre-deploy job, and size the instance.

Problem statement

The build finishes, then the deploy fails and rolls back:

ERROR failed health checks after 6 attempts with error
Readiness probe failed: dial tcp 10.244.12.7:8080: connect: connection refused

or:

Deploy Error: Health Checks
Your deploy failed because your container exited / health check failed.

App Platform starts your container and checks that it answers on the configured HTTP port (8080 unless you change it). If the check fails a number of times in a row, the new version is never put in front of traffic. For Django, the usual causes are Gunicorn listening on a different port or address, an HTTP health check that gets a redirect or 400 from Django, a start command that runs slow migrations, or a container that runs out of memory.

Why the deploy rolls backContainer startsrun commandHealth checkport 8080 path /No healthy answerrefused, 400, 301Deploy failsold version staysThe platform needs a fast 2xx on the configured port before it switches traffic.
  1. Bind Gunicorn to 0.0.0.0:$PORT; App Platform sets PORT to the component's HTTP port.
  2. Set the health check to a dedicated path such as /healthz/ that returns 200 without redirects.
  3. Exempt that path from SECURE_SSL_REDIRECT, and allow the platform's host in ALLOWED_HOSTS.
  4. Move migrate into a pre-deploy job instead of the run command.
  5. If logs show the container being killed, raise the instance size or lower Gunicorn workers.

Step-by-step solution

1. Read the runtime and deploy logs

In the app's Activity tab, open the failed deployment and view both the build and the runtime logs. From the CLI:

doctl apps logs <app-id> web --type run
doctl apps logs <app-id> --type deploy

connection refused means nothing listened on the port. A Django traceback or DisallowedHost means it listened but answered badly. No output at all after start usually means the process exited, often out of memory.

2. Listen on the right port and address

gunicorn config.wsgi:application --bind 0.0.0.0:$PORT --workers 2 --timeout 60

The component's HTTP Port setting and the port Gunicorn binds must match. Binding to 127.0.0.1 makes the app unreachable from the health checker, which connects over the pod network.

what the logs showLog showsCause and fixconnection refusedwrong port or 127.0.0.1;bind 0.0.0.0:$PORTDisallowedHost in logschecker host notallowed; dedicated pathhealth path returns 301SSL redirect; exempt the health pathno output, then exitOOM or slow migrate;resize or pre-deploy jobA healthy app that answers the wrong way still fails the checkdjango-deployment.com
Use the runtime log, not the deploy summary, to tell these apart.

3. Give the check a path Django answers simply

Configure the health check in the app spec:

services:
  - name: web
    http_port: 8080
    run_command: gunicorn config.wsgi --bind 0.0.0.0:$PORT --workers 2
    health_check:
      http_path: /healthz/
      initial_delay_seconds: 20
      period_seconds: 10
      failure_threshold: 6

And make the view cheap, with no redirect:

# urls.py
from django.http import HttpResponse
urlpatterns = [path("healthz/", lambda r: HttpResponse("ok")), ...]

# settings
SECURE_REDIRECT_EXEMPT = [r"^healthz/$"]

If Django logs DisallowedHost for the checker's internal address, either add that host pattern or answer /healthz/ in a small middleware placed before host validation. Do not set ALLOWED_HOSTS = ["*"].

4. Run migrations in a pre-deploy job

A run command such as python manage.py migrate && gunicorn ... delays the listener until migrations finish, and with several instances every one of them tries to migrate. Use a job component instead:

jobs:
  - name: migrate
    kind: PRE_DEPLOY
    run_command: python manage.py migrate --noinput

The job runs once before the new version starts, and a failed migration stops the deploy cleanly.

5. Check memory

Basic instances have little RAM. Three or four Gunicorn workers of 200 MB each exceed a 512 MB instance, and the container is killed during startup. Reduce workers or move up an instance size; the runtime metrics show memory per instance.

what happens on each deploy1Build the imagebuildpack or Dockerfile2PRE_DEPLOY jobmigrate --noinput3Start new containersGunicorn on $PORT4Health check /healthz/fast 200, no redirect5Traffic switchesold version retiredKeep slow work out of the step that the health check watchesdjango-deployment.com
A failed pre-deploy job stops the release before any container is replaced.

Explanation

App Platform runs components on Kubernetes under the hood. The health check is a readiness probe: until it passes, the new pods receive no traffic, and if it keeps failing, the deployment is marked failed and the previous version keeps serving. Probes come from inside the cluster, connect to the pod's IP on the HTTP port, and follow the same rules as any HTTP client. Django's host validation and HTTPS redirect, both correct for public traffic, can turn a healthy app into a failing probe.

Edge cases / notes

  • TCP checks. If you configure no HTTP path, App Platform checks only that the port accepts connections. That avoids host problems but hides a broken app.
  • Workers and jobs. Worker components have no HTTP port and no HTTP health check; they fail only if the process exits.
  • Dockerfile EXPOSE. EXPOSE does not set the port. The component's HTTP port setting does.

Next steps

FAQ

What port does App Platform expect?

The component's HTTP port, 8080 by default, exposed to your process as the PORT variable.

Why does the health check get a 301?

SECURE_SSL_REDIRECT redirects the plain HTTP probe to HTTPS. Exempt the health path.

Should migrations run in the run command?

No. Use a PRE_DEPLOY job so they run once and do not delay the health check.

How long does App Platform wait before failing?

It depends on the initial delay, period, and failure threshold in your health check settings. Raise the initial delay if the app needs time to warm up.

Keep going