-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathdocker-compose.production.yml
More file actions
269 lines (256 loc) · 9.56 KB
/
Copy pathdocker-compose.production.yml
File metadata and controls
269 lines (256 loc) · 9.56 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
# Production overrides — apply on top of docker-compose.yml:
# docker compose -f docker-compose.yml -f docker-compose.production.yml up -d
#
# Goals:
# - everything restarts on failure
# - internal services (postgres / redis / minio / grobid) NOT exposed to host
# - Caddy in front, only one external service (ports 80/443)
# - secrets pulled from /opt/openxiv/.env on the host
name: openxiv
services:
postgres:
ports: !reset []
deploy:
resources:
limits:
memory: 3g
redis:
ports: !reset []
deploy:
resources:
limits:
memory: 512m
minio:
ports: !reset []
environment:
MINIO_ROOT_USER: ${S3_ACCESS_KEY_ID:?set S3_ACCESS_KEY_ID}
MINIO_ROOT_PASSWORD: ${S3_SECRET_ACCESS_KEY:?set S3_SECRET_ACCESS_KEY}
deploy:
resources:
limits:
memory: 1g
minio-init:
environment:
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:?set S3_ACCESS_KEY_ID}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:?set S3_SECRET_ACCESS_KEY}
entrypoint: >-
/bin/sh -c "
mc alias set local http://minio:9000 $$S3_ACCESS_KEY_ID $$S3_SECRET_ACCESS_KEY &&
mc mb -p local/openxiv-blobs || true &&
mc anonymous set download local/openxiv-blobs || true &&
echo 'bucket ready'"
grobid:
image: lfoppiano/grobid:0.8.1
ports: !reset []
environment:
# Cap GROBID Java heap so it fits the 12 GB budget alongside Postgres.
JAVA_OPTS: "-Xmx3g -XX:+UseG1GC"
# The image ships without curl/wget — the upstream healthcheck (which
# invokes `curl -f http://localhost:8070/api/isalive`) is permanently
# FAILED on every install of lfoppiano/grobid:0.8.1. We override with
# a bash /dev/tcp pseudo-device probe that asks for /api/isalive over
# raw HTTP/1.0 and checks the response body for the literal "true".
# bash and grep are both present in the image.
healthcheck:
test:
- "CMD-SHELL"
- "bash -c 'exec 3<>/dev/tcp/localhost/8070; printf \"GET /api/isalive HTTP/1.0\\r\\nHost: localhost\\r\\n\\r\\n\" >&3; head -c 4096 <&3 | grep -qi true'"
interval: 15s
timeout: 5s
retries: 8
start_period: 90s
deploy:
resources:
limits:
memory: 4g
api:
env_file:
- .env
environment:
NODE_ENV: production
LOG_LEVEL: info
API_PORT: 4000
PUBLIC_API_BASE: https://openxiv.net
PUBLIC_WEB_BASE: https://openxiv.net
DATABASE_URL: postgres://openxiv:openxiv@postgres:5432/openxiv
REDIS_URL: redis://redis:6379
S3_ENDPOINT: http://minio:9000
S3_PUBLIC_ENDPOINT: https://openxiv.net
S3_REGION: auto
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:?set S3_ACCESS_KEY_ID}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:?set S3_SECRET_ACCESS_KEY}
S3_BUCKET: openxiv-blobs
S3_FORCE_PATH_STYLE: "true"
GROBID_URL: http://grobid:8070
USE_MOCK_CLIENTS: "false"
USE_MOCK_LLM: "false"
USE_MOCK_GROBID: "false"
USE_MOCK_ORCID: "false"
USE_MOCK_BLUESKY: "false"
# Intake runs in the API process and must compile the real source for
# preview/GROBID. A mock here produces a blank PDF and dirty metadata.
USE_MOCK_TECTONIC: "false"
USE_MOCK_LATEXML: "false"
USE_MOCK_DETECTOR: "false"
RATE_LIMIT_MAX: "60"
RATE_LIMIT_WINDOW_MS: "60000"
CORS_ORIGINS: "https://openxiv.net,https://www.openxiv.net,https://bsky.social"
SENTRY_DSN: ${SENTRY_DSN:-}
SENTRY_RELEASE: ${SENTRY_RELEASE:-}
SENTRY_TRACES_SAMPLE_RATE: ${SENTRY_TRACES_SAMPLE_RATE:-0}
# Override placeholder secrets from base docker-compose.yml — these come
# from /opt/openxiv/.env via compose's top-level .env variable substitution.
SESSION_SECRET: ${SESSION_SECRET}
JWT_SECRET: ${JWT_SECRET}
ports: !reset []
deploy:
resources:
limits:
memory: 1g
worker:
env_file:
- .env
environment:
NODE_ENV: production
LOG_LEVEL: info
OPENXIV_RUN_MIGRATIONS: "false"
DATABASE_URL: postgres://openxiv:openxiv@postgres:5432/openxiv
REDIS_URL: redis://redis:6379
WORKER_COMPILE_CONCURRENCY: "1"
WORKER_EMBED_CONCURRENCY: "2"
WORKER_EXPLAIN_CONCURRENCY: "2"
S3_ENDPOINT: http://minio:9000
S3_PUBLIC_ENDPOINT: https://openxiv.net
S3_REGION: auto
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:?set S3_ACCESS_KEY_ID}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:?set S3_SECRET_ACCESS_KEY}
S3_BUCKET: openxiv-blobs
S3_FORCE_PATH_STYLE: "true"
GROBID_URL: http://grobid:8070
USE_MOCK_CLIENTS: "false"
USE_MOCK_LLM: "false"
USE_MOCK_GROBID: "false"
# USE_MOCK_TECTONIC flipped to false — real Tectonic binary
# installed in the worker image (Alpine community pkg). The
# client adapter (packages/clients/src/compiler/tectonic.ts)
# now spawns the binary directly; the DooD path is gone.
USE_MOCK_TECTONIC: "false"
# LaTeXML is installed in the worker image and must succeed before the
# saga can publish. Mocking is forbidden in production.
USE_MOCK_LATEXML: "false"
USE_MOCK_DETECTOR: "false"
SENTRY_DSN: ${SENTRY_DSN:-}
SENTRY_RELEASE: ${SENTRY_RELEASE:-}
SENTRY_TRACES_SAMPLE_RATE: ${SENTRY_TRACES_SAMPLE_RATE:-0}
SESSION_SECRET: ${SESSION_SECRET}
JWT_SECRET: ${JWT_SECRET}
deploy:
resources:
limits:
memory: 1500m
web:
env_file:
- .env
environment:
NODE_ENV: production
HOST: 0.0.0.0
PORT: 4321
PUBLIC_API_BASE: https://openxiv.net
PUBLIC_WEB_BASE: https://openxiv.net
INTERNAL_API_BASE: http://api:4000
# Twitter Pixel — read at SSR time by TwitterPixel.astro via
# import.meta.env. Client-side React islands (HandlePicker,
# SubmissionWizard) use the same names but their values get
# inlined at *build* time; the source code carries the launch
# defaults as fallback so a missing build-time var doesn't
# silently break tracking. The flag below is the only knob we
# need at runtime to kill the pixel entirely.
PUBLIC_TWITTER_PIXEL_ID: ${PUBLIC_TWITTER_PIXEL_ID:-rch4y}
PUBLIC_TWITTER_SIGNUP_EVENT_ID: ${PUBLIC_TWITTER_SIGNUP_EVENT_ID:-tw-rch4y-rch5b}
PUBLIC_TWITTER_PAPERSUBMIT_EVENT_ID: ${PUBLIC_TWITTER_PAPERSUBMIT_EVENT_ID:-tw-rch4y-rch5e}
PUBLIC_TWITTER_TRACKING_ENABLED: ${PUBLIC_TWITTER_TRACKING_ENABLED:-true}
# Self-hosted Umami tracker (see docker-compose umami service below).
# When PUBLIC_UMAMI_SCRIPT_URL is empty, the layout skips the tag and
# no analytics is loaded. Both vars must be set for the tracker to
# appear.
PUBLIC_UMAMI_SCRIPT_URL: ${PUBLIC_UMAMI_SCRIPT_URL:-}
PUBLIC_UMAMI_WEBSITE_ID: ${PUBLIC_UMAMI_WEBSITE_ID:-}
ports: !reset []
deploy:
resources:
limits:
memory: 700m
# Privacy-friendly self-hosted analytics. Reuses the existing Postgres
# container with a separate `umami` database. The init container runs
# before umami and creates the database if it does not yet exist; the
# umami container itself then runs its own migrations on first start.
#
# First-time provisioning notes for the operator live in
# docs/ops/ANALYTICS.md. The website tracker is wired into the web app
# behind PUBLIC_UMAMI_SCRIPT_URL + PUBLIC_UMAMI_WEBSITE_ID. Caddy routes
# https://analytics.openxiv.net to this container on port 3000.
# Umami services are gated by the `analytics` Compose profile so they
# do not block the default stack from coming up. Bring them online with:
# docker compose --profile analytics -f ... -f ... up -d umami-init umami
# Until the operator generates UMAMI_APP_SECRET and adds DNS for
# analytics.openxiv.net, leave the profile inactive and the stack
# behaves exactly as before.
umami-init:
image: postgres:16-alpine
restart: "no"
profiles: ["analytics"]
depends_on:
postgres:
condition: service_started
environment:
PGPASSWORD: openxiv
entrypoint: >-
/bin/sh -c "
until pg_isready -h postgres -U openxiv -d openxiv >/dev/null 2>&1; do sleep 1; done;
psql -h postgres -U openxiv -d openxiv -tc \"SELECT 1 FROM pg_database WHERE datname='umami'\" | grep -q 1 ||
psql -h postgres -U openxiv -d openxiv -c 'CREATE DATABASE umami;';
echo 'umami database ready'
"
umami:
image: ghcr.io/umami-software/umami:postgresql-v2.20.0
restart: unless-stopped
profiles: ["analytics"]
depends_on:
postgres:
condition: service_started
umami-init:
condition: service_completed_successfully
environment:
DATABASE_URL: ${UMAMI_DATABASE_URL:-postgres://openxiv:openxiv@postgres:5432/umami}
DATABASE_TYPE: postgresql
# Default is intentionally insecure; the umami profile must not be
# enabled until the operator sets a real UMAMI_APP_SECRET via
# `openssl rand -hex 32`. The default keeps compose validation
# passing for the non-analytics stack which never starts umami.
APP_SECRET: ${UMAMI_APP_SECRET:-please-replace-via-openssl-rand-hex-32}
DISABLE_TELEMETRY: "1"
deploy:
resources:
limits:
memory: 384m
caddy:
image: caddy:2-alpine
restart: unless-stopped
depends_on:
- api
- web
- minio
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile.production:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
volumes:
pgdata:
redisdata:
miniodata:
caddy_data:
caddy_config: