A community based topic aggregation platform built on atproto
1# Coves Production Stack
2#
3# Architecture:
4# - coves.social: AppView domain (API, frontend, .well-known/did.json)
5# - pds.coves.me: PDS domain (canonical hostname for relay registration)
6# - coves.me: PDS domain (legacy, kept for compatibility)
7#
8# Hardware: AMD Epyc 7351p (16c/32t), 256GB RAM, 2x500GB NVMe RAID
9#
10# Usage:
11# docker-compose -f docker-compose.prod.yml up -d
12#
13# Prerequisites:
14# 1. DNS configured for both domains
15# 2. SSL certificates (Caddy handles this automatically)
16# 3. .env.prod file with secrets
17# 4. .well-known/did.json deployed to coves.social
18
19services:
20 # PostgreSQL Database for AppView
21 postgres:
22 image: postgres:15
23 container_name: coves-prod-postgres
24 restart: unless-stopped
25 environment:
26 POSTGRES_DB: ${POSTGRES_DB}
27 POSTGRES_USER: ${POSTGRES_USER}
28 POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
29 volumes:
30 - postgres-data:/var/lib/postgresql/data
31 # Mount backup directory for pg_dump
32 - ./backups:/backups
33 networks:
34 - coves-internal
35 healthcheck:
36 test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
37 interval: 10s
38 timeout: 5s
39 retries: 5
40 # Generous limits for 256GB server
41 deploy:
42 resources:
43 limits:
44 memory: 32G
45 reservations:
46 memory: 4G
47
48 # Coves AppView (Go Server)
49 appview:
50 build:
51 context: .
52 dockerfile: Dockerfile
53 image: coves/appview:${VERSION:-latest}
54 container_name: coves-prod-appview
55 restart: unless-stopped
56 ports:
57 - "127.0.0.1:8080:8080" # Only expose to localhost (Caddy proxies)
58 environment:
59 # Database
60 DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable
61
62 # Instance identity
63 INSTANCE_DID: did:web:coves.social
64 INSTANCE_DOMAIN: coves.social
65 APPVIEW_PUBLIC_URL: https://coves.social
66
67 # PDS connection (separate domain!)
68 PDS_URL: https://coves.me
69
70 # Jetstream (Bluesky production firehose)
71 JETSTREAM_URL: wss://jetstream2.us-east.bsky.network/subscribe
72
73 # Custom lexicon consumers (use production Jetstream with collection filters)
74 COMMUNITY_JETSTREAM_URL: wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=social.coves.community.profile&wantedCollections=social.coves.community.subscription
75 POST_JETSTREAM_URL: wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=social.coves.community.post
76 AGGREGATOR_JETSTREAM_URL: wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=social.coves.aggregator.service&wantedCollections=social.coves.aggregator.authorization
77 VOTE_JETSTREAM_URL: wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=social.coves.feed.vote
78 COMMENT_JETSTREAM_URL: wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=social.coves.community.comment
79
80 # Security - MUST be false in production
81 AUTH_SKIP_VERIFY: "false"
82 SKIP_DID_WEB_VERIFICATION: "false"
83
84 # OAuth (for community account provisioning)
85 OAUTH_CLIENT_ID: ${OAUTH_CLIENT_ID}
86 OAUTH_REDIRECT_URI: ${OAUTH_REDIRECT_URI}
87 OAUTH_SEAL_SECRET: ${OAUTH_SEAL_SECRET}
88
89 # OAuth Confidential Client (optional - enables 1-year sessions instead of 14-day)
90 # Generate keys with: go run ./cmd/tools/generate-oauth-key
91 OAUTH_CLIENT_PRIVATE_KEY: ${OAUTH_CLIENT_PRIVATE_KEY:-}
92 OAUTH_CLIENT_KEY_ID: ${OAUTH_CLIENT_KEY_ID:-}
93
94 # Application settings
95 PORT: 8080
96 ENV: production
97 LOG_LEVEL: info
98
99 # Encryption key for community credentials
100 ENCRYPTION_KEY: ${ENCRYPTION_KEY}
101
102 # Cursor encryption for pagination
103 CURSOR_SECRET: ${CURSOR_SECRET}
104
105 # PDS JWT secret for verifying HS256 tokens from the PDS
106 # Must match the PDS_JWT_SECRET configured on the PDS
107 PDS_JWT_SECRET: ${PDS_JWT_SECRET}
108 # Whitelist PDS issuer(s) allowed to use HS256 (no kid)
109 HS256_ISSUERS: ${HS256_ISSUERS}
110
111 # Restrict community creation to instance DID only
112 COMMUNITY_CREATORS: did:web:coves.social
113
114 # Trusted aggregators (bypass per-community authorization check)
115 # Comma-separated list of DIDs
116 TRUSTED_AGGREGATOR_DIDS: ${TRUSTED_AGGREGATOR_DIDS:-}
117
118 # Image proxy configuration (on-the-fly resizing with disk cache)
119 IMAGE_PROXY_ENABLED: ${IMAGE_PROXY_ENABLED:-true}
120 IMAGE_PROXY_BASE_URL: ${IMAGE_PROXY_BASE_URL:-https://coves.social}
121 IMAGE_PROXY_CACHE_PATH: /var/cache/coves/images
122 IMAGE_PROXY_CACHE_MAX_GB: ${IMAGE_PROXY_CACHE_MAX_GB:-10}
123 IMAGE_PROXY_FETCH_TIMEOUT_SECONDS: ${IMAGE_PROXY_FETCH_TIMEOUT_SECONDS:-30}
124 IMAGE_PROXY_MAX_SOURCE_SIZE_MB: ${IMAGE_PROXY_MAX_SOURCE_SIZE_MB:-10}
125
126 # OpenTelemetry Observability (optional - disabled by default)
127 OTEL_ENABLED: ${OTEL_ENABLED:-false}
128 OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-}
129 OTEL_EXPORTER_OTLP_HEADERS: ${OTEL_EXPORTER_OTLP_HEADERS:-}
130 OTEL_SERVICE_NAME: ${OTEL_SERVICE_NAME:-coves-appview}
131 OTEL_TRACES_SAMPLER_ARG: ${OTEL_TRACES_SAMPLER_ARG:-1.0}
132 OTEL_EXPORTER_OTLP_INSECURE: ${OTEL_EXPORTER_OTLP_INSECURE:-false}
133 volumes:
134 # Image proxy cache (persistent across container restarts)
135 - imageproxy-cache:/var/cache/coves/images
136 networks:
137 - coves-internal
138 depends_on:
139 postgres:
140 condition: service_healthy
141 healthcheck:
142 test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/xrpc/_health"]
143 interval: 30s
144 timeout: 5s
145 retries: 3
146 start_period: 10s
147 # Go is memory-efficient, but give it room for connection pools
148 deploy:
149 resources:
150 limits:
151 memory: 8G
152 reservations:
153 memory: 512M
154
155 # Bluesky PDS (Personal Data Server)
156 # Handles community accounts and their repositories
157 pds:
158 image: ghcr.io/bluesky-social/pds:latest
159 container_name: coves-prod-pds
160 restart: unless-stopped
161 ports:
162 - "127.0.0.1:3000:3000" # Only expose to localhost (Caddy proxies)
163 environment:
164 # PDS identity (use pds.coves.me for fresh relay registration)
165 PDS_HOSTNAME: pds.coves.me
166 PDS_PORT: 3000
167 PDS_DATA_DIRECTORY: /pds
168 PDS_BLOB_UPLOAD_LIMIT: 104857600 # 100 MB
169
170 # S3-compatible blob storage
171 PDS_BLOBSTORE_S3_BUCKET: ${PDS_S3_BUCKET}
172 PDS_BLOBSTORE_S3_REGION: ${PDS_S3_REGION}
173 PDS_BLOBSTORE_S3_ENDPOINT: ${PDS_S3_ENDPOINT}
174 PDS_BLOBSTORE_S3_ACCESS_KEY_ID: ${PDS_S3_ACCESS_KEY_ID}
175 PDS_BLOBSTORE_S3_SECRET_ACCESS_KEY: ${PDS_S3_SECRET_ACCESS_KEY}
176 PDS_BLOBSTORE_S3_FORCE_PATH_STYLE: "true"
177
178 # PLC Directory (production)
179 PDS_DID_PLC_URL: https://plc.directory
180
181 # Handle domains
182 # Community handles use @community.coves.social (AppView domain)
183 # Note: Root domain (coves.social) handle works via .well-known resolution
184 PDS_SERVICE_HANDLE_DOMAINS: .coves.social
185
186 # Security (set real values in .env.prod)
187 PDS_JWT_SECRET: ${PDS_JWT_SECRET}
188 PDS_ADMIN_PASSWORD: ${PDS_ADMIN_PASSWORD}
189 PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX: ${PDS_ROTATION_KEY}
190
191 # Email (optional, for account recovery)
192 # NOTE: Must set BOTH or NEITHER - PDS fails with partial config
193 # PDS_EMAIL_SMTP_URL: ${PDS_EMAIL_SMTP_URL}
194 # PDS_EMAIL_FROM_ADDRESS: ${PDS_EMAIL_FROM_ADDRESS}
195
196 # Production mode
197 PDS_DEV_MODE: "false"
198 PDS_INVITE_REQUIRED: "true"
199
200 # Logging
201 NODE_ENV: production
202 LOG_ENABLED: "true"
203 LOG_LEVEL: info
204
205 # AppView proxy (for app.bsky.* methods like getProfile, notifications, etc.)
206 PDS_BSKY_APP_VIEW_URL: https://api.bsky.app
207 PDS_BSKY_APP_VIEW_DID: did:web:api.bsky.app
208
209 # Report service (for reporting content)
210 PDS_REPORT_SERVICE_URL: https://mod.bsky.app
211 PDS_REPORT_SERVICE_DID: did:plc:ar7c4by46qjdydhdevvrndac
212
213 # Relay crawlers (for federation with Bluesky network)
214 PDS_CRAWLERS: https://bsky.network,https://relay1.us-east.bsky.network,https://relay1.us-west.bsky.network,https://relay.fire.hose.cam,https://relay.upcloud.world
215 volumes:
216 - pds-data:/pds
217 networks:
218 - coves-internal
219 healthcheck:
220 test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/xrpc/_health"]
221 interval: 30s
222 timeout: 5s
223 retries: 5
224 # PDS (Node.js) needs memory for blob handling
225 deploy:
226 resources:
227 limits:
228 memory: 16G
229 reservations:
230 memory: 1G
231
232 # Caddy Reverse Proxy
233 # Handles HTTPS automatically via Let's Encrypt
234 # Uses Cloudflare plugin for wildcard SSL certificates (*.coves.social)
235 caddy:
236 # Pre-built Caddy with Cloudflare DNS plugin
237 # Updates automatically with docker-compose pull
238 # Alternative: build your own with Dockerfile.caddy
239 image: ghcr.io/slothcroissant/caddy-cloudflaredns:latest
240 container_name: coves-prod-caddy
241 restart: unless-stopped
242 ports:
243 - "80:80"
244 - "443:443"
245 environment:
246 # Required for wildcard SSL via DNS challenge
247 # Create at: Cloudflare Dashboard → My Profile → API Tokens → Create Token
248 # Permissions: Zone:DNS:Edit for coves.social zone
249 CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN}
250 volumes:
251 - ./Caddyfile:/etc/caddy/Caddyfile:ro
252 - caddy-data:/data
253 - caddy-config:/config
254 # Static files (.well-known, client-metadata.json, oauth callback)
255 - ./static:/srv:ro
256 networks:
257 - coves-internal
258 depends_on:
259 - appview
260 - pds
261
262networks:
263 coves-internal:
264 driver: bridge
265 name: coves-prod-network
266
267volumes:
268 postgres-data:
269 name: coves-prod-postgres-data
270 pds-data:
271 name: coves-prod-pds-data
272 caddy-data:
273 name: coves-prod-caddy-data
274 caddy-config:
275 name: coves-prod-caddy-config
276 imageproxy-cache:
277 name: coves-prod-imageproxy-cache