Repository navigation
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
154 lines (149 loc) · 5.33 KB
/
Copy pathdocker-compose.yml
File metadata and controls
154 lines (149 loc) · 5.33 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
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: freedink
POSTGRES_PASSWORD: freedink
POSTGRES_DB: freedink
ports:
- '5432:5432'
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U freedink -d freedink']
interval: 5s
timeout: 3s
retries: 10
app:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL: postgres://freedink:freedink@postgres:5432/freedink
PUBLIC_ORIGIN: ${PUBLIC_ORIGIN:-http://localhost:3000}
PUBLIC_RP_ID: ${PUBLIC_RP_ID:-localhost}
PUBLIC_RP_NAME: ${PUBLIC_RP_NAME:-Freed Ink}
SESSION_SECRET: ${SESSION_SECRET:-dev_only_replace_in_prod_dev_only_replace_in_prod_aaaaaaaaaaaaaaaa}
SMTP_URL: ${SMTP_URL:-}
SMTP_FROM: ${SMTP_FROM:-noreply@freed.ink}
NODE_ENV: production
PORT: 3000
HOST: 0.0.0.0
ORIGIN: ${PUBLIC_ORIGIN:-http://localhost:3000}
SHUTDOWN_GRACE_SECONDS: ${SHUTDOWN_GRACE_SECONDS:-25}
ports:
- '3000:3000'
healthcheck:
# /healthz returns 200 only when the DB round-trips in <2s.
test: ['CMD-SHELL', 'wget -q -O- http://localhost:3000/healthz || exit 1']
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
# Periodic pg_dump → /backups (mounted volume). Independent of the app.
# See scripts/backup.sh for behaviour; tune BACKUP_INTERVAL_SECONDS to taste.
backup:
image: postgres:16-alpine
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
PGHOST: postgres
PGUSER: freedink
PGPASSWORD: freedink
PGDATABASE: freedink
BACKUP_INTERVAL_SECONDS: ${BACKUP_INTERVAL_SECONDS:-21600}
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-14}
BACKUP_DIR: /backups
volumes:
- freedink_backups:/backups
- ./scripts/backup.sh:/usr/local/bin/backup.sh:ro
entrypoint: ['/bin/sh', '/usr/local/bin/backup.sh']
healthcheck:
# Healthy iff a backup completed within 2× the configured interval.
# The sentinel file is rewritten on every successful pg_dump.
test:
- 'CMD-SHELL'
- 'now=$$(date +%s); last=$$(cat /backups/.last_ok_at 2>/dev/null || echo 0); max=$$((${BACKUP_INTERVAL_SECONDS:-21600} * 2)); test $$((now - last)) -lt $$max'
interval: 60s
timeout: 5s
retries: 5
start_period: 60s
# Background reaper for ephemeral / expired rows (sessions, nonces, rate
# limits, etc.) AND the status-page probe loop. Both processes run in this
# one container — saves an image pull and shares the postgres-js dependency.
#
# Process layout: a tiny inline launcher backgrounds the status probe and
# `exec`s the cleanup loop as PID 1 so docker's SIGTERM lands on cleanup
# directly. When cleanup exits we kill the probe; when the probe dies the
# container keeps running (cleanup is the critical job) but the missing
# status_checks rows will surface on /status as a stale-data warning.
scheduler:
image: node:22-alpine
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL: postgres://freedink:freedink@postgres:5432/freedink
CLEANUP_INTERVAL_SECONDS: ${CLEANUP_INTERVAL_SECONDS:-600}
CLEANUP_LOOP: '1'
CLEANUP_SENTINEL: /tmp/cleanup.last_ok
STATUS_PROBE_INTERVAL_SECONDS: ${STATUS_PROBE_INTERVAL_SECONDS:-30}
STATUS_PROBE_URL: ${STATUS_PROBE_URL:-http://app:3000/healthz}
STATUS_PROBE_LOOP: '1'
STATUS_PROBE_SENTINEL: /tmp/status-probe.last_ok
NODE_ENV: production
working_dir: /app
# Bind-mount source so we don't need a separate build target. node:22-alpine
# ships with everything we need for the cleanup script (postgres dep is
# installed via /app/node_modules from the app's build context).
volumes:
- ./scripts:/app/scripts:ro
- ./node_modules:/app/node_modules:ro
- ./package.json:/app/package.json:ro
entrypoint: ['/bin/sh', '-c']
command:
- |
set -e
node --experimental-strip-types scripts/status-probe.ts &
PROBE_PID=$$!
# Propagate cleanup's exit so docker's restart policy can react if
# the critical reaper dies. Kill the probe so we don't leak it.
trap 'kill $$PROBE_PID 2>/dev/null || true' EXIT
exec node --experimental-strip-types scripts/cleanup.ts
healthcheck:
# Healthy iff the sentinel file is younger than 2× the interval.
test:
- 'CMD-SHELL'
- 'now=$$(date +%s); last=$$(stat -c %Y /tmp/cleanup.last_ok 2>/dev/null || echo 0); max=$$((${CLEANUP_INTERVAL_SECONDS:-600} * 2)); test $$((now - last)) -lt $$max'
interval: 60s
timeout: 5s
retries: 5
start_period: 30s
caddy:
image: caddy:2-alpine
restart: unless-stopped
depends_on:
- app
ports:
- '80:80'
- '443:443'
environment:
PUBLIC_HOST: ${PUBLIC_HOST:-freed.ink.localhost}
ACME_EMAIL: ${ACME_EMAIL:-admin@freed.ink}
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
volumes:
pgdata:
caddy_data:
caddy_config:
freedink_backups: