Saltar al contenido
AI Workforce OS
ES EN
Crear cuenta

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.

6 min de lectura

En esta página
  1. Topología
  2. Requisitos
  3. Instalación paso a paso
  4. Generar secretos y validar la configuración
  5. Arranque y comprobación de salud
  6. Actualizar, migraciones y rollback
  7. Copias de seguridad y restauración de Postgres
  8. Checklist de salida a producción
  9. Publicación de imágenes en CI
  10. Layout reducido en un servidor compartido
Volver arriba

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_*.

ServicioImagen o origenRedesLímites (compose)
caddycaddy:2.10.2-alpineedge, internal256 MB, 0,5 CPU
postgrespostgres:16.6-alpine (volumen pgdata)internal1 GB, 1 CPU
redisredis:7.4.2-alpine (AOF, volumen redisdata)internal384 MB, 0,5 CPU
agent-runtimeghcr.io/OWNER/aiworkforce-agent-runtime:TAGinternal, egress768 MB, 1 CPU
backendghcr.io/OWNER/aiworkforce-backend:TAGinternal512 MB, 1 CPU
frontendghcr.io/OWNER/aiworkforce-frontend:TAGinternal512 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 (y AAAA si 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.io con un token con permiso read: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.*.

  1. Clona el repositorio en el servidor y copia la plantilla .env.prod.example a .env.prod.
  2. Restringe sus permisos con chmod 600 .env.prod.
  3. Rellena las variables. El bloque siguiente muestra un .env.prod mínimo completo.
Bash
git clone <repo-url> && cd <repo>
cp .env.prod.example .env.prod
chmod 600 .env.prod
$EDITOR .env.prod
.env.prodBash
# 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.

Bash
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.

Bash
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.

Bash
# 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.

Bash
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 ps solo muestra puertos publicados en caddy.
  • .env.prod tiene permisos 600 y está fuera de git.
  • JWT_SECRET, POSTGRES_PASSWORD, REDIS_PASSWORD y RUNTIME_TOKEN son únicos y aleatorios (32 bytes en hex o más).
  • ALLOWED_ORIGINS es exactamente https://example.com y ENABLE_DEMO_RESET es false.
  • La clave de proveedor es válida, ALLOWED_PROVIDERS refleja la política de datos de la empresa y BUDGET_USD tiene el tope que quieres.
  • TAG apunta a un sha-<corto> concreto que pasó el CI, no a latest.
  • 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.

Texto
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)