Python y Django
Despliega apps Python y proyectos Django en Skiffly con Railpack — pip, uv, Poetry, PDM y Pipenv, la versión de Python, gunicorn y `PORT`, Postgres y Redis, migraciones y `collectstatic`, archivos estáticos y errores comunes.
El proveedor de Python de Railpack construye cualquier proyecto Python; Django recibe algunos extras. Flask y FastAPI se cubren en su propia página.
Qué detecta Railpack#
| Revisa | Efecto |
|---|---|
requirements.txt, pyproject.toml o Pipfile; o uno de main.py, app.py, start.py, bot.py, hello.py, server.py | el servicio es una app de Python |
requirements.txt → pip · pyproject.toml + poetry.lock → Poetry · + pdm.lock → PDM · + uv.lock → uv · Pipfile → Pipenv | cómo se instalan las dependencias |
RAILPACK_PYTHON_VERSION, luego .python-version / .tool-versions / mise.toml, luego runtime.txt, luego Pipfile | la versión de Python (por defecto 3.13) |
manage.py + django en las dependencias | Django: comando de inicio python manage.py migrate && gunicorn <project>.wsgi:application |
psycopg/psycopg2, mysqlclient, pycairo, pdf2image, pydub en las dependencias | se instalan los paquetes apt correspondientes (libpq5, default-mysql-client, ffmpeg, …) |
Railpack define PYTHONUNBUFFERED=1 (los logs aparecen de inmediato), PYTHONDONTWRITEBYTECODE=1 y PIP_DISABLE_PIP_VERSION_CHECK=1. Fija la versión con .python-version (3.12); el valor por defecto cambia con las versiones de Railpack.
Puerto y servidor#
Skiffly define PORT; el proceso debe enlazarse a 0.0.0.0:$PORT. El comando de inicio de Django que genera Railpack confía en que gunicorn lea PORT (gunicorn se enlaza a 0.0.0.0:$PORT cuando la variable está definida). De todos modos, hazlo explícito en tu propio comando de inicio:
gunicorn mysite.wsgi --bind 0.0.0.0:$PORT --workers 2 --timeout 60Agrega gunicorn (o uvicorn + gunicorn con UvicornWorker para ASGI/Channels) a tus dependencias; Railpack no agrega servidores. python manage.py runserver es solo para desarrollo.
Comando de inicio de Django#
La plantilla de Django que incluye Skiffly usa este comando de inicio, que es un buen valor por defecto para la mayoría de los proyectos:
python manage.py migrate && python manage.py collectstatic --noinput && gunicorn mysite.wsgi --bind 0.0.0.0:$PORTConfigúralo en Configuración → Despliegue → Comando de inicio (o startCommand / start: en configuración como código) y reemplaza mysite por el paquete de tu proyecto. RAILPACK_DJANGO_APP_NAME=mysite.wsgi hace lo mismo con el comando generado por Railpack si prefieres conservarlo.
Ajustes que importan en producción:
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)} # lee DATABASE_URLSKIFFLY_PUBLIC_DOMAIN se define automáticamente en cuanto el servicio tiene un dominio. Genera SECRET_KEY una sola vez (skiffly variables set SECRET_KEY=$(openssl rand -hex 32) o generate("hex64") en configuración como código).
Postgres y Redis#
skiffly deploy --template postgres
skiffly deploy --template redis
skiffly variables set 'DATABASE_URL=${{Postgres.DATABASE_URL}}' 'REDIS_URL=${{Redis.REDIS_URL}}'DATABASE_URL es postgresql://postgres:…@postgres:5432/app en la red privada; tanto dj-database-url como DATABASES["default"] = {"ENGINE": "django.db.backends.postgresql", …} a partir de urllib.parse funcionan. Instala psycopg[binary] (o psycopg2-binary); Railpack agrega libpq cuando ve el paquete. Celery y django-redis toman REDIS_URL tal cual. SQLite también funciona, pero solo con un volumen montado donde vive el archivo, y con una sola réplica.
Migraciones#
No hay fase de release. Mantén python manage.py migrate en el comando de inicio (se ejecuta antes de gunicorn en cada despliegue; el arranque tiene hasta cinco minutos antes de que el health check se rinda) o ejecútalo como comando puntual desde tu máquina cuando quieras controlar el momento:
skiffly ssh -- python manage.py migrate
skiffly ssh -- python manage.py createsuperuserCon varias réplicas, cada contenedor ejecuta el comando de inicio; la tabla de migraciones de Django hace que las llamadas concurrentes a migrate sean seguras para migraciones normales, pero las migraciones de datos largas van en un comando puntual.
Archivos estáticos y de medios#
- Estáticos:
collectstaticen el comando de inicio (arriba) o en el comando de build, y luego sírvelos con WhiteNoise (whitenoise.middleware.WhiteNoiseMiddleware,STATIC_ROOT = BASE_DIR / "staticfiles"). Ejecutarlo en tiempo de build (buildCommand: python manage.py collectstatic --noinput) funciona porque todas las variables del servicio están disponibles para el build; asegúrate de definirSECRET_KEYantes del primer build. - Medios / subidas: el disco del contenedor es efímero. Monta un volumen en
MEDIA_ROOT(skiffly volume add --mount-path /app/media) o usa almacenamiento compatible con S3 (django-storagescon la plantillaminioo un bucket externo).
Workers y tareas programadas#
Los workers de Celery o RQ son un segundo servicio del mismo repositorio con el comando de inicio celery -A mysite worker -l info y sin dominio; Celery beat es un tercero, o un servicio con programación cron cuyo comando de inicio es un comando de management (python manage.py send_digests). Los servicios comparten variables mediante ${{shared.NAME}} o referencias.
Health check#
path("healthz/", lambda r: HttpResponse("ok")) y configura la ruta del health check en /healthz/. Como ALLOWED_HOSTS se comprueba en cada solicitud, la cabecera Host de la sonda (la dirección privada del servicio) debe aceptarse: un middleware al estilo django-healthcheck que responda antes de la comprobación del host, o ALLOWED_HOSTS = ["*"] detrás del proxy de Skiffly, son ambas opciones comunes.
Problemas comunes#
| Síntoma | Solución |
|---|---|
DisallowedHost at / | agrega el dominio (y SKIFFLY_PUBLIC_DOMAIN) a ALLOWED_HOSTS |
CSRF verification failed. Origin checking failed | CSRF_TRUSTED_ORIGINS = ["https://app.example.com"] |
FAILED después de 5 minutos, gunicorn registra Listening at: http://127.0.0.1:8000 | enlaza a 0.0.0.0:$PORT |
ModuleNotFoundError: No module named 'mysite' | --chdir/paquete incorrecto en el comando de inicio, o el directorio raíz del servicio no es la raíz del proyecto Django |
El build de psycopg2 falla (pg_config not found) | usa psycopg[binary] / psycopg2-binary |
collectstatic falla con SECRET_KEY vacío | define SECRET_KEY antes de desplegar |
| Los archivos estáticos dan 404 en producción | DEBUG=False desactiva el servido de estáticos de Django: agrega WhiteNoise o sírvelos desde un CDN |
| Las subidas desaparecen después de un despliegue | MEDIA_ROOT está en el disco efímero: monta un volumen o usa almacenamiento de objetos |
Ejemplo#
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.