compose.yaml 4.8 KB

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