compose.yaml 6.1 KB

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