fix(docker): reach services by unique aliases, not bare service names

In Dokploy, services with a domain join the shared dokploy-network, where
another project also has an `api`. nginx proxied /api to `api:3000`, so
Docker DNS returned both containers and half of the requests went to the
other app: 404 "Cannot POST /api/auth/token" on sign-in and "sign-in not
configured" when /api/auth/config was answered by it.

Services now get task-stream-* aliases on the project network and nginx and
the API use them (task-stream-api, -postgres, -mongo, -redis). The API's
OPENAPI_FILE is empty in the container; docs 07 explain the aliases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 15:26:47 +02:00
parent 341241a52f
commit 7532938b4e
5 changed files with 38 additions and 6 deletions

View File

@@ -1,4 +1,6 @@
# Same-origin API for the SPA: REST under /api, GraphQL (HTTP + WebSocket) under /graphql. # Same-origin API for the SPA: REST under /api, GraphQL (HTTP + WebSocket) under /graphql.
# The API is reached by its unique alias (task-stream-api), not by the service name `api`:
# on a shared network (Dokploy) another project's `api` would get half of the requests.
map $http_upgrade $connection_upgrade { map $http_upgrade $connection_upgrade {
default upgrade; default upgrade;
'' close; '' close;
@@ -9,13 +11,13 @@ server {
root /usr/share/nginx/html; root /usr/share/nginx/html;
location /api/ { location /api/ {
proxy_pass http://api:3000; proxy_pass http://task-stream-api:3000;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
} }
location /graphql { location /graphql {
proxy_pass http://api:3000; proxy_pass http://task-stream-api:3000;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade; proxy_set_header Connection $connection_upgrade;

View File

@@ -2,6 +2,10 @@
# No ports are published here: services only talk over the compose network, and the web is # No ports are published here: services only talk over the compose network, and the web is
# exposed through the platform's proxy (Dokploy domain → service `web`, port 80). # exposed through the platform's proxy (Dokploy domain → service `web`, port 80).
# #
# Services talk to each other by unique aliases (task-stream-*), never by the bare service
# name: on a shared network (Dokploy's dokploy-network) other projects also have an `api`,
# `postgres` or `redis`, and Docker DNS would hand out theirs too.
#
# Locally, `docker compose` also loads docker-compose.override.yml, which publishes the ports: # Locally, `docker compose` also loads docker-compose.override.yml, which publishes the ports:
# docker compose up -d --build → everything: web on http://localhost:8080 # docker compose up -d --build → everything: web on http://localhost:8080
# docker compose up -d postgres mongo redis → only infrastructure (then `npx nx serve api` / `npx nx serve web`) # docker compose up -d postgres mongo redis → only infrastructure (then `npx nx serve api` / `npx nx serve web`)
@@ -16,6 +20,7 @@ services:
secrets: [nexus_token] secrets: [nexus_token]
image: task-stream-web image: task-stream-web
expose: ['80'] expose: ['80']
networks: [default]
depends_on: depends_on:
api: { condition: service_healthy } api: { condition: service_healthy }
restart: unless-stopped restart: unless-stopped
@@ -27,12 +32,15 @@ services:
secrets: [nexus_token] secrets: [nexus_token]
image: task-stream-api image: task-stream-api
expose: ['3000'] expose: ['3000']
networks:
default:
aliases: [task-stream-api]
environment: environment:
POSTGRES_URL: postgres://taskstream:taskstream@postgres:5432/taskstream POSTGRES_URL: postgres://taskstream:taskstream@task-stream-postgres:5432/taskstream
# No migrations yet: TypeORM creates the event store and lookup tables. # No migrations yet: TypeORM creates the event store and lookup tables.
POSTGRES_SYNCHRONIZE: 'true' POSTGRES_SYNCHRONIZE: 'true'
MONGO_URL: mongodb://mongo:27017/taskstream_read MONGO_URL: mongodb://task-stream-mongo:27017/taskstream_read
REDIS_URL: redis://redis:6379 REDIS_URL: redis://task-stream-redis:6379
EVENT_STREAM_KEY: task-stream:events EVENT_STREAM_KEY: task-stream:events
EVENTS_BRIDGE_ENABLED: 'true' EVENTS_BRIDGE_ENABLED: 'true'
PIREON_BASE_URL: https://oauth.pireon.dev PIREON_BASE_URL: https://oauth.pireon.dev
@@ -40,8 +48,10 @@ services:
PIREON_CLIENT_ID: ${PIREON_CLIENT_ID:-} PIREON_CLIENT_ID: ${PIREON_CLIENT_ID:-}
PIREON_CLIENT_SECRET: ${PIREON_CLIENT_SECRET:-} PIREON_CLIENT_SECRET: ${PIREON_CLIENT_SECRET:-}
GRAPHQL_PLAYGROUND: 'true' GRAPHQL_PLAYGROUND: 'true'
# The SDL file is only useful in development (codegen). # The SDL / OpenAPI files are only useful in development (codegen); the
# document is still served at /api/docs-json.
GRAPHQL_SCHEMA_FILE: '' GRAPHQL_SCHEMA_FILE: ''
OPENAPI_FILE: ''
depends_on: depends_on:
postgres: { condition: service_healthy } postgres: { condition: service_healthy }
mongo: { condition: service_healthy } mongo: { condition: service_healthy }
@@ -50,6 +60,9 @@ services:
postgres: postgres:
image: postgres:17-alpine image: postgres:17-alpine
networks:
default:
aliases: [task-stream-postgres]
environment: environment:
POSTGRES_USER: taskstream POSTGRES_USER: taskstream
POSTGRES_PASSWORD: taskstream POSTGRES_PASSWORD: taskstream
@@ -62,6 +75,9 @@ services:
mongo: mongo:
image: mongo:8 image: mongo:8
networks:
default:
aliases: [task-stream-mongo]
volumes: [mongo-data:/data/db] volumes: [mongo-data:/data/db]
healthcheck: healthcheck:
test: ['CMD', 'mongosh', '--quiet', '--eval', 'db.adminCommand("ping")'] test: ['CMD', 'mongosh', '--quiet', '--eval', 'db.adminCommand("ping")']
@@ -70,6 +86,9 @@ services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
networks:
default:
aliases: [task-stream-redis]
command: ['redis-server', '--appendonly', 'yes'] command: ['redis-server', '--appendonly', 'yes']
volumes: [redis-data:/data] volumes: [redis-data:/data]
healthcheck: healthcheck:

View File

@@ -27,6 +27,15 @@ docker compose down # parar (-v borra los datos)
Para probar en local exactamente lo de producción: `docker compose -f docker-compose.yml up -d --build`. Para probar en local exactamente lo de producción: `docker compose -f docker-compose.yml up -d --build`.
### Nombres únicos en la red
Los servicios se llaman entre sí por **alias únicos** (`task-stream-api`, `task-stream-postgres`, `task-stream-mongo`,
`task-stream-redis`), no por el nombre del servicio. Al dar dominio a un servicio, Dokploy lo conecta a la red
compartida `dokploy-network`, donde otros proyectos también tienen un `api`, un `postgres` o un `redis`: con nombres
genéricos, el DNS de Docker devuelve también los suyos y nginx reparte las peticiones entre ambos (síntoma: la mitad
de las llamadas a `/api/*` responden `404 Cannot GET …` y el login dice «no configurado»). Si añades un servicio,
dale su alias `task-stream-*` y úsalo en las URLs.
Todos tienen healthcheck; `web` espera a que `api` esté sana, y `api` a las tres bases de datos. Todos tienen healthcheck; `web` espera a que `api` esté sana, y `api` a las tres bases de datos.
### Imágenes ### Imágenes
@@ -78,6 +87,8 @@ donde los lee Compose.
| `AUTH_SESSION_TTL_SECONDS` | `60` | Cuánto se confía en un token validado antes de volver a preguntar a Pireon | | `AUTH_SESSION_TTL_SECONDS` | `60` | Cuánto se confía en un token validado antes de volver a preguntar a Pireon |
| `GRAPHQL_PLAYGROUND` | `true` | GraphiQL e introspección | | `GRAPHQL_PLAYGROUND` | `true` | GraphiQL e introspección |
| `GRAPHQL_SCHEMA_FILE` | `apps/api/schema.gql` | Dónde escribir el SDL (vacío = no escribir; así está en Docker) | | `GRAPHQL_SCHEMA_FILE` | `apps/api/schema.gql` | Dónde escribir el SDL (vacío = no escribir; así está en Docker) |
| `SWAGGER_ENABLED` | `true` | API REST documentada: Swagger UI en `/api/docs`, OpenAPI en `/api/docs-json` |
| `OPENAPI_FILE` | `apps/api/openapi.json` | Dónde escribir el documento OpenAPI al arrancar (vacío = no escribir; así en Docker) |
La web no tiene configuración de entorno: llama a la API en su mismo origen (`/api`, `/graphql`). La web no tiene configuración de entorno: llama a la API en su mismo origen (`/api`, `/graphql`).

Binary file not shown.

Binary file not shown.