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:
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.exampleapps/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):
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
Smoke test:
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:
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.