forked from gamesgamesgamesgamesgames/happyview
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker-compose.prod.postgres.yml
More file actions
299 lines (276 loc) · 13.7 KB
/
Copy pathdocker-compose.prod.postgres.yml
File metadata and controls
299 lines (276 loc) · 13.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# Production stack — Postgres.
#
# The sibling of `docker-compose.prod.sqlite.yml`. Pick one: SQLite for small
# to medium instances, Postgres when you need multiple HappyView replicas
# sharing a database, a larger-than-memory working set, or external tools with
# direct read access to the records table.
#
# Its own project name, so `down` here never tears down the SQLite prod stack,
# the dev stack (`happyview`), or the test stacks (`happyview-test`,
# `happyview-e2e`). Every compose file in this directory would otherwise
# default to the same project name and share containers, networks and volumes.
name: happyview-prod-postgres
# Required variables (keep them in their own file — see the warning below):
#
# PUBLIC_URL https://happyview.example.com — must match exactly the
# URL users hit, scheme included, or OAuth login breaks.
# Do NOT include BASE_PATH here.
# SESSION_SECRET openssl rand -base64 48 (>= 64 chars recommended)
# TOKEN_ENCRYPTION_KEY openssl rand -base64 32 (exactly 32 bytes, base64)
# POSTGRES_PASSWORD openssl rand -hex 32
#
# Compose fails fast with a message naming the generator command if any is
# missing.
#
# Compose auto-loads a `.env` sitting beside this file, so a development `.env`
# left in a deployment clone will quietly supply these — including the
# `POSTGRES_USER`/`POSTGRES_PASSWORD`/`POSTGRES_DB` triplet that `.env.example`
# ships commented out. Keep production values in their own file and pass it
# explicitly:
#
# docker compose --env-file .env.prod -f docker-compose.prod.postgres.yml up -d
#
# HappyView does not terminate TLS. Put a reverse proxy (Caddy, nginx,
# Cloudflare Tunnel, a platform load balancer) in front of it and point
# PUBLIC_URL at the public HTTPS URL. If you don't already have one, a
# commented-out Caddy service at the bottom of this file will do it, with
# certificates obtained and renewed automatically.
services:
postgres:
image: postgres:${POSTGRES_VERSION:-17}
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-happyview}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?generate one with `openssl rand -hex 32`}
POSTGRES_DB: ${POSTGRES_DB:-happyview}
# Without this, the image trusts any connection from inside the compose
# network regardless of password.
POSTGRES_HOST_AUTH_METHOD: scram-sha-256
POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256
# Raised from the stock 100. HappyView opens two pools: the main one
# (DATABASE_MAX_CONNECTIONS, default 32 on Postgres) and a separate backfill
# pool sized `(BACKFILL_CONCURRENT_PDS * BACKFILL_CONCURRENT_DIDS_PER_PDS)
# + BACKFILL_CONCURRENT_RESOLUTION + 4` — 134 on the defaults, capped at
# 256. Both are lazy, so an idle instance holds almost nothing, but a
# running backfill peaks near 166 and the stock limit would fail it with
# "sorry, too many clients already". Raise this further if you raise the
# backfill concurrency, add replicas, or lower DATABASE_MAX_CONNECTIONS.
command:
- -c
- max_connections=${POSTGRES_MAX_CONNECTIONS:-200}
volumes:
- pgdata:/var/lib/postgresql/data
# Not published. The only client is HappyView, over the compose network.
# Uncomment to reach it with psql from the host — bound to loopback, since
# `ports` bypasses the host firewall on most Docker installs.
# ports:
# - "127.0.0.1:5432:5432"
healthcheck:
# `$$` escapes compose interpolation, so the container's own environment
# supplies these rather than the host's.
test: ["CMD-SHELL", 'pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"']
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# Docker's default SIGTERM is a Postgres *smart* shutdown: it waits for
# every client to disconnect, so it hangs for the full grace period and
# then takes a SIGKILL, leaving the next boot to run crash recovery. SIGINT
# is the fast shutdown — roll back open transactions, checkpoint, exit.
stop_signal: SIGINT
stop_grace_period: 60s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
happyview:
image: ghcr.io/gamesgamesgamesgamesgames/happyview:${HAPPYVIEW_VERSION:-latest}
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
# Published on loopback by default — the reverse proxy in front is what
# should be exposed. Set HTTP_BIND=0.0.0.0:3000 to publish on all
# interfaces, or drop `ports` entirely and attach the proxy to this
# compose network.
ports:
- "${HTTP_BIND:-127.0.0.1:3000}:3000"
environment:
# --- Database -----------------------------------------------------
# Assembled from the same values the postgres service uses, so the two
# cannot drift. The backend is auto-detected from the URL scheme.
#
# A password containing `@`, `/`, `:` or `#` must be percent-encoded here
# — those are URL delimiters and would truncate the connection string.
# `openssl rand -hex 32` avoids the problem entirely.
DATABASE_URL: postgres://${POSTGRES_USER:-happyview}:${POSTGRES_PASSWORD:?generate one with `openssl rand -hex 32`}@postgres:5432/${POSTGRES_DB:-happyview}
# Main pool ceiling. Must be counted against POSTGRES_MAX_CONNECTIONS
# above, together with the backfill pool and any replicas.
DATABASE_MAX_CONNECTIONS: ${DATABASE_MAX_CONNECTIONS:-32}
# --- Identity / networking ----------------------------------------
PUBLIC_URL: ${PUBLIC_URL:?set PUBLIC_URL to the public HTTPS URL, e.g. https://happyview.example.com}
HOST: 0.0.0.0
PORT: "3000"
# Subpath prefix when sharing a domain with another service, e.g. /hv.
# PUBLIC_URL must NOT include it. Applied at container start, so no
# rebuild is needed. /health stays at the domain root regardless.
BASE_PATH: ${BASE_PATH:-}
# --- Secrets ------------------------------------------------------
# Signs the dashboard session cookie. An unset or too-short value does
# not stop boot — it silently disables cookie login — so it is required
# here instead.
SESSION_SECRET: ${SESSION_SECRET:?generate one with `openssl rand -base64 48`}
# AES-256-GCM key for OAuth tokens, DPoP private keys and plugin secrets
# at rest. Without it, DPoP sessions, spaces and service identity are
# disabled. Rotating it makes everything already encrypted unreadable.
TOKEN_ENCRYPTION_KEY: ${TOKEN_ENCRYPTION_KEY:?generate one with `openssl rand -base64 32` (must decode to exactly 32 bytes)}
# Attestation signing uses a key generated and persisted to the database
# on first run. To supply your own, uncomment this — but note it must be
# a valid hex-encoded 32-byte secp256k1 key. An empty value is treated as
# "set", and fails signer construction instead of falling back to the
# generated key, which is why it is not declared with an empty default.
# ATTESTATION_PRIVATE_KEY: ${ATTESTATION_PRIVATE_KEY:-}
# --- Upstream services --------------------------------------------
JETSTREAM_URL: ${JETSTREAM_URL:-wss://jetstream1.us-east.bsky.network}
RELAY_URL: ${RELAY_URL:-https://bsky.network}
PLC_URL: ${PLC_URL:-https://plc.directory}
# --- Branding -----------------------------------------------------
# Seed values for the OAuth authorization screen, each overridden by the
# matching dashboard setting once one is saved. Left undeclared on
# purpose: the code distinguishes unset from empty, and an empty value
# here would put a blank app name and empty URIs on the consent screen
# rather than falling back to the defaults. Uncomment to use them.
# APP_NAME: ${APP_NAME:-}
# LOGO_URI: ${LOGO_URI:-}
# TOS_URI: ${TOS_URI:-}
# POLICY_URI: ${POLICY_URI:-}
# --- Operational ---------------------------------------------------
# The dev default (`happyview=debug`) is very noisy in production.
RUST_LOG: ${RUST_LOG:-happyview=info,tower_http=info,sqlx=warn}
EVENT_LOG_RETENTION_DAYS: ${EVENT_LOG_RETENTION_DAYS:-30}
DEFAULT_RATE_LIMIT_CAPACITY: ${DEFAULT_RATE_LIMIT_CAPACITY:-100}
DEFAULT_RATE_LIMIT_REFILL_RATE: ${DEFAULT_RATE_LIMIT_REFILL_RATE:-2.0}
# Backfill concurrency. Each of these feeds the backfill pool size — see
# the POSTGRES_MAX_CONNECTIONS note above before raising them.
# BACKFILL_CONCURRENT_PDS: ${BACKFILL_CONCURRENT_PDS:-10}
# BACKFILL_CONCURRENT_DIDS_PER_PDS: ${BACKFILL_CONCURRENT_DIDS_PER_PDS:-3}
# BACKFILL_CONCURRENT_RESOLUTION: ${BACKFILL_CONCURRENT_RESOLUTION:-100}
# Plugins to preload, comma-separated: `id|url|sha256:<digest>`.
# PLUGIN_URLS: ${PLUGIN_URLS:-}
# The runtime image carries no curl or wget, so this probes /health over
# bash's /dev/tcp. `sh` is dash here and does not support it — hence CMD
# rather than CMD-SHELL.
#
# 60s covers migrations on first boot. Unlike the SQLite stack there is no
# startup VACUUM to wait out — scheduling one is rejected with 400 on
# Postgres — so this does not need the SQLite file's longer window.
healthcheck:
test:
- CMD
- bash
- -c
- 'exec 3<>/dev/tcp/127.0.0.1/3000 && printf "GET /health HTTP/1.0\r\nHost: localhost\r\n\r\n" >&3 && head -n 1 <&3 | grep -q 200'
interval: 30s
timeout: 5s
retries: 3
start_period: 60s
# `entrypoint.sh` execs the server, making it PID 1 — where the kernel
# drops signals that have only their default disposition, and the server
# installs no SIGTERM handler. Without an init, every `stop`, `restart` and
# `down` therefore hangs for the full grace period and ends in SIGKILL
# (exit 137). `init: true` puts tini at PID 1 to forward the signal, so the
# server exits promptly. Note this is prompt, not graceful: there is no
# graceful-shutdown handler, so in-flight requests are dropped either way,
# and an interrupted job is re-queued on the next boot.
init: true
stop_grace_period: 30s
# Container stdout is the only log sink; without a cap json-file logs grow
# unbounded. Ship these to an aggregator if you need retention.
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
# --------------------------------------------------------------------------
# Optional: Caddy as the TLS-terminating reverse proxy.
#
# HappyView does not terminate TLS. Uncommenting this puts Caddy in front of
# it with an automatically obtained and renewed Let's Encrypt certificate.
# Skip it if you already have a proxy, a platform load balancer, or a
# Cloudflare Tunnel doing the same job.
#
# Create a `Caddyfile` beside this file containing:
#
# {$CADDY_DOMAIN} {
# reverse_proxy happyview:3000
# }
#
# and set CADDY_DOMAIN in your env file to the hostname from PUBLIC_URL with
# the scheme stripped — `happyview.example.com` for
# `https://happyview.example.com`. PUBLIC_URL itself stays the full URL.
#
# When you enable this, also:
#
# - Delete the `ports` block from the `happyview` service. Caddy reaches it
# over the compose network, so publishing it to the host as well only
# widens the exposure. Leave HTTP_BIND unset.
# - Point DNS at this host *before* the first `up`. Caddy asks Let's
# Encrypt for a certificate on startup, and a challenge against a
# hostname that does not resolve here fails and retries with backoff.
#
# For a subpath deployment (BASE_PATH), the Caddyfile needs an extra rewrite
# so `/xrpc/*` still resolves at the domain root — see "Reverse proxy
# subpath" in the deployment docs.
#
# caddy:
# image: caddy:2-alpine
# restart: unless-stopped
#
# # Deliberately `service_started`, not `service_healthy`. Caddy should come
# # up and start the ACME exchange immediately rather than waiting out
# # HappyView's start period, which on a first boot is minutes of migrations
# # with nothing listening on 80 or 443. A request arriving before the
# # backend is ready just gets a 502.
# depends_on:
# happyview:
# condition: service_started
#
# # Port 80 must stay open. It serves the HTTP->HTTPS redirect *and* the
# # ACME HTTP-01 challenge — which is also how renewal works, so closing it
# # once the first certificate issues makes renewal fail silently about 60
# # days later. 443/udp carries HTTP/3.
# #
# # DNS-01 (needed for wildcards, or when 80 cannot be exposed) is not
# # possible with this image: the stock build ships no DNS provider
# # modules, and adding one means building a custom image with xcaddy.
# ports:
# - "80:80"
# - "443:443"
# - "443:443/udp"
#
# environment:
# CADDY_DOMAIN: ${CADDY_DOMAIN:?set CADDY_DOMAIN to the hostname in PUBLIC_URL, without the scheme}
#
# volumes:
# - ./Caddyfile:/etc/caddy/Caddyfile:ro
# # Issued certificates and the ACME account key. This MUST persist.
# # Without it every recreate re-issues from scratch, and Let's Encrypt
# # allows only 5 duplicate certificates per week before it starts
# # refusing — which locks the site out of HTTPS for days. Back it up
# # with the same care as the data volume, or accept a re-issue on
# # restore.
# - caddy-data:/data
# - caddy-config:/config
#
# logging:
# driver: json-file
# options:
# max-size: "10m"
# max-file: "5"
volumes:
# The database. This is the only stateful path — back this volume up.
pgdata:
# Uncomment together with the caddy service above.
# caddy-data:
# caddy-config: