# ============================================================================ # 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: