compose.yaml 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. # Local multi-replica validation (task 46).
  2. #
  3. # Three stateless app replicas against ONE Postgres + ONE Redis — the local
  4. # analogue of `deploy/k8s`. Proves shared-state behavior: a token issued by
  5. # one replica is usable/introspectable via another, and migrations apply
  6. # exactly once via the one-shot `migrate` service (never per replica).
  7. #
  8. # docker compose up --build --scale app=3
  9. # docker compose port app 8080 # per-replica host ports (assigned randomly)
  10. #
  11. # Validation sketch (public listener is TLS with a throwaway self-signed
  12. # cert, so use -sk; `<port-N>` from `docker compose port`):
  13. #
  14. # curl -sk https://127.0.0.1:<port-1>/.well-known/oauth-authorization-server
  15. # # complete an authorization-code+PKCE flow against replica 1, then
  16. # # introspect the token against replica 2 — shared Redis makes it work.
  17. #
  18. # Host ports are assigned RANDOMLY per replica (short-syntax `"8080"`), so
  19. # `--scale app=3` never collides. The health listener (8081) is exposed, not
  20. # published — same rule as the cluster Service.
  21. #
  22. # mTLS is OFF here (no `D_OAUTH_TLS__CLIENT_CA_PATH`): the L4-passthrough
  23. # constraint is a cluster concern, documented in docs/DEPLOYMENT.md.
  24. #
  25. # Secrets mirror the cluster's file-mount pattern: the
  26. # one-shot `gensecrets` service materializes DEV-ONLY placeholder values as
  27. # 0400 files owned by 65532 in the `devsecrets` volume, mounted at
  28. # /run/secrets; the app and migrate services read them through
  29. # D_OAUTH_TOKEN_PEPPER_FILE / D_OAUTH_TOTP_ENCRYPTION_KEY_FILE /
  30. # D_OAUTH_PII_ENCRYPTION_KEY_FILE, so no key material enters the process
  31. # environment. Cluster secrets are never baked.
  32. name: d-oauth
  33. services:
  34. db:
  35. image: postgres:18-alpine
  36. environment:
  37. POSTGRES_USER: oauth
  38. POSTGRES_PASSWORD: oauth-dev-only
  39. POSTGRES_DB: oauth
  40. volumes:
  41. # postgres 18 keeps PGDATA under a major-version subdirectory, so the
  42. # volume mounts the parent (a mount on .../data is rejected by the image).
  43. - pgdata:/var/lib/postgresql
  44. healthcheck:
  45. test: ["CMD-SHELL", "pg_isready -U oauth -d oauth"]
  46. interval: 5s
  47. timeout: 3s
  48. retries: 20
  49. redis:
  50. image: redis:8-alpine
  51. # The server asserts this contract at startup: noeviction + AOF.
  52. command: ["redis-server", "--appendonly", "yes", "--maxmemory-policy", "noeviction"]
  53. volumes:
  54. - redisdata:/data
  55. healthcheck:
  56. test: ["CMD", "redis-cli", "ping"]
  57. interval: 5s
  58. timeout: 3s
  59. retries: 20
  60. # One-shot: throwaway self-signed server cert for the local TLS listener.
  61. gencerts:
  62. image: alpine:3.21
  63. volumes:
  64. - certs:/certs
  65. command:
  66. - sh
  67. - -ec
  68. - |
  69. apk add --no-cache openssl >/dev/null
  70. if [ ! -f /certs/tls.crt ]; then
  71. openssl req -x509 -newkey rsa:2048 -nodes -days 2 \
  72. -keyout /certs/tls.key -out /certs/tls.crt \
  73. -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
  74. fi
  75. chown 65532:65532 /certs/tls.crt /certs/tls.key
  76. chmod 600 /certs/tls.key
  77. chmod 644 /certs/tls.crt
  78. # One-shot: DEV-ONLY pepper/TOTP/PII key files, shaped like the cluster's
  79. # tmpfs secret volume (0400, owned by 65532). Values are throwaway
  80. # placeholders; never reuse them outside this local compose file.
  81. gensecrets:
  82. image: alpine:3.21
  83. volumes:
  84. - devsecrets:/run/secrets
  85. command:
  86. - sh
  87. - -ec
  88. - |
  89. if [ ! -f /run/secrets/token-pepper ]; then
  90. printf '%s' 'dev-only-pepper-ROTATE-IN-PROD-000000' > /run/secrets/token-pepper
  91. printf '%s' 'dev-only-totp-key-ROTATE-IN-PROD-000' > /run/secrets/totp-encryption-key
  92. printf '%s' 'dev-only-pii-key-ROTATE-IN-PROD-0000' > /run/secrets/pii-encryption-key
  93. fi
  94. chown 65532:65532 /run/secrets/token-pepper /run/secrets/totp-encryption-key /run/secrets/pii-encryption-key
  95. chmod 400 /run/secrets/token-pepper /run/secrets/totp-encryption-key /run/secrets/pii-encryption-key
  96. # Exactly-once migration (sqlx advisory lock). Replicas never migrate.
  97. migrate:
  98. build:
  99. context: .
  100. dockerfile: Dockerfile
  101. image: d-oauth-server:local
  102. entrypoint: ["/d-oauth-server"]
  103. command: ["migrate"]
  104. depends_on:
  105. gencerts:
  106. condition: service_completed_successfully
  107. gensecrets:
  108. condition: service_completed_successfully
  109. db:
  110. condition: service_healthy
  111. redis:
  112. condition: service_healthy
  113. environment:
  114. D_OAUTH_ISSUER: https://localhost:8080
  115. D_OAUTH_ENVIRONMENT: development
  116. D_OAUTH_BIND_ADDR: 0.0.0.0:8080
  117. D_OAUTH_HEALTH_ADDR: 0.0.0.0:8081
  118. D_OAUTH_TLS__CERT_PATH: /certs/tls.crt
  119. D_OAUTH_TLS__KEY_PATH: /certs/tls.key
  120. D_OAUTH_DATABASE__URL: postgres://oauth:oauth-dev-only@db:5432/oauth?sslmode=disable
  121. D_OAUTH_REDIS__URL: redis://redis:6379
  122. D_OAUTH_REDIS__DURABILITY: waitaof
  123. D_OAUTH_TOKEN_PEPPER_FILE: /run/secrets/token-pepper
  124. D_OAUTH_TOTP_ENCRYPTION_KEY_FILE: /run/secrets/totp-encryption-key
  125. D_OAUTH_PII_ENCRYPTION_KEY_FILE: /run/secrets/pii-encryption-key
  126. volumes:
  127. - certs:/certs:ro
  128. - devsecrets:/run/secrets:ro
  129. app:
  130. build:
  131. context: .
  132. dockerfile: Dockerfile
  133. image: d-oauth-server:local
  134. depends_on:
  135. migrate:
  136. condition: service_completed_successfully
  137. gensecrets:
  138. condition: service_completed_successfully
  139. db:
  140. condition: service_healthy
  141. redis:
  142. condition: service_healthy
  143. environment:
  144. D_OAUTH_ISSUER: https://localhost:8080
  145. D_OAUTH_ENVIRONMENT: development
  146. D_OAUTH_BIND_ADDR: 0.0.0.0:8080
  147. D_OAUTH_HEALTH_ADDR: 0.0.0.0:8081
  148. D_OAUTH_TLS__CERT_PATH: /certs/tls.crt
  149. D_OAUTH_TLS__KEY_PATH: /certs/tls.key
  150. D_OAUTH_DATABASE__URL: postgres://oauth:oauth-dev-only@db:5432/oauth?sslmode=disable
  151. D_OAUTH_REDIS__URL: redis://redis:6379
  152. D_OAUTH_REDIS__DURABILITY: waitaof
  153. D_OAUTH_TOKEN_PEPPER_FILE: /run/secrets/token-pepper
  154. D_OAUTH_TOTP_ENCRYPTION_KEY_FILE: /run/secrets/totp-encryption-key
  155. D_OAUTH_PII_ENCRYPTION_KEY_FILE: /run/secrets/pii-encryption-key
  156. expose:
  157. - 8080
  158. - 8081
  159. # Random host ports per replica: `--scale app=3` never collides.
  160. ports:
  161. - "8080"
  162. - "8081"
  163. read_only: true
  164. # Shell-less self-probe: no shell or curl in the distroless image.
  165. healthcheck:
  166. test: ["CMD", "/d-oauth-server", "healthcheck"]
  167. interval: 5s
  168. timeout: 3s
  169. retries: 20
  170. start_period: 10s
  171. volumes:
  172. - certs:/certs:ro
  173. - devsecrets:/run/secrets:ro
  174. volumes:
  175. pgdata:
  176. redisdata:
  177. certs:
  178. devsecrets: