diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2257648 --- /dev/null +++ b/.env.example @@ -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 diff --git a/caddy/Caddyfile b/caddy/Caddyfile new file mode 100644 index 0000000..a9ff3a4 --- /dev/null +++ b/caddy/Caddyfile @@ -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 +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..53b5b03 --- /dev/null +++ b/docker-compose.dev.yml @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..cb680b4 --- /dev/null +++ b/docker-compose.yml @@ -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: