-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker-compose.prod.yml
More file actions
165 lines (159 loc) · 6.67 KB
/
Copy pathdocker-compose.prod.yml
File metadata and controls
165 lines (159 loc) · 6.67 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
# Production self-host stack for maarkn.dev (EC2 / any Docker host).
# Next app (built from Dockerfile) + Postgres/pgvector + Traefik v3.6 (TLS).
#
# docker compose -f docker-compose.prod.yml up -d --build
#
# Expects a .env file beside this compose with production values:
# POSTGRES_PASSWORD, APP_DOMAIN, ACME_EMAIL, OPENAI_API_KEY, AUTH_SECRET,
# ADMIN_EMAIL, ADMIN_PASSWORD, and DATABASE_URL pointing at the `postgres`
# service, e.g.:
# DATABASE_URL=postgresql://maarkn:${POSTGRES_PASSWORD}@postgres:5432/maarkn?schema=public
#
# First deploy — apply migrations before the app serves traffic:
# docker compose -f docker-compose.prod.yml run --rm migrate
# (the `migrate` service reuses the build and runs `prisma migrate deploy`).
#
# EC2 security group: open inbound 80 (HTTP→HTTPS redirect + ACME) and 443 (HTTPS).
# Point an A record for ${APP_DOMAIN} at the instance's public IP before `up`.
services:
traefik:
image: traefik:v3.6
restart: unless-stopped
command:
# --- providers ---
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --providers.docker.network=web
# --- entrypoints ---
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
# global HTTP -> HTTPS redirect
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
# TLS on the HTTPS entrypoint. NOTE: this line does *not* set HSTS --
# Strict-Transport-Security is emitted by the app itself (see
# `securityHeaders` in next.config.ts), so it stays version-controlled and
# unit-tested. If you ever serve something on this entrypoint that is not
# the app (e.g. the Traefik dashboard below), give it HSTS too with a
# headers middleware:
# traefik.http.middlewares.hsts.headers.stsSeconds=31536000
# traefik.http.routers.<router>.middlewares=hsts
- --entrypoints.websecure.http.tls=true
# --- Let's Encrypt (TLS-ALPN-01 challenge, needs port 443) ---
- --certificatesresolvers.le.acme.tlschallenge=true
- --certificatesresolvers.le.acme.email=${ACME_EMAIL:?set ACME_EMAIL}
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
# --- observability ---
- --accesslog=true
- --log.level=INFO
- --ping=true
# --- dashboard (optional): uncomment + the router labels below to expose it
# - --api.dashboard=true
ports:
- "80:80"
- "443:443"
volumes:
# WARNING: `:ro` is not a security boundary. It only stops writes to the
# socket *file*; anything that can talk to the socket can drive the Docker
# daemon -- create a privileged container, bind-mount `/`, take the host.
# Traefik only needs to *read* container metadata, so the real fix is a
# socket proxy (e.g. tecnativa/docker-socket-proxy with CONTAINERS=1 and
# everything else off) on the `internal` network, with Traefik pointed at
# `--providers.docker.endpoint=tcp://docker-socket-proxy:2375` and this
# mount removed. Not done here because it changes the running topology and
# could not be validated from a workstation -- owner's call.
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik-letsencrypt:/letsencrypt
networks:
- web
healthcheck:
test: ["CMD", "traefik", "healthcheck", "--ping"]
interval: 15s
timeout: 5s
retries: 3
# --- optional secured dashboard at https://traefik.${APP_DOMAIN} ---
# Generate a hash: `echo $(htpasswd -nB admin) | sed -e s/\$/\$\$/g`
# labels:
# - traefik.enable=true
# - traefik.http.routers.dashboard.rule=Host(`traefik.${APP_DOMAIN:-maarkn.dev}`)
# - traefik.http.routers.dashboard.entrypoints=websecure
# - traefik.http.routers.dashboard.tls.certresolver=le
# - traefik.http.routers.dashboard.service=api@internal
# - traefik.http.routers.dashboard.middlewares=dashboard-auth
# - traefik.http.middlewares.dashboard-auth.basicauth.users=${TRAEFIK_DASHBOARD_AUTH}
postgres:
image: pgvector/pgvector:pg17
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-maarkn}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-maarkn}
volumes:
- maarkn-pg-data:/var/lib/postgresql/data
networks:
# internal only — never reachable from Traefik / the public entrypoints
- internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-maarkn} -d ${POSTGRES_DB:-maarkn}"]
interval: 5s
timeout: 3s
retries: 5
app:
build:
context: .
dockerfile: Dockerfile
args:
# Build-time flag (inlined into the client bundle): forward unknown
# terminal input with 3+ words to the AI assistant. Off by default.
NEXT_PUBLIC_TERMINAL_ASK_FALLBACK: ${NEXT_PUBLIC_TERMINAL_ASK_FALLBACK:-false}
restart: unless-stopped
env_file: .env
volumes:
- maarkn-uploads:/data/uploads
depends_on:
postgres:
condition: service_healthy
networks:
- web # reachable by Traefik
- internal # reaches Postgres
labels:
- traefik.enable=true
- traefik.docker.network=web
- traefik.http.routers.maarkn.rule=Host(`${APP_DOMAIN:-maarkn.dev}`)
- traefik.http.routers.maarkn.entrypoints=websecure
- traefik.http.routers.maarkn.tls.certresolver=le
- traefik.http.services.maarkn.loadbalancer.server.port=5050
expose:
- "5050"
# One-off tasks (migrate / seed / ingest). Built from the `builder` stage
# because it has the full toolchain (Prisma CLI v6, tsx) that the slim runtime
# image deliberately lacks. Not started by `up` (manual "tools" profile).
# migrate: docker compose -f docker-compose.prod.yml run --rm migrate
# seed: docker compose -f docker-compose.prod.yml run --rm --entrypoint sh migrate -c "npm run db:seed"
# ingest: docker compose -f docker-compose.prod.yml run --rm --entrypoint sh migrate -c "npm run db:ingest"
migrate:
build:
context: .
dockerfile: Dockerfile
target: builder
profiles: ["tools"]
env_file: .env
depends_on:
postgres:
condition: service_healthy
networks:
- web # seed/ingest reach external APIs (OpenAI embeddings) via here
- internal # reaches Postgres
command: ["node_modules/.bin/prisma", "migrate", "deploy"]
networks:
# public-facing: Traefik <-> app
web:
name: web
# private backend: app <-> postgres (no external exposure)
internal:
name: internal
internal: true
volumes:
maarkn-pg-data:
traefik-letsencrypt:
maarkn-uploads: