Перейти к содержимому

Поиск по документации

Поиск по документации Skiffly

Python и Django

Deploy Python apps and Django projects on Skiffly with Railpack — pip, uv, Poetry, PDM and Pipenv, the Python version, gunicorn and `PORT`, Postgres and Redis, migrations and `collectstatic`, static files and common errors.

Railpack's Python provider builds any Python project; Django gets a few extras. Flask and FastAPI are covered on their own page.

What Railpack detects#

Looks atEffect
requirements.txt, pyproject.toml or Pipfile; or one of main.py, app.py, start.py, bot.py, hello.py, server.pythe service is a Python app
requirements.txt → pip · pyproject.toml + poetry.lock → Poetry · + pdm.lock → PDM · + uv.lock → uv · Pipfile → Pipenvhow dependencies are installed
RAILPACK_PYTHON_VERSION, then .python-version / .tool-versions / mise.toml, then runtime.txt, then Pipfilethe Python version (default 3.13)
manage.py + django in the dependenciesDjango: start command python manage.py migrate && gunicorn <project>.wsgi:application
psycopg/psycopg2, mysqlclient, pycairo, pdf2image, pydub in the dependenciesthe matching apt packages (libpq5, default-mysql-client, ffmpeg, …) are installed

Railpack sets PYTHONUNBUFFERED=1 (logs appear immediately), PYTHONDONTWRITEBYTECODE=1 and PIP_DISABLE_PIP_VERSION_CHECK=1. Pin the version with .python-version (3.12) — the default moves with Railpack releases.

Port and server#

Skiffly sets PORT; the process must bind to 0.0.0.0:$PORT. Railpack's Django start command relies on gunicorn reading PORT (gunicorn binds to 0.0.0.0:$PORT when the variable is set). Make it explicit in your own start command anyway:

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

Add gunicorn (or uvicorn + gunicorn with UvicornWorker for ASGI/Channels) to your dependencies; Railpack does not add servers. python manage.py runserver is for development only.

Django start command#

The Django template Skiffly ships uses this start command, which is a good default for most projects:

python manage.py migrate && python manage.py collectstatic --noinput && gunicorn mysite.wsgi --bind 0.0.0.0:$PORT

Set it in Settings → Deploy → Start command (or startCommand / start: in config as code) and replace mysite with your project package. RAILPACK_DJANGO_APP_NAME=mysite.wsgi does the same for Railpack's generated command if you prefer to keep it.

Settings that matter in production:

import os
DEBUG = os.environ.get("DEBUG", "") == "1"
SECRET_KEY = os.environ["SECRET_KEY"]
ALLOWED_HOSTS = [os.environ.get("SKIFFLY_PUBLIC_DOMAIN", "localhost"), "app.example.com"]
CSRF_TRUSTED_ORIGINS = [f"https://{h}" for h in ALLOWED_HOSTS]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
DATABASES = {"default": dj_database_url.config(conn_max_age=60)}   # reads DATABASE_URL

SKIFFLY_PUBLIC_DOMAIN is set automatically once the service has a domain. Generate SECRET_KEY once (skiffly variables set SECRET_KEY=$(openssl rand -hex 32) or generate("hex64") in config as code).

Postgres and Redis#

skiffly deploy --template postgres
skiffly deploy --template redis
skiffly variables set 'DATABASE_URL=${{Postgres.DATABASE_URL}}' 'REDIS_URL=${{Redis.REDIS_URL}}'

DATABASE_URL is postgresql://postgres:…@postgres:5432/app on the private network — dj-database-url or DATABASES["default"] = {"ENGINE": "django.db.backends.postgresql", …} from urllib.parse both work. Install psycopg[binary] (or psycopg2-binary); Railpack adds libpq when it sees the package. Celery and django-redis take REDIS_URL as-is. SQLite works too, but only with a volume mounted where the file lives, and with one replica.

Migrations#

There is no release phase. Either keep python manage.py migrate in the start command (it runs before gunicorn on every deployment; startup gets up to five minutes before the health check gives up), or run it as a one-off from your machine when you want to control the moment:

skiffly ssh -- python manage.py migrate
skiffly ssh -- python manage.py createsuperuser

With several replicas each container runs the start command; Django's migration table makes concurrent migrate calls safe for ordinary migrations, but long data migrations belong in a one-off.

Static and media files#

  • Static: collectstatic in the start command (above) or in the build command, then serve with WhiteNoise (whitenoise.middleware.WhiteNoiseMiddleware, STATIC_ROOT = BASE_DIR / "staticfiles"). Running it at build time (buildCommand: python manage.py collectstatic --noinput) works because every service variable is available to the build; make sure SECRET_KEY is set before the first build.
  • Media / uploads: the container's disk is ephemeral. Mount a volume at MEDIA_ROOT (skiffly volume add --mount-path /app/media), or use S3-compatible storage (django-storages with the minio template or an external bucket).

Workers and scheduled jobs#

Celery or RQ workers are a second service from the same repository with the start command celery -A mysite worker -l info and no domain; Celery beat is a third one, or a service with a cron schedule whose start command is a management command (python manage.py send_digests). Services share variables through ${{shared.NAME}} or references.

Health check#

path("healthz/", lambda r: HttpResponse("ok")) and set the healthcheck path to /healthz/. Because ALLOWED_HOSTS is checked on every request, the probe's Host header (the service's private address) must be accepted: django-healthcheck-style middleware that answers before the host check, or ALLOWED_HOSTS = ["*"] behind Skiffly's proxy, are both common.

Common problems#

SymptomFix
DisallowedHost at /add the domain (and SKIFFLY_PUBLIC_DOMAIN) to ALLOWED_HOSTS
CSRF verification failed. Origin checking failedCSRF_TRUSTED_ORIGINS = ["https://app.example.com"]
FAILED after 5 minutes, gunicorn logs Listening at: http://127.0.0.1:8000bind to 0.0.0.0:$PORT
ModuleNotFoundError: No module named 'mysite'wrong --chdir/package in the start command, or the service's root directory is not the Django project root
psycopg2 build fails (pg_config not found)use psycopg[binary] / psycopg2-binary
collectstatic fails with SECRET_KEY emptyset SECRET_KEY before deploying
Static files 404 in productionDEBUG=False disables Django's static serving: add WhiteNoise or serve from a CDN
Uploads disappear after a deployMEDIA_ROOT is on the ephemeral disk: mount a volume or use object storage

Example#

.skiffly/skiffly.ts
import { defineSkiffly, generate, github, postgres, project, redis, service } from "@skiffly/config";
 
export default defineSkiffly(() => {
  const db = postgres("db");
  const cache = redis("cache");
  const web = service("web", {
    source: github("acme/mysite"),
    start: "python manage.py migrate && python manage.py collectstatic --noinput && gunicorn mysite.wsgi --bind 0.0.0.0:$PORT",
    healthcheck: "/healthz/",
    port: 8000,
    env: { DATABASE_URL: db.env.DATABASE_URL, REDIS_URL: cache.env.REDIS_URL, SECRET_KEY: generate("hex64"), DJANGO_SETTINGS_MODULE: "mysite.settings" },
    domain: true,
  });
  const worker = service("worker", { source: github("acme/mysite"), start: "celery -A mysite worker -l info", env: { DATABASE_URL: db.env.DATABASE_URL, REDIS_URL: cache.env.REDIS_URL, SECRET_KEY: web.env.SECRET_KEY } });
  return project("mysite", { resources: [db, cache, web, worker] });
});

Tutorial: Django + Postgres + Redis. Landing: /deploy/django.