Files
sharper.ly/docker-compose.yml
T
IllusionandClaude Opus 5.5 709a978da5 feat(web): add Cloudflare Turnstile to the public forms
Render the widget on the contact, event registration and both application
forms, and verify the token with Siteverify in each action before anything
is written to the CMS. Verification fails closed. The check is off unless
both TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY are set.

Allow challenges.cloudflare.com in script-src and frame-src.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 12:05:51 +02:00

146 lines
6.5 KiB
YAML

# ============================================================================
# SHARP-ER - full stack for local development, review and small deployments
# ----------------------------------------------------------------------------
# cp .env.example .env
# ./scripts/generate-secrets.sh --write .env
# docker compose up -d --build
#
# web http://localhost:3000 (redirects / to /en or /ar)
# cms http://localhost:1337/admin (create the first admin user here)
# db 127.0.0.1:5433 (5433 on purpose - see .env.example)
#
# Every ${VARIABLE} below is read from the .env file next to this one. The
# secrets use ${VAR:?...}, so a missing value stops the stack immediately with
# a readable message instead of a Strapi crash loop.
#
# Full walkthrough, including teardown and the CMS media caveat: README.md.
# ============================================================================
name: sharper
services:
# --------------------------------------------------------------------------
# PostgreSQL 16 - the CMS database. Data lives in a named volume, so
# `docker compose down` keeps it and only `down -v` deletes it.
# --------------------------------------------------------------------------
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${DATABASE_NAME:-sharper_cms}
POSTGRES_USER: ${DATABASE_USERNAME:-postgres}
POSTGRES_PASSWORD: ${DATABASE_PASSWORD:?set DATABASE_PASSWORD in .env}
# Matches the manual path (createdb --encoding UTF8). Arabic content is
# stored as UTF-8 either way; being explicit keeps the two paths identical.
POSTGRES_INITDB_ARGS: "--encoding=UTF8"
volumes:
- postgres-data:/var/lib/postgresql/data
ports:
# Published on 5433 by default so it cannot collide with a PostgreSQL
# already running on the host's 5432.
- "127.0.0.1:${POSTGRES_PUBLISHED_PORT:-5433}:5432"
healthcheck:
# pg_isready also fails while the entrypoint's temporary init server is
# up, so `service_healthy` really does mean "ready for connections".
test:
["CMD-SHELL", "pg_isready -U ${DATABASE_USERNAME:-postgres} -d ${DATABASE_NAME:-sharper_cms} -h 127.0.0.1"]
interval: 5s
timeout: 5s
retries: 12
start_period: 15s
# --------------------------------------------------------------------------
# Strapi 5 - REST API and admin panel. Waits for a database that is actually
# accepting connections, then runs its own migrations and the boot-time
# reconciliation in apps/cms/src/bootstrap (locales + public permissions).
# --------------------------------------------------------------------------
cms:
build:
context: .
dockerfile: apps/cms/Dockerfile
image: sharper-cms:local
restart: unless-stopped
# The healthcheck this service is judged by (GET /_health) is declared in
# apps/cms/Dockerfile, so `docker run` gets it too.
depends_on:
postgres:
condition: service_healthy
environment:
NODE_ENV: production
HOST: 0.0.0.0
PORT: 1337
IS_PROXIED: ${IS_PROXIED:-false}
# Absolute URL the admin panel and generated links use. Change it
# together with CMS_PORT, and set it to the public origin in production.
PUBLIC_URL: ${CMS_PUBLIC_URL:-http://localhost:1337}
LOG_LEVEL: ${LOG_LEVEL:-info}
PARTNER_REVIEW_NOTIFICATION_EMAILS: ${PARTNER_REVIEW_NOTIFICATION_EMAILS:-}
# Inside the compose network the database is reachable by service name on
# its own port, never through the published one.
DATABASE_CLIENT: postgres
DATABASE_HOST: postgres
DATABASE_PORT: 5432
DATABASE_NAME: ${DATABASE_NAME:-sharper_cms}
DATABASE_USERNAME: ${DATABASE_USERNAME:-postgres}
DATABASE_PASSWORD: ${DATABASE_PASSWORD:?set DATABASE_PASSWORD in .env}
DATABASE_SSL: "false"
# Generate all six with: ./scripts/generate-secrets.sh --write .env
APP_KEYS: ${APP_KEYS:?run ./scripts/generate-secrets.sh --write .env}
API_TOKEN_SALT: ${API_TOKEN_SALT:?run ./scripts/generate-secrets.sh --write .env}
ADMIN_JWT_SECRET: ${ADMIN_JWT_SECRET:?run ./scripts/generate-secrets.sh --write .env}
JWT_SECRET: ${JWT_SECRET:?run ./scripts/generate-secrets.sh --write .env}
TRANSFER_TOKEN_SALT: ${TRANSFER_TOKEN_SALT:?run ./scripts/generate-secrets.sh --write .env}
ENCRYPTION_KEY: ${ENCRYPTION_KEY:?run ./scripts/generate-secrets.sh --write .env}
# Browser origins allowed to call the API directly.
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000}
volumes:
# Media Library files. Like the database, they survive `down` and are
# only removed by `down -v`.
- cms-uploads:/repo/apps/cms/public/uploads
ports:
- "127.0.0.1:${CMS_PORT:-1337}:1337"
# --------------------------------------------------------------------------
# React Router 8 server-side renderer. Waits for a CMS that answers /_health,
# so the first page render already has content. (Pages do fall back to their
# bundled draft copy if the CMS goes away later - the site never 500s because
# Strapi is down.)
# --------------------------------------------------------------------------
web:
build:
context: .
dockerfile: apps/web/Dockerfile
image: sharper-web:local
restart: unless-stopped
# Healthcheck (GET /en) is declared in apps/web/Dockerfile.
depends_on:
cms:
condition: service_healthy
environment:
NODE_ENV: production
PORT: 3000
# Server-side fetches go to the service name on the compose network.
# This origin is also prefixed onto uploaded media URLs that reach the
# browser - see "Images from the CMS" in README.md.
STRAPI_URL: ${DOCKER_STRAPI_URL:-http://cms:1337}
STRAPI_TOKEN: ${STRAPI_TOKEN:-}
# Canonical URLs, hreflang alternates and JSON-LD are built from this.
SITE_URL: ${SITE_URL:-http://localhost:3000}
# Cloudflare Turnstile on the public forms - off unless both are set.
TURNSTILE_SITE_KEY: ${TURNSTILE_SITE_KEY:-}
TURNSTILE_SECRET_KEY: ${TURNSTILE_SECRET_KEY:-}
volumes:
# Panton and the Arabic faces are licensed and deliberately excluded from
# the image (.dockerignore). Mounting the host folder read-only lets a
# local container use them without ever baking them into an artefact.
- ./apps/web/public/fonts:/repo/apps/web/build/client/fonts:ro
ports:
- "127.0.0.1:${WEB_PORT:-3000}:3000"
volumes:
postgres-data:
cms-uploads: