Repository navigation
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
148 lines (144 loc) · 5.47 KB
/
Copy pathdocker-compose.yml
File metadata and controls
148 lines (144 loc) · 5.47 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
# OpenStudy stack: postgres + fastapi + frontend.
# All three containers live on an internal docker network. Only `frontend`
# (which serves the SPA and reverse-proxies API traffic to fastapi) and
# `openstudy` (kept exposed for direct curl debugging) are bound to the host.
# A user's outer reverse proxy (Caddy / nginx / Traefik / whatever)
# terminates TLS and forwards to 127.0.0.1:8080 (the frontend container).
#
# Bring up: ./deploy.sh
# Logs: docker compose logs -f openstudy
# Shell in: docker compose exec openstudy bash
services:
postgres:
# Exact minor + digest. deploy.sh refuses a major-version change here
# (that needs pg_upgrade / dump-restore, not a container swap).
image: postgres:16.15-alpine@sha256:f4610795587492cd4fb887c2dc3de301b813278bb6f616992061141b5f15ac36
container_name: openstudy-postgres
restart: unless-stopped
env_file:
- path: .env.docker
format: raw
required: true
environment:
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C"
volumes:
- /opt/postgres-data:/var/lib/postgresql/data
# Least privilege: read-only rootfs, only the capabilities the official
# entrypoint needs (chown the data/socket dirs, then drop to `postgres`).
read_only: true
tmpfs:
- /var/run/postgresql
- /tmp
cap_drop: [ALL]
cap_add: [CHOWN, DAC_OVERRIDE, FOWNER, SETGID, SETUID]
security_opt: ["no-new-privileges:true"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 5s
timeout: 3s
retries: 12
start_period: 15s
networks: [internal]
# Never expose postgres to the host or internet — only the openstudy
# backend talks to it via the internal docker network (psycopg pool).
openstudy:
build:
context: .
dockerfile: Dockerfile
image: openstudy:latest
container_name: openstudy
restart: unless-stopped
env_file:
# format: raw disables ${VAR} interpolation inside values — required so
# APP_PASSWORD_HASH ("$argon2id$v=19$m=...") isn't mangled by compose
# treating $argon2id, $v, $m as undefined variables.
- path: .env
format: raw
required: true
- path: .env.docker
format: raw
required: true
environment:
# Tell the storage layer where the canonical course tree lives.
STUDY_ROOT: "/opt/courses"
# Least privilege: non-root user (Dockerfile), no capabilities,
# read-only rootfs. /tmp is where uploads spool.
read_only: true
tmpfs:
- /tmp
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
depends_on:
postgres:
condition: service_healthy
volumes:
# Bind mount, not volume — files live on the host, container reads/writes
# them in place. Same path inside and outside for sanity.
- /opt/courses:/opt/courses:rw
ports:
# Bind to localhost only; Caddy on the host reverse-proxies in.
- "127.0.0.1:8000:8000"
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8000/api/health"]
interval: 10s
timeout: 5s
retries: 6
start_period: 30s
networks: [internal]
frontend:
build:
# Context is the repo root (not just web/) because the Vite prebuild
# invokes scripts/build-seo.mjs which lives one level up.
context: .
dockerfile: web/Dockerfile
args:
# Build-time env baked into the SPA bundle.
#
# API_BASE_URL is empty: the in-image Caddy proxies /api/* to the
# openstudy container on the internal network, so the SPA always
# talks to the API as same-origin.
#
# PUBLIC_SITE_URL / PUBLIC_SITE_NAME come from your .env.docker
# (or shell env). They land in <link rel=canonical>, OpenGraph
# tags, JSON-LD, robots.txt, sitemap.xml and the manifest. Set
# them to YOUR domain and name; the localhost default keeps the
# build deterministic for first-run dev but is not what you want
# for a public deploy.
VITE_API_BASE_URL: ""
VITE_SITE_URL: "${PUBLIC_SITE_URL:-http://localhost:8080}"
VITE_SITE_NAME: "${PUBLIC_SITE_NAME:-OpenStudy}"
VITE_SHOW_LANDING: "${PUBLIC_SHOW_LANDING:-false}"
# Google Search Console verification token (paste from Search Console's
# "HTML tag" verification — the value of `content="..."`). Empty by
# default so forks aren't carrying the upstream owner's token.
VITE_GOOGLE_SITE_VERIFICATION: "${PUBLIC_GOOGLE_SITE_VERIFICATION:-}"
image: openstudy-frontend:latest
container_name: openstudy-frontend
restart: unless-stopped
depends_on:
openstudy:
condition: service_started
ports:
# Bind to localhost only; the host's reverse proxy (Caddy etc.)
# terminates TLS and forwards here.
- "127.0.0.1:8080:8080"
# Least privilege: non-root user on an unprivileged port, no
# capabilities, read-only rootfs (Caddy's state dirs are tmpfs).
read_only: true
tmpfs:
# Owned by the image's `web` user (uid 10001) — tmpfs defaults to root.
- /data:uid=10001,gid=10001,mode=0700
- /config:uid=10001,gid=10001,mode=0700
- /tmp
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/"]
interval: 15s
timeout: 5s
retries: 4
start_period: 10s
networks: [internal]
networks:
internal:
driver: bridge