Files
sharper.ly/docker-compose.yml
2026-09-11 02:46:56 +02:00

141 lines
6.2 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.
- "${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
# 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}
# 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:
- "${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}
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:
- "${WEB_PORT:-3000}:3000"
volumes:
postgres-data:
cms-uploads: