# Deployment Architecture ## Single published port Production Docker Compose (`compose.yml`) publishes **exactly one** host port: `edge` (Nginx), mapped via `APP_HTTPS_PORT` (default `443`). No other service (`api`, `worker`, `postgres`, `redis`) defines a Compose `ports:` mapping — they are reachable only over the internal `travel` bridge network via their service DNS names. `scripts/verify-compose-invariants.mjs` (wired into `pnpm test:compose`) fails the build if this invariant regresses. ## TLS The edge container listens on container port 443 only and requires a certificate and private key mounted at the paths configured by `TLS_CERT_FILE`/`TLS_KEY_FILE` (bind-mounted read-only to `/run/tls/tls.crt` and `/run/tls/tls.key`). There is no port-80 fallback in this phase. ## Internal reachability - PostgreSQL and Redis are not reachable from the host through Compose-published ports; only containers on the `travel` network can reach them. - The API is not reachable from the host directly; all external traffic reaches it through `edge`'s `/api/` and `/health/` proxy locations. - The worker process has neither `ports` nor `expose` — it accepts no inbound traffic at all. - Outbound egress from `api` and `worker` (e.g. to Mistral, web research, SMTP providers in later phases) remains allowed. ## Image tags TeamCity supplies immutable `IMAGE_TAG` values (see `scripts/teamcity/build-images.sh`); `latest`/floating tags are refused. `REGISTRY` and `IMAGE_TAG` together select the exact image digest-equivalent tag deployed to a host. ## TeamCity wiring | TeamCity stage | Repository entry point | |------------------------|----------------------------------| | Validate | `scripts/teamcity/validate.sh` | | Build + Push | `scripts/teamcity/build-images.sh` | | Deploy over SSH | `scripts/teamcity/deploy.sh` | | Post-deploy smoke | `scripts/teamcity/smoke.sh` | | Rollback | `scripts/teamcity/rollback.sh` | The existing TeamCity project configures these as command-line/SSH build steps; all deployment logic stays in version control, not in TeamCity step configuration. ## OIDC configuration (Phase 02+) `api` and `worker` both require `OIDC_ISSUER` and `OIDC_AUDIENCE` at startup (validated fail-fast by `loadEnvironment`, same as `DATABASE_URL`/`REDIS_URL`). Neither is a secret — this is a public PKCE client with no client secret. The Angular production bundle bakes `OIDC_ISSUER`/`OIDC_CLIENT_ID` in at **image build time** via `docker/edge.Dockerfile` build args (sourced from the `OIDC_ISSUER`/`OIDC_CLIENT_ID` environment variables passed to `docker compose build`), not at container runtime, since static frontend assets cannot read server-side environment variables after the fact. ## Migrations (Phase 02+) `backend/apps/api/src/migration.ts` now runs real, versioned `node-pg-migrate` migrations from `backend/migrations/`; the Phase 01 no-op body has been replaced. The container command contract (`node backend/dist/apps/api/src/migration.js`) is unchanged, so `scripts/teamcity/deploy.sh` required no changes. Migrations are copied into the `travel-api` image so `docker compose run --rm --no-deps api node backend/dist/apps/api/src/migration.js` has everything it needs.