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.
- Add
.upsun/config.yamlwith an application (type: python:3.12), a PostgreSQL service, and a route. - Install dependencies and run
collectstaticin the build hook (no database access there). - Run
migratein the deploy hook, which runs with services available. - Read database credentials from the relationship variables or
PLATFORM_RELATIONSHIPS. - 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.
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.
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
collectstaticin 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
- Compare options in Alternatives to Heroku for Hosting Django.
- Make the most of branch environments with Django Preview Environments for Pull Requests.
- Serve assets with Serve Static Files With WhiteNoise.
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.