-
Notifications
You must be signed in to change notification settings - Fork 65
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
214 lines (206 loc) · 8.74 KB
/
Copy pathdocker-compose.yml
File metadata and controls
214 lines (206 loc) · 8.74 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
# ─────────────────────────────────────────────────────────────
# Self-hosting stack: postgres + minio + api + worker + bot + web.
# Coolify uses docker-compose.coolify.yml; local dev uses docker-compose.dev.yml
# (backing services only).
#
# cp .env.example .env # fill in the required values below
# docker compose up -d
#
# Required in .env (the stack refuses to start without them):
# POSTGRES_PASSWORD, BETTER_AUTH_SECRET, APP_ENCRYPTION_KEY, WORKER_INTERNAL_TOKEN,
# S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, API_URL, APP_URL
# Generate each secret with: openssl rand -base64 32
#
# The four services run from images published on each release; update with
# docker compose pull && docker compose up -d
# VERSION in .env pins a release (defaults to the latest one). To run this
# checkout instead of the published images, build them:
# docker compose up -d --build
# ─────────────────────────────────────────────────────────────
name: itsaplan
services:
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-itsaplan}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
POSTGRES_DB: ${POSTGRES_DB:-itsaplan}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-itsaplan} -d ${POSTGRES_DB:-itsaplan}']
interval: 5s
timeout: 5s
retries: 10
# S3-compatible object store for issue attachments.
# Both the S3 API (:9000) and the console (:9001) stay inside the compose
# network: the api streams attachment bytes through itself, so no port is
# published. Administer through mc from inside the network or an SSH tunnel.
minio:
image: minio/minio:latest
restart: unless-stopped
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${S3_ACCESS_KEY_ID:?set S3_ACCESS_KEY_ID in .env}
MINIO_ROOT_PASSWORD: ${S3_SECRET_ACCESS_KEY:?set S3_SECRET_ACCESS_KEY in .env}
volumes:
- minio-data:/data
healthcheck:
test: ['CMD', 'mc', 'ready', 'local']
interval: 15s
timeout: 5s
retries: 5
start_period: 20s
# One-shot: waits for MinIO, creates the attachments bucket, then exits.
minio-init:
image: minio/mc:latest
restart: 'no'
depends_on:
- minio
environment:
MINIO_ROOT_USER: ${S3_ACCESS_KEY_ID}
MINIO_ROOT_PASSWORD: ${S3_SECRET_ACCESS_KEY}
S3_BUCKET: ${S3_BUCKET:-planner-attachments}
entrypoint: >
/bin/sh -c "
until mc alias set local http://minio:9000 \"$$MINIO_ROOT_USER\" \"$$MINIO_ROOT_PASSWORD\"; do echo 'waiting for minio...'; sleep 2; done;
mc mb --ignore-existing local/\"$$S3_BUCKET\";
echo 'bucket ready: '\"$$S3_BUCKET\";
"
# Applies database migrations on startup, then serves the API.
api:
image: ghcr.io/croffasia/itsaplan-api:${VERSION:-latest}
build:
context: .
dockerfile: apps/api/Dockerfile
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
minio-init:
condition: service_completed_successfully
environment:
NODE_ENV: production
API_PORT: 3000
DATABASE_URL: postgres://${POSTGRES_USER:-itsaplan}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-itsaplan}
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?set BETTER_AUTH_SECRET in .env}
# Encrypts stored provider credentials at rest (AES-256-GCM). Changing it
# makes existing stored credentials undecryptable.
APP_ENCRYPTION_KEY: ${APP_ENCRYPTION_KEY:?set APP_ENCRYPTION_KEY in .env}
# Authenticates the worker and the bot against the /internal/* routes.
WORKER_INTERNAL_TOKEN: ${WORKER_INTERNAL_TOKEN:?set WORKER_INTERNAL_TOKEN in .env}
# Public origins, each entered once. API_URL is the api origin (auth handler,
# email links); APP_URL is the web origin (CORS, passkey, email links).
API_URL: ${API_URL:?public origin of the api, e.g. https://api.example.com}
APP_URL: ${APP_URL:?public origin of the web app, e.g. https://app.example.com}
# Set COOKIE_DOMAIN only to override the parent domain derived from
# APP_URL (multi-label TLDs, deep subdomains).
COOKIE_DOMAIN: ${COOKIE_DOMAIN:-}
S3_ENDPOINT: http://minio:9000
S3_BUCKET: ${S3_BUCKET:-planner-attachments}
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
S3_REGION: ${S3_REGION:-us-east-1}
S3_FORCE_PATH_STYLE: ${S3_FORCE_PATH_STYLE:-}
# Hosts trusted even though they resolve privately. Also governs webhook URL
# validation and attachment import.
SSRF_ALLOWED_HOSTS: ${SSRF_ALLOWED_HOSTS:-}
# How long the pre-migration dumps are kept, and the switch that upgrades
# without one. BACKUP_DIR is not among them: the volume below pins it.
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-}
SKIP_PRE_MIGRATION_BACKUP: ${SKIP_PRE_MIGRATION_BACKUP:-}
# Holds the dump migrate.ts takes before it applies a release's migrations, and
# keeps it for BACKUP_RETENTION_DAYS (30) so a downgrade has something to restore.
volumes:
- db-backups:/backups
# start_period covers the backup and the migrations the container applies before
# it listens.
healthcheck:
test: ['CMD', 'wget', '-q', '-O', '/dev/null', 'http://127.0.0.1:3000/']
interval: 15s
timeout: 5s
retries: 5
start_period: 180s
ports:
- '${API_PORT:-3000}:3000'
# Webhook delivery, notification delivery, agent schedules, and agent runs.
# Shares the database with the api. No public ports.
worker:
image: ghcr.io/croffasia/itsaplan-worker:${VERSION:-latest}
build:
context: .
dockerfile: apps/worker/Dockerfile
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
api:
condition: service_started
environment:
NODE_ENV: production
DATABASE_URL: postgres://${POSTGRES_USER:-itsaplan}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-itsaplan}
SERVICE_URL_API: http://api:3000
WORKER_INTERNAL_TOKEN: ${WORKER_INTERNAL_TOKEN}
# Anonymous telemetry (TELEMETRY.md). Either variable, set to anything but 0,
# turns it off.
TELEMETRY_DISABLED: ${TELEMETRY_DISABLED:-}
DO_NOT_TRACK: ${DO_NOT_TRACK:-}
TELEMETRY_DEBUG: ${TELEMETRY_DEBUG:-}
# The same list the api reads. This process delivers webhooks to those hosts.
SSRF_ALLOWED_HOSTS: ${SSRF_ALLOWED_HOSTS:-}
# Optional tuning lives in apps/worker/src/config.ts.
# Telegram bot: long polling for the instance bot, completing the account links
# users start from their profile. The bot token is stored in the database and
# edited in god mode, so it is not an environment variable here.
#
# Must stay at one replica: Telegram hands each getUpdates call to a single
# caller, so a second instance would take updates away from the first.
bot:
image: ghcr.io/croffasia/itsaplan-bot:${VERSION:-latest}
build:
context: .
dockerfile: apps/bot/Dockerfile
restart: unless-stopped
depends_on:
api:
condition: service_started
environment:
NODE_ENV: production
SERVICE_URL_API: http://api:3000
WORKER_INTERNAL_TOKEN: ${WORKER_INTERNAL_TOKEN}
# The origins are read at startup and handed to the browser by the server render,
# so the image carries none of them and a changed API_URL only needs a restart.
web:
image: ghcr.io/croffasia/itsaplan-web:${VERSION:-latest}
build:
context: .
dockerfile: apps/web/Dockerfile
restart: unless-stopped
depends_on:
- api
environment:
NODE_ENV: production
PORT: 3001
HOSTNAME: 0.0.0.0
# The api origin the browser uses.
API_URL: ${API_URL:?public origin of the api, e.g. https://api.example.com}
# Where web itself reaches the api: it streams avatars and attachments from
# there (/media) so they are served from the web origin.
SERVICE_URL_API: http://api:3000
# Legal document URLs shown on the logged-out screens; empty hides the notice.
PRIVACY_URL: ${PRIVACY_URL:-}
TERMS_URL: ${TERMS_URL:-}
# /login renders without a session, and without reaching the api.
healthcheck:
test: ['CMD', 'wget', '-q', '-O', '/dev/null', 'http://127.0.0.1:3001/login']
interval: 15s
timeout: 5s
retries: 5
start_period: 20s
ports:
- '${WEB_PORT:-3001}:3001'
volumes:
postgres-data:
minio-data:
db-backups: