Skip to content

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

ServiceProfileDepends OnDescription
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-proxybackend--Local Flagsmith Edge Proxy (port 8000)
base-nodebackend, frontend--Builds shared base Docker image
pnpm-installbackendbase-nodeRuns pnpm install --frozen-lockfile once
api-gatewaybackendgraphql-router, flagsmith-edge-proxyTraefik reverse proxy, routes traffic
api-gateway-daprbackendapi-gatewayDapr sidecar for api-gateway
apibackendpnpm-install, postgres, redis, flagsmith-edge-proxyLegacy v1 REST/GraphQL API
match-gatewaybackendpnpm-install, apiReal-time match event service
graphql-routerbackendnotifications, identityApollo Router with supergraph composition
notificationsbackendpnpm-install, codegen, flagsmith-edge-proxyNotifications service (push + in-app)
notifications-daprbackendnotificationsDapr sidecar for notifications
identitybackendpnpm-install, codegen, flagsmith-edge-proxyIdentity subgraph service
identity-daprbackendidentityDapr sidecar for identity
loyaltybackendpnpm-install, codegen, mongodb, flagsmith-edge-proxyLoyalty subgraph service
loyalty-daprbackendloyaltyDapr sidecar for loyalty
businessbackendpnpm-install, codegenBusiness service (club operations)
business-daprbackendbusinessDapr sidecar for business
docsfrontendbase-nodeVitePress documentation site
aspire-dashboardbackend, 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

ProfileServices
backendbase-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
frontendbase-node, docs
monitoringaspire-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.

AccessSDK apiUrlUsed by
API gatewayhttp://localhost:8888/v2/flagsWeb/native browser SDKs
Direct (debugging)http://localhost:8000/api/v1Next.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

  1. Copy infra/flagsmith/.env.example to infra/flagsmith/.env.

  2. Set ENVIRONMENT_KEY_PAIRS with the server-side and client-side keys for your Flagsmith environment:

    json
    [{ "server_side_key": "<server-key>", "client_side_key": "<client-key>" }]
  3. 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:

VariableValue
FLAGSMITH_API_URLhttp://localhost:8000/api/v1
FLAGSMITH_CLIENT_ENVIRONMENT_KEYclient-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 at 127.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:

VariableValueDescription
DAPR_HOST127.0.0.1Dapr sidecar (shared namespace)
DAPR_PORT3500Dapr 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 Redis

The 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

VolumePurpose
postgres_dataPersistent PostgreSQL data
mongodb_dataPersistent MongoDB data
pnpm-storepnpm content-addressable store
root-node-modulesRoot node_modules
api-node-modulesapps/api/node_modules
match-gateway-node-modulesapps/match-gateway/node_modules
notifications-node-modulesapps/notifications/node_modules
identity-node-modulesapps/identity/node_modules
loyalty-node-modulesapps/loyalty/node_modules
business-node-modulesapps/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).