Desplegar Django con Postgres y Redis
Paso a paso: un proyecto Django con gunicorn, una base de datos Postgres, Redis para caché y Celery, un servicio worker y una tarea cron en Skiffly — CLI, panel o MCP, con el resultado esperado de cada paso.
Qué vas a construir: un proyecto mysite con cuatro servicios — web (Django + gunicorn), worker (Celery), Postgres y Redis — más un comando de administración nocturno con una programación cron. Las migraciones y collectstatic se ejecutan cuando arranca el contenedor web. Unos 20 minutos.
Necesitas: un proyecto Django en GitHub con requirements.txt (o pyproject.toml) que incluya gunicorn, psycopg[binary], dj-database-url, whitenoise, redis y celery; la CLI skiffly (npm i -g skiffly && skiffly login).
1. Configuración de producción#
import os, dj_database_url
SECRET_KEY = os.environ["SECRET_KEY"]
DEBUG = os.environ.get("DEBUG") == "1"
# Solo tus dominios llegan al contenedor (el edge enruta por host) y la sonda de salud usa la dirección
# del contenedor como Host, así que "*" es seguro aquí; mantén los orígenes CSRF explícitos.
ALLOWED_HOSTS = ["*"]
CSRF_TRUSTED_ORIGINS = [f"https://{h}" for h in [os.environ.get("SKIFFLY_PUBLIC_DOMAIN"), os.environ.get("APP_HOST")] if h]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
DATABASES = {"default": dj_database_url.config(conn_max_age=60)} # DATABASE_URL
CACHES = {"default": {"BACKEND": "django.core.cache.backends.redis.RedisCache", "LOCATION": os.environ.get("REDIS_URL", "redis://localhost:6379")}}
CELERY_BROKER_URL = os.environ.get("REDIS_URL")
MIDDLEWARE.insert(1, "whitenoise.middleware.WhiteNoiseMiddleware")
STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES = {"staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"}, "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"}}Un endpoint de salud:
from django.http import HttpResponse
urlpatterns = [path("healthz/", lambda r: HttpResponse("ok")), ...]Fija la versión de Python con un archivo .python-version (3.12). Haz commit y push.
2. Proyecto, base de datos, caché#
cd mysite
skiffly init --name mysite
skiffly deploy --template postgres # servicio "Postgres": postgres:17, volumen de 10 GB, DATABASE_URL
skiffly deploy --template redis # servicio "Redis": redis:7, volumen de 2 GB, REDIS_URL
skiffly statusResultado esperado: ambos servicios en SUCCESS. (Panel: Nuevo servicio → Plantilla dos veces. MCP: pídele al agente que cree los servicios Postgres y Redis; usa create-service + create-volume + set-variables.)
3. Variables para el servicio web#
Configúralas antes del primer build para que collectstatic y Django puedan arrancar:
skiffly up -y -d # crea el servicio "mysite" (vacío hasta que termine el primer build); puedes cortar con Ctrl-C
skiffly variables set -s mysite --skip-deploys \
SECRET_KEY="$(openssl rand -hex 32)" \
DJANGO_SETTINGS_MODULE=mysite.settings \
'DATABASE_URL=${{Postgres.DATABASE_URL}}' \
'REDIS_URL=${{Redis.REDIS_URL}}'--skip-deploys guarda los valores sin volver a desplegar en cada llamada; el siguiente skiffly up los toma todos.
4. Comando de inicio y health check#
Railpack adivinaría python manage.py migrate && gunicorn mysite.wsgi:application. Hazlo explícito con collectstatic y la dirección de bind:
mysite → Configuración → Despliegue → comando de inicio:
python manage.py migrate && python manage.py collectstatic --noinput && gunicorn mysite.wsgi --bind 0.0.0.0:$PORT --workers 2Ruta del health check: /healthz/.
5. Desplegar#
skiffly up -s mysite
skiffly logs -s mysite -fResultado esperado en el log del build: [railpack] detected python, pip install -r requirements.txt. En el log de ejecución: Applying … OK, N static files copied, Listening at: http://0.0.0.0:8080. Estado SUCCESS cuando /healthz/ responde.
skiffly domain -s mysite # https://mysite-x1y2.skiffly.cloud
skiffly ssh -s mysite -- python manage.py createsuperuserAbre /admin/ en el dominio e inicia sesión.
6. El worker de Celery#
Un segundo servicio desde el mismo repositorio, mismas variables, sin dominio. El camino más rápido es la configuración como código, que además captura todo lo anterior:
skiffly config init # .skiffly/skiffly.ts a partir del entorno actualAgrega el worker y la tarea cron al archivo generado:
import { defineSkiffly, github, postgres, preserve, project, redis, service } from "@skiffly/config";
export default defineSkiffly(() => {
const db = postgres("Postgres");
const cache = redis("Redis");
const env = {
SECRET_KEY: process.env.SECRET_KEY ?? preserve(),
DJANGO_SETTINGS_MODULE: "mysite.settings",
DATABASE_URL: db.env.DATABASE_URL,
REDIS_URL: cache.env.REDIS_URL,
};
const web = service("mysite", {
source: github("acme/mysite"),
start: "python manage.py migrate && python manage.py collectstatic --noinput && gunicorn mysite.wsgi --bind 0.0.0.0:$PORT --workers 2",
healthcheck: "/healthz/",
env,
domain: true,
});
const worker = service("worker", {
source: github("acme/mysite"),
// todo servicio de larga duración se sondea en su puerto: el http.server mantiene el rollout saludable
start: "sh -c 'python -m http.server $PORT --bind 0.0.0.0 -d /tmp & exec celery -A mysite worker -l info --concurrency 2'",
env: { ...env, SECRET_KEY: web.env.SECRET_KEY },
});
const nightly = service("nightly", {
source: github("acme/mysite"),
start: "python manage.py send_digests",
cron: "0 3 * * *",
env: { ...env, SECRET_KEY: web.env.SECRET_KEY },
});
return project("mysite", { resources: [db, cache, web, worker, nightly] });
});skiffly config plan # + service worker, + service nightly
skiffly config applyResultado esperado: worker muestra SUCCESS con celery@… ready. en su log (el python -m http.server delante de Celery existe solo para que la sonda del rollout en el puerto del servicio pase; un servicio que no escucha en nada se marca como FAILED a los cinco minutos); nightly muestra la hora de la próxima ejecución y ningún contenedor corriendo entre ticks. (Sin configuración como código: Nuevo servicio → Repositorio de GitHub dos veces en el panel, y luego configura el comando de inicio, las variables y la programación cron en la configuración de cada servicio.)
7. Comprobar la cola#
skiffly ssh -s mysite -- python manage.py shell -c "from mysite.tasks import ping; print(ping.delay().get(timeout=10))"
skiffly logs -s worker -n 208. Archivos multimedia#
Las subidas escritas en MEDIA_ROOT desaparecen en el siguiente despliegue. Monta un volumen — skiffly volume add -s mysite --mount-path /app/media --size 5 y MEDIA_ROOT = "/app/media" (una réplica) — o usa django-storages con un bucket compatible con S3 (skiffly deploy --template minio).
Solución de problemas#
| Síntoma | Solución |
|---|---|
DisallowedHost | ALLOWED_HOSTS es una lista fija sin el dominio; SKIFFLY_PUBLIC_DOMAIN solo se define después de skiffly domain |
CSRF verification failed al iniciar sesión en el admin | a CSRF_TRUSTED_ORIGINS le falta el dominio: define APP_HOST o vuelve a desplegar después de skiffly domain |
KeyError: 'SECRET_KEY' durante collectstatic | las variables se configuraron después de que empezara el build: ejecuta skiffly up de nuevo |
django.db.utils.OperationalError: could not translate host name "postgres" | el servicio está en otro entorno, o la referencia se configuró en el servicio equivocado |
El worker registra Connection refused hacia Redis | REDIS_URL no está definida en el worker; las referencias son por servicio |
El servicio cron muestra FAILED | el comando terminó con código distinto de cero: skiffly logs -s nightly |
Siguiente: Guía de Python y Django · Servicios: tareas cron · Configuración como código