Saltar a contenido

Cómo desplegar Travel en un servidor nuevo

How-to (Diátaxis): pasos concretos para levantar Travel de cero en un servidor que no tiene nada corriendo todavía. No duplica contenido que ya vive en otro lado — linkea a la referencia viva (ENVIRONMENT.md, auto-generado) y a los runbooks puntuales existentes.

0. Prerrequisitos de infra (compartidos con el resto del monorepo)

Travel corre sobre la infra compartida de Timeliber (Postgres, redes Docker, Traefik). Esa parte no es específica de Travel — seguir infra/README.md primero:

./manage.sh bootstrap        # crea las redes externas timeliber-net / proxy-net + copia .env templates
./manage.sh up infra-core    # Postgres + Redis
./manage.sh up infra-traefik # Proxy + TLS — OJO: "up infra" (sin sufijo) NO levanta Traefik, solo infra-core

Al terminar este paso deben existir: la red timeliber-net, la red proxy-net, el contenedor timeliber-postgres corriendo, y Traefik enrutando websecure con Let's Encrypt.

⚠️ El dominio del backend/frontends (TRAVEL_PUBLIC_DOMAIN, default travel.timeliber.com.co) debe tener DNS "A" apuntando directo a la IP del servidor — sin proxy Cloudflare naranja. Con proxy activo, el challenge HTTP de Let's Encrypt le pega a Cloudflare en vez de a este servidor y falla.

1. Variables de entorno

Backend (apps/travel/backend/)

La lista completa — 34 variables, cuáles son requeridas y cuáles secretas — está auto-generada en backend/docs/03_REFERENCE/ENVIRONMENT.md directo desde app/core/config.py. No se duplica acá porque envejecería en la primera migración de config; esa referencia es la fuente de verdad. Copiar el template y rellenar:

cp apps/travel/backend/.env.example apps/travel/backend/.env

Las credenciales reales (Postgres, Wompi, R2, Resend, Sentry) las tiene el fundador/quien administra la infra — no están en el repo por diseño.

Docker Compose (orquestación, no secretos de app)

Cada componente tiene su propio compose.env.example (redes externas, puerto publicado, target de build). Si los defaults del docker-compose.yml alcanzan, no hace falta crear compose.env:

  • apps/travel/backend/compose.env.example
  • apps/travel/frontend/admin-panel/compose.env.example

Frontends (VITE_* / NEXT_PUBLIC_*)

Estos no se auto-generan (no hay tooling de extracción para Vite/Next en el stack hoy — ver documentation_standard.md §5.1, "si no se puede auto-generar, a mano"). Son pocos, están acá:

Componente Variable Uso
frontend/admin-panel VITE_API_URL /api/v1 en prod (ruta relativa tras Traefik)
frontend/admin-panel VITE_PUBLIC_HOTEL_BASE_URL Base para el link "Ver sitio" del sidebar
frontend/admin-panel VITE_WOMPI_PUBLIC_KEY Llave pública Wompi — build-time, sin ella el botón de pago queda deshabilitado
frontend/public-hotel VITE_API_URL Igual que arriba
frontend/marketing-site NEXT_PUBLIC_TRAVEL_API_URL Backend, para el signup
frontend/marketing-site NEXT_PUBLIC_TRAVEL_PUBLIC_URL Link "Ver mi hotel"
frontend/marketing-site NEXT_PUBLIC_ENV development / production

Cada componente tiene su .env.example — copiar y rellenar igual que el backend.

⚠️ VITE_*/NEXT_PUBLIC_* se hornean en build time, vía args: de cada docker-compose.yml (pasan por ${VAR:-default}, que Docker resuelve desde el .env/shell al momento del build). Si cambian después de un deploy, reconstruir la imagen — reiniciar el contenedor no alcanza.

Dominio y redes (compartidas entre backend + 3 frontends)

Todos los docker-compose.yml de Travel leen estas tres del entorno de shell (o de un compose.env si se creó), con el mismo default:

Variable Default Qué controla
TRAVEL_PUBLIC_DOMAIN travel.timeliber.com.co Host que enruta Traefik a cada servicio
PROXY_NETWORK_NAME proxy-net Red externa hacia Traefik
INFRA_NETWORK_NAME timeliber-net Red externa hacia Postgres/Redis

Si el servidor nuevo usa otro dominio, exportarla antes de los docker compose up de abajo: export TRAVEL_PUBLIC_DOMAIN=mi-dominio.com.

2. Base de datos — schema + multitenancy (RLS)

⚠️ Antes de levantar Postgres por primera vez: infra/postgres/init/01_travel.sql + 01b_travel_enum_extensions.sql son un schema crudo desactualizado que compite con Alembic (relation already exists si ambos corren en la misma BD). Sácalos del directorio de init (ej. infra/postgres/init/_disabled/) antes del primer boot — deja 00_travel.sh (solo crea el rol+DB vacía). Alembic es la única fuente de verdad del schema.

2.1 Migraciones

BD nueva (lo normal en un servidor nuevo). No hay Python/alembic en el host (todo corre en contenedores) — usar el entrypoint vacío contra la imagen ya buildeada:

