Deploy Django on Upsun (Formerly Platform.sh)

Deploy Django on Upsun, formerly Platform.sh: write the application, services, and routes configuration, run collectstatic in the build hook and migrate in the deploy hook, read service credentials from relationships, and use mounts and preview environments.

Problem statement

Upsun (the platform formerly known as Platform.sh) builds every Git branch into a full environment: code, database, cache, and routes, cloned from production data if you want. That makes preview environments easy, but the platform is opinionated. The application filesystem is read-only at runtime, builds have no access to services, credentials arrive through relationships, and everything is described in one YAML file. A Django project needs a few adjustments to fit.

Django on UpsunConfig YAMLapp, services, routesBuild hookinstall, collectstaticDeploy hookmigrateBranch envscloned dataBuild runs without services; the deploy hook runs with them, so migrations go there.
  1. Add .upsun/config.yaml with an application (type: python:3.12), a PostgreSQL service, and a route.
  2. Install dependencies and run collectstatic in the build hook (no database access there).
  3. Run migrate in the deploy hook, which runs with services available.
  4. Read database credentials from the relationship variables or PLATFORM_RELATIONSHIPS.
  5. Declare writable mounts for media or temporary files; everything else is read-only at runtime.

Step-by-step solution

1. Describe the project

# .upsun/config.yaml
applications:
  shop:
    type: "python:3.12"
    hooks:
      build: |
        set -eu
        pip install -r requirements.txt
        python manage.py collectstatic --noinput
      deploy: |
        set -eu
        python manage.py migrate --noinput
    web:
      commands:
        start: "gunicorn config.wsgi --bind unix:$SOCKET --workers 3"
      upstream:
        socket_family: unix
    relationships:
      database: "db:postgresql"
      cache: "cache:redis"
    mounts:
      "media":
        source: storage
        source_path: media
services:
  db:
    type: postgresql:16
  cache:
    type: redis:7.2
routes:
  "https://{default}/":
    type: upstream
    upstream: "shop:http"
  "https://www.{default}/":
    type: redirect
    to: "https://{default}/"

Check the supported service versions in Upsun's documentation; they change over time. The legacy Platform.sh format used .platform.app.yaml, .platform/services.yaml, and .platform/routes.yaml; the concepts are the same.

2. Make settings read relationships

Upsun passes service credentials through environment variables. The traditional source is PLATFORM_RELATIONSHIPS, base64-encoded JSON:

import base64, json, os

rels = json.loads(base64.b64decode(os.environ.get("PLATFORM_RELATIONSHIPS", "e30=")))
if "database" in rels:
    db = rels["database"][0]
    DATABASES = {
        "default": {
            "ENGINE": "django.db.backends.postgresql",
            "NAME": db["path"], "USER": db["username"], "PASSWORD": db["password"],
            "HOST": db["host"], "PORT": db["port"],
            "CONN_MAX_AGE": 60,
        }
    }

Recent Upsun versions also expose per-relationship variables (for example DATABASE_HOST, DATABASE_URL) named after the relationship; if your project has them, dj-database-url reads DATABASE_URL directly. The default value "e30=" is {} so the code also works locally and during the build.

build hook versus deploy hookBuild hookDeploy hookpip installyesnocollectstaticyesnomigrate!noyesServices reachable!noyesFilesystemwritableread-only + mountsAnything that needs the database belongs in the deploy hookdjango-deployment.com
Settings must import during the build without database credentials.

3. Configure hosts and security

Each environment gets its own generated domain. Upsun exposes routes in PLATFORM_ROUTES; the simplest approach is a wildcard for the platform domain plus your production domain:

ALLOWED_HOSTS = [".platformsh.site", ".upsun.app", "shop.example.com"]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", os.environ.get("PLATFORM_PROJECT_ENTROPY", ""))

PLATFORM_PROJECT_ENTROPY is a per-project random value that works as a fallback secret; set your own DJANGO_SECRET_KEY variable for production.

4. Static and media files

collectstatic writes into the application directory at build time, which is then read-only. Serve those files with WhiteNoise or a locations entry in the web configuration. Uploaded media must go to a mount (media above, shared storage) or to object storage; writing anywhere else fails with a read-only filesystem error.

5. Workers and preview environments

Add a workers: entry for Celery with its own start command; it shares the application's code and relationships. Every Git branch you activate becomes an environment with copied data. Run a data anonymization command in the deploy hook for non-production environments, guarded by PLATFORM_ENVIRONMENT_TYPE.

branch to running environmentPush branchgit pushBuild imagebuild hookClone datafrom parent envDeploy hookmigrate, anonymizeEnv liveown URLEach branch gets real infrastructure and a copy of the parent’s datadjango-deployment.com
Cloned production data needs anonymizing before reviewers see it.

Explanation

Upsun treats the repository plus its YAML configuration as the full definition of an environment. The build phase produces an immutable image and caches it by content, which is why services are not available there. The deploy phase runs inside the live environment with services attached, before traffic switches. Read-only application files make every container identical and make rollbacks reliable; mounts are the explicit exception for data that must change at runtime.

Edge cases / notes

  • Build-time settings. Any setting that requires a variable present only at runtime breaks collectstatic in the build hook; give it a safe default.
  • Long migrations. The deploy hook blocks the environment until it finishes; keep schema changes small or run large backfills separately.
  • Naming. Documentation and tooling still reference Platform.sh in places; the platform and CLI were rebranded to Upsun.

Next steps

FAQ

Why does migrate fail in the build hook?

Services are not reachable during the build. Run migrations in the deploy hook.

Why do file uploads fail with a read-only filesystem error?

The application directory is read-only at runtime. Write uploads to a declared mount or object storage.

How does Django get database credentials?

From relationship environment variables, either PLATFORM_RELATIONSHIPS or the per-relationship variables your project exposes.

Is Upsun the same as Platform.sh?

Upsun is the rebranded platform; the configuration concepts are the same, with a single combined config file.

Keep going