compose.yaml 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  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:16
  36. environment:
  37. POSTGRES_USER: oauth
  38. POSTGRES_PASSWORD: oauth-dev-only
  39. POSTGRES_DB: oauth
  40. volumes:
  41. - pgdata:/var/lib/postgresql/data
  42. healthcheck:
  43. test: ["CMD-SHELL", "pg_isready -U oauth -d oauth"]
  44. interval: 5s
  45. timeout: 3s
  46. retries: 20
  47. redis:
  48. image: redis:7
  49. # The server asserts this contract at startup: noeviction + AOF.
  50. command: ["redis-server", "--appendonly", "yes", "--maxmemory-policy", "noeviction"]
  51. volumes:
  52. - redisdata:/data
  53. healthcheck:
  54. test: ["CMD", "redis-cli", "ping"]
  55. interval: 5s
  56. timeout: 3s
  57. retries: 20
  58. # One-shot: throwaway self-signed server cert for the local TLS listener.
  59. gencerts:
  60. image: alpine:3.21
  61. volumes:
  62. - certs:/certs
  63. command:
  64. - sh
  65. - -ec
  66. - |
  67. apk add --no-cache openssl >/dev/null
  68. if [ ! -f /certs/tls.crt ]; then
  69. openssl req -x509 -newkey rsa:2048 -nodes -days 2 \
  70. -keyout /certs/tls.key -out /certs/tls.crt \
  71. -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
  72. fi
  73. chown 65532:65532 /certs/tls.crt /certs/tls.key
  74. chmod 600 /certs/tls.key
  75. chmod 644 /certs/tls.crt
  76. # One-shot: DEV-ONLY pepper/TOTP/PII key files, shaped like the cluster's
  77. # tmpfs secret volume (0400, owned by 65532). Values are throwaway
  78. # placeholders; never reuse them outside this local compose file.
  79. gensecrets:
  80. image: alpine:3.21
  81. volumes:
  82. - devsecrets:/run/secrets
  83. command:
  84. - sh
  85. - -ec
  86. - |
  87. if [ ! -f /run/secrets/token-pepper ]; then
  88. printf '%s' 'dev-only-pepper-ROTATE-IN-PROD-000000' > /run/secrets/token-pepper
  89. printf '%s' 'dev-only-totp-key-ROTATE-IN-PROD-000' > /run/secrets/totp-encryption-key
  90. printf '%s' 'dev-only-pii-key-ROTATE-IN-PROD-0000' > /run/secrets/pii-encryption-key
  91. fi
  92. chown 65532:65532 /run/secrets/token-pepper /run/secrets/totp-encryption-key /run/secrets/pii-encryption-key
  93. chmod 400 /run/secrets/token-pepper /run/secrets/totp-encryption-key /run/secrets/pii-encryption-key
  94. # Exactly-once migration (sqlx advisory lock). Replicas never migrate.
  95. migrate:
  96. build:
  97. context: .
  98. dockerfile: Dockerfile
  99. image: d-oauth-server:local
  100. entrypoint: ["/d-oauth-server"]
  101. command: ["migrate"]
  102. depends_on:
  103. gencerts:
  104. condition: service_completed_successfully
  105. gensecrets:
  106. condition: service_completed_successfully
  107. db:
  108. condition: service_healthy
  109. redis:
  110. condition: service_healthy
  111. environment:
  112. D_OAUTH_ISSUER: https://localhost:8080
  113. D_OAUTH_ENVIRONMENT: development
  114. D_OAUTH_BIND_ADDR: 0.0.0.0:8080
  115. D_OAUTH_HEALTH_ADDR: 0.0.0.0:8081
  116. D_OAUTH_TLS__CERT_PATH: /certs/tls.crt
  117. D_OAUTH_TLS__KEY_PATH: /certs/tls.key
  118. D_OAUTH_DATABASE__URL: postgres://oauth:oauth-dev-only@db:5432/oauth?sslmode=disable
  119. D_OAUTH_REDIS__URL: redis://redis:6379
  120. D_OAUTH_REDIS__DURABILITY: waitaof
  121. D_OAUTH_TOKEN_PEPPER_FILE: /run/secrets/token-pepper
  122. D_OAUTH_TOTP_ENCRYPTION_KEY_FILE: /run/secrets/totp-encryption-key
  123. D_OAUTH_PII_ENCRYPTION_KEY_FILE: /run/secrets/pii-encryption-key
  124. volumes:
  125. - certs:/certs:ro
  126. - devsecrets:/run/secrets:ro
  127. app:
  128. build:
  129. context: .
  130. dockerfile: Dockerfile
  131. image: d-oauth-server:local
  132. depends_on:
  133. migrate:
  134. condition: service_completed_successfully
  135. gensecrets:
  136. condition: service_completed_successfully
  137. db:
  138. condition: service_healthy
  139. redis:
  140. condition: service_healthy
  141. environment:
  142. D_OAUTH_ISSUER: https://localhost:8080
  143. D_OAUTH_ENVIRONMENT: development
  144. D_OAUTH_BIND_ADDR: 0.0.0.0:8080
  145. D_OAUTH_HEALTH_ADDR: 0.0.0.0:8081
  146. D_OAUTH_TLS__CERT_PATH: /certs/tls.crt
  147. D_OAUTH_TLS__KEY_PATH: /certs/tls.key
  148. D_OAUTH_DATABASE__URL: postgres://oauth:oauth-dev-only@db:5432/oauth?sslmode=disable
  149. D_OAUTH_REDIS__URL: redis://redis:6379
  150. D_OAUTH_REDIS__DURABILITY: waitaof
  151. D_OAUTH_TOKEN_PEPPER_FILE: /run/secrets/token-pepper
  152. D_OAUTH_TOTP_ENCRYPTION_KEY_FILE: /run/secrets/totp-encryption-key
  153. D_OAUTH_PII_ENCRYPTION_KEY_FILE: /run/secrets/pii-encryption-key
  154. expose:
  155. - 8080
  156. - 8081
  157. # Random host ports per replica: `--scale app=3` never collides.
  158. ports:
  159. - "8080"
  160. - "8081"
  161. read_only: true
  162. # Shell-less self-probe: no shell or curl in the distroless image.
  163. healthcheck:
  164. test: ["CMD", "/d-oauth-server", "healthcheck"]
  165. interval: 5s
  166. timeout: 3s
  167. retries: 20
  168. start_period: 10s
  169. volumes:
  170. - certs:/certs:ro
  171. - devsecrets:/run/secrets:ro
  172. volumes:
  173. pgdata:
  174. redisdata:
  175. certs:
  176. devsecrets: