-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
256 lines (250 loc) · 12.1 KB
/
Copy pathdocker-compose.yml
File metadata and controls
256 lines (250 loc) · 12.1 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
services:
mongo:
image: mongo:7
restart: unless-stopped
# Auth is enabled automatically when these are set. The init script then
# creates a least-privilege app user. NOTE: these only take effect on a FRESH
# data dir - to enable auth on an existing one, wipe ./data/mongo first.
environment:
MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USER:-root}
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD:?set MONGO_ROOT_PASSWORD in .env}
MONGO_INITDB_DATABASE: ${MONGO_DB:-kiwi}
MONGO_APP_USER: ${MONGO_APP_USER:-kiwi_app}
MONGO_APP_PASSWORD: ${MONGO_APP_PASSWORD:?set MONGO_APP_PASSWORD in .env}
# Bound to localhost only - the api reaches Mongo over the internal compose
# network; this mapping is just for host-side admin access.
ports:
- "27016:27017"
# Local-folder bind mount (no named volume). The host directory ./data/mongo
# holds the database files directly.
volumes:
- ./data/mongo:/data/db
- ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
healthcheck:
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping')"]
interval: 10s
timeout: 5s
retries: 5
mem_limit: 1g
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" }
redis:
image: redis:7-alpine
restart: unless-stopped
# Internal only (no host port). Password-protected. PERSISTENT: RDB snapshots
# to the bind-mounted ./data/redis so the warmed page caches (leaderboards /
# cheaters / alt clusters) + the ready-anchor pointer SURVIVE a container
# recreate. A deploy / --force-recreate / image bump no longer wipes the
# cache, so users keep seeing the last snapshot (the warmer refreshes it in
# the background) - no cold "first visitor pays for it" gap, even mid-ingest.
# `--save 60 1` snapshots ~60s after any change (and Redis also saves on a
# graceful shutdown). maxmemory + allkeys-lru bounds it within mem_limit and
# evicts the OLDEST snapshots under pressure instead of failing writes.
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD:?set REDIS_PASSWORD in .env}",
"--save", "60 1", "--appendonly", "no",
"--maxmemory", "200mb", "--maxmemory-policy", "allkeys-lru"]
volumes:
- ./data/redis:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
mem_limit: 256m
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" }
# Dedicated store for the high-volume leaderboards domain (entries, boards,
# players, activity estimates) - partitioned by anchor + bulk-loaded via COPY.
# Everything else stays in Mongo. Internal only; host port for admin access.
postgres:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-kiwi_lb}
POSTGRES_USER: ${POSTGRES_USER:-kiwi}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
ports:
- "5432:5432"
volumes:
- ./data/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-kiwi} -d ${POSTGRES_DB:-kiwi_lb}"]
interval: 10s
timeout: 5s
retries: 5
# Size for the leaderboards working set - bump as it grows (the whole point of
# the move off the 1g-capped Mongo). shm_size for big sorts/hashes/COPY.
mem_limit: 4g
shm_size: 1g
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" }
api:
build: .
restart: unless-stopped
# One container fronted by two hostnames at the reverse proxy, both -> :15546:
# api.aallyn.net -> only /v1 + /health (production API)
# dev.aallyn.net -> full app: /auth, /tokens, /docs (developer portal)
# Bound to localhost so only the local reverse proxy can reach it.
ports:
- "15546:8000"
environment:
MONGO_URI: mongodb://${MONGO_APP_USER:-kiwi_app}:${MONGO_APP_PASSWORD}@mongo:27017/${MONGO_DB:-kiwi}?authSource=${MONGO_DB:-kiwi}
MONGO_DB: ${MONGO_DB:-kiwi}
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
# Postgres - the leaderboards domain only (entries/boards/players/activity).
POSTGRES_DSN: ${POSTGRES_DSN:-postgresql://${POSTGRES_USER:-kiwi}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-kiwi_lb}}
# Override these in a .env file (see .env.example) - do NOT ship the defaults.
SECRET_KEY: ${SECRET_KEY:-CHANGE_ME_insecure_dev_secret_do_not_use_in_production}
CAPTCHA_PROVIDER: ${CAPTCHA_PROVIDER:-turnstile}
CAPTCHA_SECRET: ${CAPTCHA_SECRET:-}
CAPTCHA_SITEKEY: ${CAPTCHA_SITEKEY:-}
# Master/admin account - created or promoted to superuser on startup.
ADMIN_EMAIL: ${ADMIN_EMAIL:-}
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-}
USAGE_RETENTION_DAYS: ${USAGE_RETENTION_DAYS:-30}
# Email (Postfix on the host). host.docker.internal resolves via extra_hosts.
SMTP_HOST: ${SMTP_HOST:-}
SMTP_PORT: ${SMTP_PORT:-25}
SMTP_USERNAME: ${SMTP_USERNAME:-}
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
SMTP_STARTTLS: ${SMTP_STARTTLS:-false}
SMTP_SSL: ${SMTP_SSL:-false}
MAIL_FROM: ${MAIL_FROM:-no-reply@aallyn.net}
# Daily rate-limit digest recipient (and how aggressive).
RATE_LIMIT_ALERT_EMAIL: ${RATE_LIMIT_ALERT_EMAIL:-aallyn@aallyn.net}
RATE_LIMIT_ALERT_THRESHOLD: ${RATE_LIMIT_ALERT_THRESHOLD:-20}
# "Sign in with GitHub" (optional). Leave blank to disable.
GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:-}
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:-}
# "Sign in with Discord" + Discord interactions (optional). Leave blank to disable.
DISCORD_CLIENT_ID: ${DISCORD_CLIENT_ID:-}
DISCORD_CLIENT_SECRET: ${DISCORD_CLIENT_SECRET:-}
DISCORD_PUBLIC_KEY: ${DISCORD_PUBLIC_KEY:-}
DISCORD_BOT_TOKEN: ${DISCORD_BOT_TOKEN:-}
DISCORD_OAUTH_SCOPE: ${DISCORD_OAUTH_SCOPE:-identify email guilds}
DISCORD_OAUTH_INTEGRATION_TYPE: ${DISCORD_OAUTH_INTEGRATION_TYPE:-}
DISCORD_INSTALL_URL: ${DISCORD_INSTALL_URL:-}
# Game-file version archiver. OFF by default - set to true ONLY when the
# blob disk (./.game_updates) has room; the first sync pulls multi-GB.
TROVE_UPDATE_ENABLED: ${TROVE_UPDATE_ENABLED:-false}
TROVE_UPDATE_STORE_DIR: ${TROVE_UPDATE_STORE_DIR:-/data/updates}
# Server-side ingest backlog (gzipped dumps keyed by anchor; re-ingestable
# from the admin panel). Bind-mounted below; drop <unix>.cfg files in too.
BACKLOG_DIR: ${BACKLOG_DIR:-/data/backlog}
# Mods Hub store: per-project git repos + images + compiled .tmod blobs.
# Bind-mounted below so mods survive a container rebuild.
MODS_STORE_DIR: ${MODS_STORE_DIR:-/data/mods}
# Delve-rotation source (week-based). Refresher stays off until the URL is set.
TROVE_DELVE_SOURCE_URL: ${TROVE_DELVE_SOURCE_URL:-}
TROVE_DELVE_SOURCE_REFERER: ${TROVE_DELVE_SOURCE_REFERER:-}
# BTT release relay: optional GitHub PAT (lifts 60/hr unauth -> 5000/hr).
BTT_RELEASES_REPO: ${BTT_RELEASES_REPO:-AallynReed/BetterTroveTools}
BTT_RELEASES_TOKEN: ${BTT_RELEASES_TOKEN:-}
# Community feeds: Twitch + YouTube credentials (Bilibili is credential-less).
# Leave blank to disable that feed (it just serves empty).
TWITCH_CLIENT_ID: ${TWITCH_CLIENT_ID:-}
TWITCH_CLIENT_SECRET: ${TWITCH_CLIENT_SECRET:-}
YT_API_KEY: ${YT_API_KEY:-}
depends_on:
mongo:
condition: service_healthy
redis:
condition: service_healthy
postgres:
condition: service_healthy
# Lets the container reach the host's Postfix as host.docker.internal.
extra_hosts:
- "host.docker.internal:host-gateway"
# Bind-mount the source so host edits are picked up; --reload restarts on change.
# (Production override docker-compose.prod.yml swaps --reload for --workers.)
volumes:
- ./app:/app/app
# Content-addressed blob store for the game-file archive (host: ./.game_updates).
# Many GB once enabled - kept out of git/Syncthing/the image build context.
- ./.game_updates:/data/updates
# Ingest backlog: every leaderboard dump the API receives is gzip-saved here
# keyed by anchor, so the whole history can be re-ingested from the admin
# panel with no upload. Drop <unix>.cfg files in by hand too. Kept out of
# git/Syncthing (can grow large).
- ./.backlog:/data/backlog
# Mods Hub store: shared mods' git repos, uploaded images and compiled
# .tmod release artifacts. Kept out of git/Syncthing (user content, grows).
- ./.mods:/data/mods
# BetterTroveTools showcase site (templates + static + ~20 MB of screenshots).
# Bind-mounted so the heavy binary assets don't bake into the image; the
# api container serves trove.aallyn.net out of this directory.
- ./site:/app/site:ro
# NOTE: no --reload. It's a dev-only flag - it restarts the worker on any
# .py change (Syncthing/deploy touch those constantly), which kills in-flight
# leaderboard ingests + the cache warmer mid-pass. Use ./deploy.sh (it
# recreates the container) to pick up code changes instead.
command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000",
"--proxy-headers", "--forwarded-allow-ips", "*"]
# Headroom for the update archiver: it decompresses several .tfa archives
# concurrently (each held whole in RAM), which the old 512m couldn't fit.
# If a single giant archive still OOMs, lower TROVE_UPDATE_CONCURRENCY instead.
mem_limit: 2g
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" }
# Kiwi gateway bot (discord.py): a separate always-on process sharing the same
# image + Mongo as the api. Handles proactive Discord work (hourly-challenge
# announcements now; guild/role events later). Slash commands still go to the
# api's HTTP interactions endpoint - this is the gateway side only.
bot:
build: .
restart: unless-stopped
environment:
MONGO_URI: mongodb://${MONGO_APP_USER:-kiwi_app}:${MONGO_APP_PASSWORD}@mongo:27017/${MONGO_DB:-kiwi}?authSource=${MONGO_DB:-kiwi}
MONGO_DB: ${MONGO_DB:-kiwi}
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
SECRET_KEY: ${SECRET_KEY:-CHANGE_ME_insecure_dev_secret_do_not_use_in_production}
DISCORD_BOT_TOKEN: ${DISCORD_BOT_TOKEN:-}
DISCORD_CLIENT_ID: ${DISCORD_CLIENT_ID:-}
# Read Postgres-backed data (activity) from the API over the compose network -
# the bot has no Postgres. Points at the api service; depends_on it below.
INTERNAL_API_URL: ${INTERNAL_API_URL:-http://api:8000}
depends_on:
mongo:
condition: service_healthy
redis:
condition: service_healthy
# For the activity HTTP fallback. Soft dep (started, not healthy) - the
# internal_get calls fail gracefully if the API is briefly down.
api:
condition: service_started
# Bind-mount source like the api so a deploy = sync + recreate.
volumes:
- ./app:/app/app
command: ["python", "-m", "app.bot.runner"]
mem_limit: 512m
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" }
# Developer portal SPA at dev.aallyn.net: serves the static app only. The SPA
# calls api.aallyn.net directly (cross-origin; CORS-enabled).
portal:
image: nginx:alpine
restart: unless-stopped
# Reverse-proxied (dev.aallyn.net) -> host :25470 -> container :80.
ports:
- "25470:80"
volumes:
- ./portal/html:/usr/share/nginx/html:ro
- ./portal/nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- api
docs:
image: nginx:alpine
restart: unless-stopped
# Reverse-proxied (docs.aallyn.net) -> host :25468 -> container :80.
ports:
- "25468:80"
# Static docs served straight from the local ./docs folder (read-only bind mount).
# docs-nginx.conf gives clean URLs (/reference instead of /reference.html).
volumes:
- ./docs:/usr/share/nginx/html:ro
- ./docs-nginx.conf:/etc/nginx/conf.d/default.conf:ro