Add Docker Compose stack with PostgreSQL and Caddy
Production traffic enters through Caddy, which terminates TLS for the public domain and the CMS subdomain and sets HSTS and the other security headers. PostgreSQL, Strapi and Nuxt publish no ports of their own. docker-compose.dev.yml publishes the ports on localhost and drops Caddy for local testing. It is a separate file rather than an override.yml so it can only be applied when passed explicitly, never by accident in production.
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
# Copy to .env and replace every placeholder. Never commit .env.
|
||||
|
||||
# --- Domains (Caddy) ---
|
||||
PUBLIC_DOMAIN=blog.localhost
|
||||
CMS_DOMAIN=cms.blog.localhost
|
||||
ACME_EMAIL=admin@example.com
|
||||
|
||||
# --- Database ---
|
||||
POSTGRES_DB=blog
|
||||
POSTGRES_USER=blog
|
||||
POSTGRES_PASSWORD=change-me
|
||||
|
||||
# --- Strapi ---
|
||||
# Generate each secret with: openssl rand -base64 32
|
||||
APP_KEYS=change-me-1,change-me-2
|
||||
API_TOKEN_SALT=change-me
|
||||
ADMIN_JWT_SECRET=change-me
|
||||
TRANSFER_TOKEN_SALT=change-me
|
||||
JWT_SECRET=change-me
|
||||
ENCRYPTION_KEY=change-me
|
||||
|
||||
# --- Frontend ---
|
||||
# Server-side only: internal Docker address of Strapi.
|
||||
STRAPI_URL=http://cms:1337
|
||||
# Public base URL of the website, used for canonical URLs and Open Graph.
|
||||
PUBLIC_SITE_URL=https://blog.localhost
|
||||
# Public base URL of Strapi, used to build absolute media URLs in the browser.
|
||||
PUBLIC_STRAPI_URL=https://cms.blog.localhost
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
email {$ACME_EMAIL}
|
||||
}
|
||||
|
||||
(security_headers) {
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
-Server
|
||||
}
|
||||
}
|
||||
|
||||
{$PUBLIC_DOMAIN} {
|
||||
import security_headers
|
||||
encode zstd gzip
|
||||
reverse_proxy frontend:3000
|
||||
}
|
||||
|
||||
{$CMS_DOMAIN} {
|
||||
import security_headers
|
||||
encode zstd gzip
|
||||
# Uploaded media can be large; Strapi's own limit still applies.
|
||||
request_body {
|
||||
max_size 100MB
|
||||
}
|
||||
reverse_proxy cms:1337
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# Local testing without domains or TLS: publishes the app ports on localhost and
|
||||
# leaves Caddy out. Never used in production — it must be passed explicitly:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build cms frontend
|
||||
services:
|
||||
database:
|
||||
ports:
|
||||
- "127.0.0.1:5432:5432"
|
||||
|
||||
cms:
|
||||
ports:
|
||||
- "127.0.0.1:1337:1337"
|
||||
environment:
|
||||
STRAPI_URL: http://localhost:1337
|
||||
|
||||
frontend:
|
||||
ports:
|
||||
- "127.0.0.1:3000:3000"
|
||||
environment:
|
||||
NUXT_PUBLIC_SITE_URL: http://localhost:3000
|
||||
NUXT_PUBLIC_STRAPI_URL: http://localhost:1337
|
||||
@@ -0,0 +1,78 @@
|
||||
services:
|
||||
database:
|
||||
image: postgres:17-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
cms:
|
||||
build: ./cms
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
database:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
HOST: 0.0.0.0
|
||||
PORT: 1337
|
||||
DATABASE_CLIENT: postgres
|
||||
DATABASE_HOST: database
|
||||
DATABASE_PORT: 5432
|
||||
DATABASE_NAME: ${POSTGRES_DB}
|
||||
DATABASE_USERNAME: ${POSTGRES_USER}
|
||||
DATABASE_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
APP_KEYS: ${APP_KEYS}
|
||||
API_TOKEN_SALT: ${API_TOKEN_SALT}
|
||||
ADMIN_JWT_SECRET: ${ADMIN_JWT_SECRET}
|
||||
TRANSFER_TOKEN_SALT: ${TRANSFER_TOKEN_SALT}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
|
||||
STRAPI_URL: https://${CMS_DOMAIN}
|
||||
volumes:
|
||||
- cms-uploads:/app/public/uploads
|
||||
|
||||
frontend:
|
||||
build: ./frontend
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- cms
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
HOST: 0.0.0.0
|
||||
PORT: 3000
|
||||
NUXT_STRAPI_URL: ${STRAPI_URL}
|
||||
NUXT_PUBLIC_SITE_URL: ${PUBLIC_SITE_URL}
|
||||
NUXT_PUBLIC_STRAPI_URL: ${PUBLIC_STRAPI_URL}
|
||||
|
||||
caddy:
|
||||
image: caddy:2-alpine
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- frontend
|
||||
- cms
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
environment:
|
||||
PUBLIC_DOMAIN: ${PUBLIC_DOMAIN}
|
||||
CMS_DOMAIN: ${CMS_DOMAIN}
|
||||
ACME_EMAIL: ${ACME_EMAIL}
|
||||
volumes:
|
||||
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy-data:/data
|
||||
- caddy-config:/config
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
cms-uploads:
|
||||
caddy-data:
|
||||
caddy-config:
|
||||
Reference in New Issue
Block a user