Docker Compose
The docker-compose.yml at the repository root defines all services needed to run the platform locally. Services are organized into profiles so developers can start only the parts they need.
Service Architecture
| Service | Profile | Depends On | Description |
|---|---|---|---|
| postgres | (always) | -- | PostgreSQL 16 (main database) |
| redis | (always) | -- | Redis 7 Alpine — cache and pub/sub broker |
| mongodb | (always) | -- | MongoDB 7.0 document database |
| flagsmith-edge-proxy | backend | -- | Local Flagsmith Edge Proxy (port 8000) |
| base-node | backend, frontend | -- | Builds shared base Docker image |
| pnpm-install | backend | base-node | Runs pnpm install --frozen-lockfile once |
| api-gateway | backend | graphql-router, flagsmith-edge-proxy | Traefik reverse proxy, routes traffic |
| api-gateway-dapr | backend | api-gateway | Dapr sidecar for api-gateway |
| api | backend | pnpm-install, postgres, redis, flagsmith-edge-proxy | Legacy v1 REST/GraphQL API |
| match-gateway | backend | pnpm-install, api | Real-time match event service |
| graphql-router | backend | notifications, identity | Apollo Router with supergraph composition |
| notifications | backend | pnpm-install, codegen, flagsmith-edge-proxy | Notifications service (push + in-app) |
| notifications-dapr | backend | notifications | Dapr sidecar for notifications |
| identity | backend | pnpm-install, codegen, flagsmith-edge-proxy | Identity subgraph service |
| identity-dapr | backend | identity | Dapr sidecar for identity |
| loyalty | backend | pnpm-install, codegen, mongodb, flagsmith-edge-proxy | Loyalty subgraph service |
| loyalty-dapr | backend | loyalty | Dapr sidecar for loyalty |
| business | backend | pnpm-install, codegen | Business service (club operations) |
| business-dapr | backend | business | Dapr sidecar for business |
| docs | frontend | base-node | VitePress documentation site |
| aspire-dashboard | backend, monitoring | -- | OpenTelemetry traces and logs UI |
The startup order is enforced via depends_on with conditions. Infrastructure services (Postgres, Redis) start first, then base-node and pnpm-install, and finally the application services.
Profiles
| Profile | Services |
|---|---|
backend | base-node, pnpm-install, flagsmith-edge-proxy, api-gateway, api-gateway-dapr, api, match-gateway, graphql-router, notifications, notifications-dapr, identity, identity-dapr, loyalty, loyalty-dapr, business, business-dapr, aspire-dashboard |
frontend | base-node, docs |
monitoring | aspire-dashboard |
| (none) | postgres, redis, mongodb (always start) |
Infrastructure Services
PostgreSQL (postgres) — Postgres 16 with the main database (default). Additional databases such as feyenoord_auth are created on first volume init via docker/postgres/init. Data is persisted in the postgres_data volume. Includes a healthcheck that backend services wait on.
Connection string from other containers:
- Main:
postgresql://postgres:postgres@postgres:5432/main
If you still have an old postgres_main_data volume from before the service was renamed, either recreate Postgres (docker compose down -v then up) or migrate data manually.
Redis (redis) — Redis 7 Alpine. Used for caching across backend services and as the Dapr pub/sub broker via Redis Streams. Services communicate asynchronously through Dapr sidecars backed by Redis Streams (see ADR-005: Message Queue).
MongoDB (mongodb) — MongoDB 7.0. Used by the notifications service for in-app notification storage and user notification preferences. Data is persisted in the mongodb_data volume. Includes a healthcheck via mongosh.
Flagsmith Edge Proxy (flagsmith-edge-proxy) — Flagsmith Edge Proxy on the backend profile. Polls the shared Flagsmith instance at https://flagsmith.feyenoord.com/api/v1 and serves flag evaluation locally on port 8000. Browser clients reach it via the API gateway at /v2/flags; Traefik rewrites /v2/flags/* to /api/v1/* on the Flagsmith Edge Proxy upstream. See Feature Flags architecture for the full system diagram and Azure deployment.
| Access | SDK apiUrl | Used by |
|---|---|---|
| API gateway | http://localhost:8888/v2/flags | Web/native browser SDKs |
| Direct (debugging) | http://localhost:8000/api/v1 | Next.js SSR, flagsmith-nodejs backends |
Example: the browser SDK requests GET /v2/flags/flags/ via the gateway; Traefik forwards GET /api/v1/flags/ to the Flagsmith Edge Proxy.
Setup
Copy
infra/flagsmith/.env.exampletoinfra/flagsmith/.env.Set
ENVIRONMENT_KEY_PAIRSwith the server-side and client-side keys for your Flagsmith environment:json[{ "server_side_key": "<server-key>", "client_side_key": "<client-key>" }]Start the backend profile:
docker compose --profile backend up.
Backend services inside Compose use FLAGSMITH_API_URL=http://flagsmith-edge-proxy:8000/api/v1. When running the web app on the host, use the values from apps/web/.env.development:
| Variable | Value |
|---|---|
FLAGSMITH_API_URL | http://localhost:8000/api/v1 |
FLAGSMITH_CLIENT_ENVIRONMENT_KEY | client-side environment key |
Set FLAGSMITH_ENVIRONMENT_KEY in each backend app's .env (see .env.example files) to the server-side SDK key.
Application Services
API Gateway
Traefik v3 reverse proxy. Routes traffic based on path rules defined in apps/api-gateway/traefik/providers.yaml.tmpl:
/api/v1,/api/graphql— legacy API/v2/flags— Flagsmith Edge Proxy (rewrites to/api/v1/*on the upstream)/v2/graphql(and other router paths) — Apollo Router/v2/notifications,/v2/identity,/v2/loyalty,/v2/business— Dapr service invocation via the gateway's own sidecar at127.0.0.1:3500
Exposes the Traefik dashboard on port 8880.
API
Legacy v1 REST/GraphQL API. Runs in dev mode with hot reload via volume mounts. Depends on PostgreSQL (healthy) and Redis (started). Environment variables are loaded from apps/api/.env.
Match Gateway
Real-time match event service. Publishes domain events (goal scored, lineup announced, etc.) to Dapr pub/sub topics. Uses match-gateway-dapr as its sidecar for all pub/sub communication.
GraphQL Router
Apollo Router with Rover for supergraph composition. Runs with --hot-reload and composes the supergraph from local subgraph schemas on startup. Exposes the health check on port 8088.
Notifications
Notifications service handling push (OneSignal) notifications.
The notifications-dapr sidecar shares the notifications container's network namespace. Dapr calls the app's subscription handlers (registered via GET /dapr/subscribe) when events arrive on subscribed topics.
Key environment variables set by Docker Compose:
| Variable | Value | Description |
|---|---|---|
DAPR_HOST | 127.0.0.1 | Dapr sidecar (shared namespace) |
DAPR_PORT | 3500 | Dapr HTTP port |
Identity
Identity subgraph service. Uses identity-dapr as its sidecar for Dapr service invocation and pub/sub.
Loyalty
Loyalty subgraph service. Uses loyalty-dapr as its sidecar for Dapr service invocation.
Business
Business service (club operations). Uses business-dapr as its sidecar for Dapr service invocation.
Docs
VitePress documentation site. Uses the shared base image and runs pnpm dev with hot reload.
Dapr Sidecars
Each application service that uses Dapr has a corresponding sidecar container (e.g. notifications-dapr, identity-dapr). The api-gateway also has api-gateway-dapr, which Traefik uses for outbound service invocation. Sidecars use network_mode: "service:<app>" to share the app container's network namespace, so the app reaches the sidecar at localhost:3500.
Dapr configuration and component definitions are mounted from infra/dapr/:
infra/dapr/
├── config.yaml ← tracing, metrics, resiliency settings
└── resources/
├── redis-pubsub.yaml ← pubsub component backed by Redis Streams
└── redis-state.yaml ← state store component backed by RedisThe pub/sub component is named pubsub and uses Redis Streams. Swapping to a different broker in production means replacing redis-pubsub.yaml with the appropriate Dapr component — no application code changes required.
Aspire Dashboard
.NET Aspire Dashboard running unsecured on the local Docker network. All backend services export OTLP/gRPC to aspire-dashboard:18889; the UI is at http://localhost:18888. See Observability — Local Development.
Init Services
base-node
Builds Dockerfile.base and tags it as feye-base-node:local. This image is used as the BASE_IMAGE build argument for all Node.js app Dockerfiles, so local development does not require ACR access.
The service runs echo "base image built" and exits immediately — its only purpose is to produce the local image.
pnpm-install
Runs pnpm install --frozen-lockfile in the monorepo root. Backend services declare a dependency on this service with condition: service_completed_successfully, ensuring dependencies are installed before any app starts.
Installed packages are cached in named volumes to avoid reinstalling on every restart.
Volumes
| Volume | Purpose |
|---|---|
postgres_data | Persistent PostgreSQL data |
mongodb_data | Persistent MongoDB data |
pnpm-store | pnpm content-addressable store |
root-node-modules | Root node_modules |
api-node-modules | apps/api/node_modules |
match-gateway-node-modules | apps/match-gateway/node_modules |
notifications-node-modules | apps/notifications/node_modules |
identity-node-modules | apps/identity/node_modules |
loyalty-node-modules | apps/loyalty/node_modules |
business-node-modules | apps/business/node_modules |
Source code is mounted as ./:/workspace:delegated on all backend services for hot reload. The :delegated flag improves filesystem performance on macOS.
Networking
All services join the feyenoord bridge network. Services reference each other by container name (e.g., postgres, redis, api-gateway). Dapr sidecars are an exception — they use network_mode: "service:<app>" and are reachable at localhost:3500 from within the app container.
Dev Targets
Backend app Dockerfiles define a dev target that is lighter than the production runner stage. Docker Compose uses target: dev for api, match-gateway, notifications, and identity. The graphql-router uses target: base-tools which includes Rover and the Apollo Router binary.
The dev target inherits from the shared base image and sets up the working directory. The actual application runs via the command override in docker-compose.yml (e.g., pnpm --filter @repo/notifications dev).