cd apps/travel/backend
docker compose build travel_backend
docker compose run --rm --no-deps --entrypoint "" travel_backend alembic upgrade head

Detalle completo, incluido el caso de una BD existente creada con create_all (no aplica en un servidor nuevo, pero puede pasar si se restaura un dump): ver run-migrations.md.

2.2 Rol de aplicación para RLS (ADR-010)

Obligatorio. Sin esto, el backend se conecta como superuser de Postgres y las políticas RLS se ignoran silenciosamente — el aislamiento multi-tenant queda solo aplicativo (Capa 1), sin la Capa 2 en base de datos.

⚠️ scripts/init-db/01-create-app-role.sh es un hook de docker-entrypoint-initdb.d — solo corre automático en el Postgres local de docker-compose.local.yml. Contra la infra compartida (Postgres ya corriendo de antes), crear el rol con SQL directo:

docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" timeliber-postgres \
  psql -U timeliber_admin -d travel_db -v ON_ERROR_STOP=1 -c "
    CREATE ROLE travel_app LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS PASSWORD 'UNA_PASSWORD_FUERTE_NUEVA';
    GRANT CONNECT ON DATABASE travel_db TO travel_app;
    GRANT USAGE ON SCHEMA public TO travel_app;
    GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO travel_app;
    GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO travel_app;
    ALTER DEFAULT PRIVILEGES FOR ROLE travel_user IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO travel_app;
    ALTER DEFAULT PRIVILEGES FOR ROLE travel_user IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO travel_app;
  "

Verificar (ambas deben decir f — no superuser, no bypass):

SELECT rolname, rolsuper, rolbypassrls FROM pg_roles WHERE rolname = 'travel_app';

Después: POSTGRES_APP_USER=travel_app / POSTGRES_APP_PASSWORD=<la_password_de_arriba> en el .env del backend (config.py prefiere este rol sobre el admin cuando está seteado).

Detalle de por qué esto es necesario (RLS, multitenancy): ADR-010.

3. Backend

cd apps/travel/backend
docker compose build travel_backend
docker compose up -d travel_backend

Smoke test:

curl -f http://localhost:8001/api/v1/health/live

4. Frontends (los 3, mismo patrón build+up)

cd apps/travel/frontend/admin-panel   && docker compose build travel-admin      && docker compose up -d travel-admin
cd apps/travel/frontend/public-hotel  && docker compose build travel-hotel      && docker compose up -d travel-hotel
cd apps/travel/frontend/marketing-site && docker compose build travel-marketing && docker compose up -d travel-marketing

Verificar que los 4 contenedores están healthy:

docker ps --filter "name=timeliber-travel" --format '{{.Names}}\t{{.Status}}'

5. Primer admin + primer hotel (seed)

Un servidor nuevo no tiene ningún usuario ni property todavía. apps/travel/backend/scripts/ no está copiado en la imagen Docker (ni final ni dev — es deliberado, imagen de producción mínima). Los scripts de un solo uso necesitan un bind-mount ad-hoc sobre la imagen ya buildeada:

cd apps/travel/backend

# Usuario admin — lee ADMIN_EMAIL/ADMIN_PASSWORD/ADMIN_NAME del .env,
# NO hay flags --email/--password/--property-id.
docker compose run --rm --no-deps --entrypoint "" \
  -v "$(pwd)/scripts:/app/scripts:ro" \
  travel_backend python scripts/create_admin.py

# Property completa desde un YAML (mismo mecanismo usado para sembrar kasiri-01).
# Flag real es --config (no --file), y requiere el subcomando "run".
docker compose run --rm --no-deps --entrypoint "" \
  -v "$(pwd)/scripts:/app/scripts:ro" \
  travel_backend python scripts/seed_property.py run --config scripts/kasiri_seed.yaml

Si el seed falla con psycopg2.errors.InsufficientPrivilege ... row-level security policy es porque el backend está usando el rol travel_app (RLS activo, correcto para runtime) — para el seed conectar como el admin real (superuser, bypassea RLS): sobreescribir POSTGRES_USER/POSTGRES_PASSWORD en el comando anterior con las credenciales de infra/core/.env (-e POSTGRES_USER=timeliber_admin -e POSTGRES_PASSWORD=...).

Nota — por qué no hay un solo docker-compose.yml para todo

Cada componente (backend + 3 frontends) tiene su propio docker-compose.yml independiente, conectado a las redes externas de infra (proxy-net, timeliber-net) en vez de un compose monolítico. Esto es deliberado (permite rebuildear/reiniciar un componente sin tocar los otros) — no es que falte "unificarlos".

⚠️ Si encontrás docs viejos de deploy que no coinciden con esto

apps/travel/backend/docs/04_EXPLANATION/archive/02_despliegue_produccion_aws.md describe un setup con Nginx como proxy reverso en una instancia AWS — es histórico, ya no aplica. El proxy reverso real hoy es Traefik (labels traefik.http.routers.* en cada docker-compose.yml, ver arriba). Se dejó archivado como referencia histórica, no como guía vigente — esta página (deploy-servidor-nuevo.md) es la actual.