name: ghost-dev services: mysql: image: mysql:8.4.11@sha256:0f51e63e537a1f176423fa03ef559aabe6fa4372a53160a44bc7e75da7d15364 container_name: ghost-dev-mysql # Shared by every checkout's Ghost and DB test runs, which outgrow MySQL's default 151 connections command: --innodb-buffer-pool-size=1G --innodb-log-buffer-size=500M --innodb-change-buffer-max-size=50 --innodb-flush-log-at-trx_commit=0 --innodb-flush-method=O_DIRECT --log-error-verbosity=1 --max-connections=1000 ports: - '3306:3306' environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root} MYSQL_DATABASE: ${MYSQL_DATABASE:-ghost_dev} MYSQL_USER: ghost MYSQL_PASSWORD: ghost volumes: - mysql-data:/var/lib/mysql healthcheck: test: [ 'CMD', 'mysql', '-h', '127.0.0.1', '-uroot', '-p${MYSQL_ROOT_PASSWORD:-root}', '-e', 'SELECT 1', ] interval: 1s retries: 120 timeout: 5s start_period: 10s start_interval: 1s redis: image: redis:7.4@sha256:2e3eb11462c4e3759e6972678dd051d150ff54b1e60714b3c33b701260172a90 container_name: ghost-dev-redis command: ['redis-server', '--loglevel', 'warning'] ports: - '6379:6379' volumes: - redis-data:/data healthcheck: test: - CMD - redis-cli - --raw - incr - ping interval: 1s retries: 120 mailpit: image: axllent/mailpit@sha256:9b3a463be15354c0f0eba91697d80e3e0d8a5623ef0e627cacc836bc17fd5231 container_name: ghost-dev-mailpit # Tags each message with the SMTP username, which `pnpm dev` sets to the checkout's name command: ['--quiet', '--smtp-auth-accept-any', '--smtp-auth-allow-insecure', '--tags-username'] ports: - '1025:1025' # SMTP server - '8025:8025' # Web interface - '8026:8025' # Web interface (for e2e tests) healthcheck: interval: 1s retries: 30 # Main development Ghost instance # Additional instances can be created programmatically via E2E GhostManager # `pnpm dev` runs Ghost and the dev servers on the host; these two services back # `pnpm dev:docker` and the modes built on it, and e2e dev mode. ghost-dev: profiles: ['docker-dev'] build: context: ./ dockerfile: docker/ghost-dev/Dockerfile container_name: ghost-dev working_dir: /home/ghost/ghost command: ['pnpm', 'dev'] # The entrypoint drains Ghost before exiting; give that longer than Docker's 10s stop_grace_period: 45s volumes: # Mount the backend source as whole-directory mounts instead of # enumerating each server-graph workspace package. `pnpm dev` runs in # ghost, and pnpm's verify-deps-before-run only checks ghost's # own dependency closure — which is fully installed in the root # /home/ghost/node_modules baked into the image and never mounted over — # so the non-server packages these dirs also expose (e.g. koenig-lexical) # fall outside that closure and don't trigger a # workspace repair. Because root node_modules is never mounted, # linux-built native modules survive; each package's local node_modules # holds only relative symlinks that re-resolve into the root store. # verify-deps-before-run=false is set below as belt-and-suspenders in # case a future pnpm widens that check back to the whole workspace. - ./ghost:/home/ghost/ghost - ./koenig:/home/ghost/koenig - ./configs:/home/ghost/configs - ./packages:/home/ghost/packages # Mount specific content subdirectories to preserve themes/adapters from source - ghost-dev-data:/home/ghost/ghost/content/data - ghost-dev-images:/home/ghost/ghost/content/images - ghost-dev-media:/home/ghost/ghost/content/media - ghost-dev-files:/home/ghost/ghost/content/files - ghost-dev-logs:/home/ghost/ghost/content/logs - shared-config:/mnt/shared-config:ro environment: NODE_ENV: development NODE_TLS_REJECT_UNAUTHORIZED: '0' DEBUG: ${DEBUG:-} pnpm_config_verify_deps_before_run: 'false' database__client: mysql2 database__connection__host: mysql database__connection__user: root database__connection__password: ${MYSQL_ROOT_PASSWORD:-root} database__connection__database: ${MYSQL_DATABASE:-ghost_dev} server__host: 0.0.0.0 server__port: 2368 # Apply Ghost(Pro)'s public query parameter allowlist in local development queryParameterFiltering__enabled: '${QUERY_PARAMETER_FILTERING_ENABLED:-true}' mail__transport: SMTP mail__options__host: mailpit mail__options__port: 1025 # Redis cache (optional) adapters__cache__Redis__host: redis adapters__cache__Redis__port: 6379 # Apps under development run on the host machine, where `localhost` is not this # container: Ghost fetches their manifests through this alias instead apps__localhostAlias: host.docker.internal # Docker Desktop and OrbStack provide host.docker.internal and forward it to the host's # loopback. On Linux it needs this, and it reaches the docker bridge rather than # loopback: an app that only listens on 127.0.0.1 (Vite's default) is not reachable # through it, so bind the app to 0.0.0.0 while developing against this container. extra_hosts: - 'host.docker.internal:host-gateway' depends_on: mysql: condition: service_healthy redis: condition: service_healthy mailpit: condition: service_healthy stripe: condition: service_healthy required: false healthcheck: test: [ 'CMD', 'node', '-e', "fetch('http://localhost:2368',{redirect:'manual'}).then(r=>process.exit(r.status<500?0:1)).catch(()=>process.exit(1))", ] timeout: 5s retries: 10 start_period: 5s # Caddy reverse proxy for the main dev instance # Routes Ghost backend + proxies asset requests to host dev servers ghost-dev-gateway: profiles: ['docker-dev'] build: context: ./docker/dev-gateway dockerfile: Dockerfile container_name: ghost-dev-gateway environment: ANALYTICS_PROXY_TARGET: ${ANALYTICS_PROXY_TARGET:-analytics:3000} ports: - '2368:80' - '80:80' extra_hosts: - 'host.docker.internal:host-gateway' volumes: # Mount Caddyfile for live config changes without rebuilding - ./docker/dev-gateway/Caddyfile:/etc/caddy/Caddyfile:ro # All workspace apps mounted read-only. Caddy file_server resolves # /srv/apps//umd dynamically for the public-app routes (see # the Caddyfile @public_app block). Replaces the per-app `vite preview` # sirv layer + avoids per-app bind mounts. - ./apps:/srv/apps:ro depends_on: ghost-dev: condition: service_healthy stripe: image: stripe/stripe-cli:latest@sha256:c8e5a48108fb4c1e83583912c85810e9ac3bc8db91070da3f915ac369ceecf31 container_name: ghost-dev-stripe entrypoint: ['/entrypoint.sh'] profiles: ['stripe'] volumes: - ./docker/stripe/entrypoint.sh:/entrypoint.sh:ro - shared-config:/mnt/shared-config environment: - GHOST_URL=${GHOST_URL:-http://ghost-dev:2368} - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY:-} # `pnpm dev:stripe --listen` forwards to the Admin dev server on the host extra_hosts: - 'host.docker.internal:host-gateway' healthcheck: test: ['CMD', 'test', '-f', '/mnt/shared-config/.env.stripe'] interval: 1s retries: 120 volumes: mysql-data: redis-data: shared-config: ghost-dev-data: ghost-dev-images: ghost-dev-media: ghost-dev-files: ghost-dev-logs: networks: default: name: ghost_dev