Despliegue en producción
Esta guía lleva AI Workforce OS a un servidor Linux propio con el compose de producción del repositorio: Caddy termina TLS y enruta hacia el frontend (Next.js) y el backend (Go), que habla con el runtime de agentes (Python), Postgres y Redis. Cubre la instalación, la operación diaria (actualizar, migrar, respaldar) y el layout reducido de la demo pública.
En esta página
- Topología
- Requisitos
- Instalación paso a paso
- Generar secretos y validar la configuración
- Arranque y comprobación de salud
- Actualizar, migraciones y rollback
- Copias de seguridad y restauración de Postgres
- Checklist de salida a producción
- Publicación de imágenes en CI
- Layout reducido en un servidor compartido
Topología#
docker-compose.prod.yml es independiente: no se combina con el docker-compose.yml de desarrollo. Sirve un solo dominio. Caddy envía /api/* y /ws al backend (puerto interno 8080) y todo lo demás al frontend (puerto interno 3000).
Solo Caddy publica puertos: 80, 443 TCP y 443 UDP. El resto de servicios vive en la red internal, declarada con internal: true y por tanto sin salida a internet. El runtime se une además a la red egress para llamar a las APIs de los proveedores de modelo, y Caddy a la red edge.
El frontend usa rutas relativas (/api/v1) y deriva la URL del WebSocket de window.location (wss:// bajo HTTPS), así que con un solo dominio no hace falta compilar variables NEXT_PUBLIC_*.
| Servicio | Imagen o origen | Redes | Límites (compose) |
|---|---|---|---|
caddy | caddy:2.10.2-alpine | edge, internal | 256 MB, 0,5 CPU |
postgres | postgres:16.6-alpine (volumen pgdata) | internal | 1 GB, 1 CPU |
redis | redis:7.4.2-alpine (AOF, volumen redisdata) | internal | 384 MB, 0,5 CPU |
agent-runtime | ghcr.io/OWNER/aiworkforce-agent-runtime:TAG | internal, egress | 768 MB, 1 CPU |
backend | ghcr.io/OWNER/aiworkforce-backend:TAG | internal | 512 MB, 1 CPU |
frontend | ghcr.io/OWNER/aiworkforce-frontend:TAG | internal | 512 MB, 1 CPU |
Nota
El Caddyfile añade a todas las respuestas Strict-Transport-Security (un año, con subdominios), X-Content-Type-Options: nosniff, X-Frame-Options: DENY y Referrer-Policy: strict-origin-when-cross-origin, y quita la cabecera Server.
Requisitos#
- Un servidor Linux con Docker Engine y el plugin Docker Compose v2, con los puertos 80 y 443 abiertos en el firewall.
- Un dominio con registro DNS
A(yAAAAsi hay IPv6) que apunte al servidor. Let's Encrypt falla si el DNS todavía no resuelve. - Acceso a las imágenes en GHCR. Si los paquetes son privados, ejecuta
docker login ghcr.iocon un token con permisoread:packages. Alternativa: construir las imágenes en el propio servidor con--build. - Una clave de DeepSeek: el compose de producción exige
DEEPSEEK_API_KEY. La de Anthropic es opcional.
Instalación paso a paso#
Los ejemplos usan example.com como dominio y OWNER y TAG como marcadores. Todo el estado vive en volúmenes con nombre, y .env.prod queda fuera de git: el .gitignore del repositorio ya ignora .env.*.
- Clona el repositorio en el servidor y copia la plantilla
.env.prod.examplea.env.prod. - Restringe sus permisos con
chmod 600 .env.prod. - Rellena las variables. El bloque siguiente muestra un
.env.prodmínimo completo.
git clone <repo-url> && cd <repo>
cp .env.prod.example .env.prod
chmod 600 .env.prod
$EDITOR .env.prod
# Dominio y TLS
DOMAIN=example.com
ACME_EMAIL=ops@example.com
ALLOWED_ORIGINS=https://example.com
# Imágenes publicadas por CI
OWNER=acme
TAG=sha-1a2b3c4
# Secretos: un valor distinto de openssl rand -hex 32 para cada uno
POSTGRES_PASSWORD=...
REDIS_PASSWORD=...
JWT_SECRET=...
RUNTIME_TOKEN=...
# Gasto y demo
BUDGET_USD=25
ENABLE_DEMO_RESET=false
# Proveedores de modelo
DEEPSEEK_API_KEY=sk-...
ANTHROPIC_API_KEY=
ALLOWED_PROVIDERS=
Atención
La plantilla trae valores de relleno como CAMBIAR_openssl_rand_hex_32. Si los dejas, cualquiera que los lea en el repositorio puede firmar tokens o entrar en la base de datos. Sustituye cada uno por un secreto aleatorio propio.
Generar secretos y validar la configuración#
Genera un valor distinto para POSTGRES_PASSWORD, REDIS_PASSWORD, JWT_SECRET y RUNTIME_TOKEN. El backend no arranca si JWT_SECRET tiene menos de 32 bytes, y el compose de producción fija AUTH_ENABLED=true. RUNTIME_TOKEN no está en la plantilla, pero conviene añadirlo: el compose lo pasa al backend y al runtime, y sin él las llamadas entre ambos no se autentican.
Las variables obligatorias del compose hacen fallar la validación si faltan: DOMAIN, ACME_EMAIL, POSTGRES_PASSWORD, REDIS_PASSWORD, OWNER, TAG, JWT_SECRET, ALLOWED_ORIGINS y DEEPSEEK_API_KEY.
openssl rand -hex 32 # repite para POSTGRES_PASSWORD, REDIS_PASSWORD, JWT_SECRET y RUNTIME_TOKEN
# Valida antes de arrancar (falla si falta una variable obligatoria)
docker compose -f docker-compose.prod.yml --env-file .env.prod config -q
Consejo
Usa secretos en hexadecimal. DATABASE_URL y REDIS_URL incluyen las contraseñas dentro de la URL, y un carácter especial sin codificar las rompe.
Arranque y comprobación de salud#
Cada servicio define un healthcheck y las dependencias usan condition: service_healthy. Postgres, Redis y el runtime arrancan primero; el backend espera a los tres, el frontend al backend y Caddy a ambos. El backend y el runtime exponen /healthz. Caddy obtiene y renueva el certificado de forma automática.
C="docker compose -f docker-compose.prod.yml --env-file .env.prod"
$C pull # opción A: imágenes publicadas por CI
# $C build # opción B: construir en el servidor
$C up -d
$C ps # todos los servicios deben quedar healthy
curl -fsS https://example.com/ -o /dev/null && echo OK
$C logs -f backend
Actualizar, migraciones y rollback#
Para actualizar, cambia TAG por un sha-<corto> publicado por CI y vuelve a levantar el stack. Haz siempre un pg_dump antes de desplegar una versión que traiga migraciones.
El backend aplica al arrancar las migraciones SQL embebidas que falten, en orden y dentro de una sola transacción, y anota cada una en la tabla schema_migrations. Nunca deshace migraciones por sí solo: el directorio backend/migrations/down/ contiene scripts de reversión que se aplican a mano.
Si la versión nueva falla y el esquema ya cambió, tienes dos caminos: restaurar el dump previo y volver al TAG anterior (pierdes lo escrito después), o desplegar una migración correctiva nueva, que es lo preferible cuando ya hay datos nuevos. Si la migración solo añadió columnas o tablas, el TAG anterior suele seguir funcionando sin tocar la base.
# 1) copia de seguridad (ver la sección siguiente)
# 2) apunta al tag nuevo
sed -i 's/^TAG=.*/TAG=sha-<NEW>/' .env.prod
$C pull
$C up -d
# Rollback sin cambios de esquema: vuelve al TAG anterior
sed -i 's/^TAG=.*/TAG=sha-<PREVIOUS>/' .env.prod
$C pull && $C up -d
# Migraciones aplicadas
$C exec postgres sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "select * from schema_migrations order by applied_at"'
Copias de seguridad y restauración de Postgres#
Postgres es la fuente de verdad. Redis guarda AOF en el volumen redisdata, pero trátalo como caché y cola. Programa el pg_dump con cron, copia los archivos fuera del servidor y prueba una restauración al menos una vez.
mkdir -p backups
$C exec -T postgres sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc' \
> backups/workforce-$(date +%F-%H%M).dump
# Restaurar
$C stop backend
$C exec -T postgres sh -c 'dropdb -U "$POSTGRES_USER" --if-exists "$POSTGRES_DB" && createdb -U "$POSTGRES_USER" "$POSTGRES_DB"'
$C exec -T postgres sh -c 'pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --no-owner' < backups/FILE.dump
$C start backend
Atención
docker compose down -v borra los volúmenes, y con ellos la base de datos y los certificados. Sin -v, down los conserva.
Checklist de salida a producción#
- El DNS apunta al servidor, 80 y 443 están abiertos y
docker compose pssolo muestra puertos publicados encaddy. .env.prodtiene permisos600y está fuera de git.JWT_SECRET,POSTGRES_PASSWORD,REDIS_PASSWORDyRUNTIME_TOKENson únicos y aleatorios (32 bytes en hex o más).ALLOWED_ORIGINSes exactamentehttps://example.comyENABLE_DEMO_RESETesfalse.- La clave de proveedor es válida,
ALLOWED_PROVIDERSrefleja la política de datos de la empresa yBUDGET_USDtiene el tope que quieres. TAGapunta a unsha-<corto>concreto que pasó el CI, no alatest.- Todos los servicios están healthy, el inicio de sesión funciona y el WebSocket conecta en
wss://example.com/ws. - El backup de Postgres está programado y la restauración se probó.
- Los logs rotan (driver
json-file, 10 MB por 5 archivos, ya configurado) y los límites de memoria y CPU encajan con el host.
Nota
El compose pasa al backend un conjunto fijo de variables y al runtime solo DEEPSEEK_API_KEY, ANTHROPIC_API_KEY, MODEL, RUNTIME_TOKEN y ALLOWED_PROVIDERS. Para usar conexiones reales (CONNECTIONS_KEK), TRUSTED_PROXIES, verificación de correo u otros proveedores de modelo, añade esas variables al bloque environment del servicio correspondiente.
Publicación de imágenes en CI#
El workflow .github/workflows/ci.yml compila y prueba backend, runtime y frontend, ejecuta govulncheck y npm audit (sin bloquear), hadolint, un smoke test y los E2E con Playwright. En cada push a main que pasa todo, el job publish sube a GHCR tres imágenes (aiworkforce-backend, aiworkforce-agent-runtime y aiworkforce-frontend) con los tags sha-<corto> y latest, usando el GITHUB_TOKEN con permiso packages: write.
En la configuración del repositorio permite que el GITHUB_TOKEN escriba paquetes y, tras la primera publicación, ajusta la visibilidad de los paquetes. Para un despliegue de un solo dominio, deja sin definir las variables de repositorio NEXT_PUBLIC_API_URL y NEXT_PUBLIC_WS_URL.
Layout reducido en un servidor compartido#
deploy/vps contiene el layout de la demo pública, pensado para un servidor que ya tiene su propio Caddy dueño de los puertos 80 y 443. Ese compose no publica ningún puerto: backend, frontend y la landing estática se unen a la red del Caddy existente (declarada como externa) y el resto queda en una red privada.
Tiene cuentas reales: Postgres, AUTH_ENABLED=true, cuentas de prueba (TRIAL_ENABLED=true), ENABLE_DEMO_RESET=false y TRUSTED_PROXIES apuntando a la red del Caddy. No incluye Redis, así que eventos y límites de tasa viven en memoria del proceso. El runtime corre con SIMULATION=true, sin claves ni costo.
El frontend se compila con NEXT_PUBLIC_API_URL y NEXT_PUBLIC_WS_URL absolutos, y el sitio de la app añade X-Robots-Tag: noindex, nofollow. deploy.sh hace git pull, compila con prioridad baja y ejecuta up -d --wait; backup.sh, pensado para un cron diario, guarda un pg_dump comprimido y borra los de más de 14 días.
example.com -> landing (static)
app.example.com -> /api/* and /ws -> wf-backend:8080 ; rest -> wf-frontend:3000
(the existing Caddy terminates TLS; this project publishes no ports)