Ir al contenido

Buscar en la documentación

Busca en la documentación de Skiffly

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#

mysite/settings.py
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:

mysite/urls.py
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 status

Resultado 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:

mysiteConfiguración → Despliegue → comando de inicio:

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

Ruta del health check: /healthz/.

5. Desplegar#

skiffly up -s mysite
skiffly logs -s mysite -f

Resultado 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 createsuperuser

Abre /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 actual

Agrega el worker y la tarea cron al archivo generado:

.skiffly/skiffly.ts
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 apply

Resultado 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 20

8. 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íntomaSolución
DisallowedHostALLOWED_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 admina CSRF_TRUSTED_ORIGINS le falta el dominio: define APP_HOST o vuelve a desplegar después de skiffly domain
KeyError: 'SECRET_KEY' durante collectstaticlas 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 RedisREDIS_URL no está definida en el worker; las referencias son por servicio
El servicio cron muestra FAILEDel 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