Skip to content

Type to search pages.

View .md

Django

Django needs nothing Antifailure specific either, and the interesting part is what it does not need. The engine does not want a SQL file or a psql in your image. It runs the command you already type:

services:
- name: web
kind: web
path: .
port: 8000
health_path: /health
migrate: "python manage.py migrate --noinput"

That runs against the branch rather than the golden, so a pull request that adds a field gets the column and nobody else’s environment does.

The working version of everything below is examples/django-api.

Read DATABASE_URL, and fail loudly without it

Section titled “Read DATABASE_URL, and fail loudly without it”

Antifailure injects DATABASE_URL pointing at the branch. Parsing it is the whole integration:

parsed = urlparse(os.environ["DATABASE_URL"])
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": parsed.path.lstrip("/"),
"USER": parsed.username or "",
"PASSWORD": parsed.password or "",
"HOST": parsed.hostname or "",
"PORT": str(parsed.port or 5432),
}
}

Raise if it is absent rather than falling back to a local database. A service that quietly connects to something else is worse than one that will not start, because the environment then reports ready and serves the wrong data.

Configure logging, or a 500 tells you nothing

Section titled “Configure logging, or a 500 tells you nothing”

This is the one that costs an afternoon. Django’s default configuration gates its console handler on DEBUG and sends request errors to mail_admins. In a DEBUG=False deployment, which is every deployment, an unhandled exception leaves an access log line reading 500 and nothing else. af logs web shows you the request and not the reason.

LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {"console": {"class": "logging.StreamHandler"}},
"loggers": {
"django.request": {"handlers": ["console"], "level": "ERROR", "propagate": False},
},
}

Standard output is where af logs reads. Without this the engine is showing you everything Django said, which is nothing.

ALLOWED_HOSTS, and why the example sets it to everything

Section titled “ALLOWED_HOSTS, and why the example sets it to everything”

The engine reaches a service through the ingress forwarder on the environment’s network, so the host header is not predictable and pinning it refuses the health check the manifest depends on.

The example sets ALLOWED_HOSTS = ["*"] and says at the setting why: an environment’s entire network is sealed by the egress proxy, and nothing outside it can reach the service at all. That reasoning is true inside an environment and false in production, so it is the one line in the example not to copy.

CMD ["gunicorn", "--bind", "0.0.0.0:8000", "config.wsgi:application"]

Inside a container localhost means that container, so a server bound there is a port that is open and that nothing outside can reach: a service that looks started and never becomes ready.

A fixture needs a second command in the manifest, and a second command is one more thing to forget. A data migration runs by the command already there:

class Migration(migrations.Migration):
dependencies = [("orders", "0001_initial")]
operations = [migrations.RunPython(seed, unseed)]

Make it reversible. A migration nobody dares run twice is a migration nobody runs.

Related: building services, masking.