# Phorge + Gorge 默认协作栈 # # 新安装直接运行: # cp .env.example .env # docker compose up -d --build # # 默认启动 Gorge 的无外部依赖核心服务。邮件、搜索与 Gitea bridge 需要额外后端 # 或密钥,分别通过 mailer、search、gitea profile 启用。数据库基础设施独立放在 # docker-compose.mysql.yml,默认栈不再依赖完整的 legacy Phorge 编排。 x-phorge-runtime: &phorge-runtime image: ${PHORGE_IMAGE_REPOSITORY:-phorge}:${PHORGE_IMAGE_TAG:-local} build: context: . dockerfile: Dockerfile volumes: - phorge-conf:/opt/phorge/phorge/conf/local - phorge-repo:/var/repo x-phorge-environment: &phorge-environment MYSQL_HOST: mysql MYSQL_PORT: "3306" MYSQL_USER: ${MYSQL_USER:-phorge} MYSQL_PASS: ${MYSQL_PASSWORD:-phorge} PHORGE_BASE_URI: ${PHORGE_BASE_URI:-http://127.0.0.1:${PHORGE_HTTP_PORT:-8088}/} PHORGE_TIMEZONE: ${PHORGE_TIMEZONE:-UTC} PHORGE_WAIT_DB: ${PHORGE_WAIT_DB:-1} PHORGE_DB_NAMESPACE: ${PHORGE_DB_NAMESPACE:-phabricator} PHORGE_GORGE_POLICY: ${PHORGE_GORGE_POLICY:-required} # 服务端点、profile 与消费者所有权一次写入 deployment.json;Web 和 daemon # 将它作为高于数据库配置的只读来源加载。 PHORGE_CONTROL_PLANE: deployment PHORGE_GORGE_STARTUP_TIMEOUT: ${PHORGE_GORGE_STARTUP_TIMEOUT:-60} GORGE_WORKER_URI: ${GORGE_WORKER_URI:-http://gorge-worker:8170} GORGE_WORKER_TOKEN: ${GORGE_WORKER_TOKEN:-${GORGE_TASKQUEUE_TOKEN:-}} GORGE_MAIL_DELIVERY_MODE: ${GORGE_MAIL_DELIVERY_MODE:-legacy} services: mysql: extends: file: docker-compose.mysql.yml service: mysql image: mysql:${MYSQL_IMAGE_TAG:-8.0.46} db-init: extends: file: docker-compose.mysql.yml service: db-init image: mysql:${MYSQL_IMAGE_TAG:-8.0.46} gorge-render: extends: file: docker-compose.gorge.yml service: gorge-render image: ghcr.io/soulteary/gorge:render-${GORGE_RENDER_IMAGE_TAG:-${GORGE_IMAGE_TAG:-2026.10.07-r2}} # Native PHP/GNU diff has been retired; the bundled render service owns # unified and prose diff generation as well as syntax highlighting. The # diff routes are pinned on in docker-compose.gorge.yml and can no longer # be turned off from .env, because there is nothing left to fall back to. gorge-conduit: extends: file: docker-compose.gorge.yml service: gorge-conduit image: ghcr.io/soulteary/gorge:conduit-${GORGE_CONDUIT_IMAGE_TAG:-${GORGE_IMAGE_TAG:-2026.10.07-r2}} gorge-notification: extends: file: docker-compose.gorge.yml service: gorge-notification image: ghcr.io/soulteary/gorge:notification-${GORGE_NOTIFICATION_IMAGE_TAG:-${GORGE_IMAGE_TAG:-2026.10.07-r2}} gorge-file-storage: extends: file: docker-compose.gorge.yml service: gorge-file-storage image: ghcr.io/soulteary/gorge:file-storage-${GORGE_FILE_IMAGE_TAG:-${GORGE_IMAGE_TAG:-2026.10.07-r2}} environment: GORGE_FILE_NAMESPACE: ${PHORGE_DB_NAMESPACE:-phabricator} gorge-webhook: extends: file: docker-compose.gorge.yml service: gorge-webhook image: ghcr.io/soulteary/gorge:webhook-${GORGE_WEBHOOK_IMAGE_TAG:-${GORGE_IMAGE_TAG:-2026.10.07-r2}} environment: GORGE_WEBHOOK_NAMESPACE: ${PHORGE_DB_NAMESPACE:-phabricator} gorge-taskqueue: extends: file: docker-compose.gorge.yml service: gorge-taskqueue image: ghcr.io/soulteary/gorge:taskqueue-${GORGE_TASKQUEUE_IMAGE_TAG:-${GORGE_IMAGE_TAG:-2026.10.07-r2}} environment: GORGE_TASKQUEUE_NAMESPACE: ${PHORGE_DB_NAMESPACE:-phabricator} gorge-maintenance: extends: file: docker-compose.gorge.yml service: gorge-maintenance gorge-worker: extends: file: docker-compose.gorge.yml service: gorge-worker image: ghcr.io/soulteary/gorge:worker-${GORGE_WORKER_IMAGE_TAG:-${GORGE_IMAGE_TAG:-2026.10.07-r2}} healthcheck: test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8170/readyz"] timeout: 6s start_period: 60s gorge-db-api: extends: file: docker-compose.gorge.yml service: gorge-db-api image: ghcr.io/soulteary/gorge:db-api-${GORGE_DB_IMAGE_TAG:-${GORGE_IMAGE_TAG:-2026.10.07-r2}} environment: GORGE_DB_NAMESPACE: ${PHORGE_DB_NAMESPACE:-phabricator} # 邮件、搜索和 Gitea bridge 不属于默认核心:它们需要用户提供外部后端或密钥。 gorge-mailer: profiles: ["mailer"] extends: file: docker-compose.gorge.yml service: gorge-mailer image: ghcr.io/soulteary/gorge:mailer-${GORGE_MAILER_IMAGE_TAG:-${GORGE_IMAGE_TAG:-2026.10.07-r2}} gorge-search: profiles: ["search"] extends: file: docker-compose.gorge.yml service: gorge-search image: ghcr.io/soulteary/gorge:search-${GORGE_SEARCH_IMAGE_TAG:-${GORGE_IMAGE_TAG:-2026.10.07-r2}} gorge-gitea: profiles: ["gitea"] extends: file: docker-compose.gorge.yml service: gorge-gitea image: ghcr.io/soulteary/gorge:gitea-${GORGE_GITEA_IMAGE_TAG:-unreleased} phorge-migrate: <<: *phorge-runtime container_name: phorge-migrate restart: "no" depends_on: mysql: condition: service_healthy db-init: condition: service_completed_successfully gorge-render: condition: service_started gorge-conduit: condition: service_started gorge-notification: condition: service_started gorge-file-storage: condition: service_started gorge-webhook: condition: service_started gorge-taskqueue: condition: service_started gorge-db-api: condition: service_started environment: <<: *phorge-environment PHORGE_CONTAINER_ROLE: migrate PHORGE_AUTO_UPGRADE: "1" PHORGE_PRODUCT_PROFILE: ${PHORGE_PRODUCT_PROFILE:-auto} GITEA_BASE_URI: ${GITEA_BASE_URI:-} GORGE_RENDER_URI: ${GORGE_RENDER_URI:-http://gorge-render:8140} GORGE_RENDER_TOKEN: ${GORGE_RENDER_TOKEN:-} GORGE_CONDUIT_URI: ${GORGE_CONDUIT_URI:-http://gorge-conduit:8150} GORGE_CONDUIT_TOKEN: ${GORGE_CONDUIT_TOKEN:-} GORGE_CONDUIT_UPSTREAM_URL: ${GORGE_CONDUIT_UPSTREAM_URL:-http://phorge:80} GORGE_NOTIFICATION_ADMIN_HOST: ${GORGE_NOTIFICATION_ADMIN_HOST:-gorge-notification} GORGE_NOTIFICATION_ADMIN_PORT: ${GORGE_NOTIFICATION_ADMIN_PORT:-22281} GORGE_NOTIFICATION_CLIENT_HOST: ${GORGE_NOTIFICATION_CLIENT_HOST:-127.0.0.1} GORGE_NOTIFICATION_CLIENT_PORT: ${GORGE_NOTIFICATION_CLIENT_PORT:-22280} GORGE_NOTIFICATION_CLIENT_PROTOCOL: ${GORGE_NOTIFICATION_CLIENT_PROTOCOL:-http} GORGE_NOTIFICATION_CLIENT_PATH: ${GORGE_NOTIFICATION_CLIENT_PATH:-} GORGE_NOTIFICATION_MODE: enable GORGE_NOTIFICATION_OUTBOX: ${GORGE_NOTIFICATION_OUTBOX:-false} GORGE_FILE_URI: ${GORGE_FILE_URI:-http://gorge-file-storage:8100} GORGE_FILE_TOKEN: ${GORGE_FILE_TOKEN:-} GORGE_FILE_UPLOADS: ${GORGE_FILE_UPLOADS:-false} GORGE_FILE_DELETION_OUTBOX: ${GORGE_FILE_DELETION_OUTBOX:-false} GORGE_WEBHOOK_URI: ${GORGE_WEBHOOK_URI:-http://gorge-webhook:8160} GORGE_WEBHOOK_TOKEN: ${GORGE_WEBHOOK_TOKEN:-} GORGE_WEBHOOK_MODE: ${GORGE_WEBHOOK_MODE:-enable} GORGE_TASKQUEUE_URI: ${GORGE_TASKQUEUE_URI:-http://gorge-taskqueue:8090} GORGE_TASKQUEUE_TOKEN: ${GORGE_TASKQUEUE_TOKEN:-} GORGE_TASKQUEUE_MODE: ${GORGE_TASKQUEUE_MODE:-enable} GORGE_DB_URL: ${GORGE_DB_URL-http://gorge-db-api:8080} GORGE_DB_TOKEN: ${GORGE_DB_TOKEN:-} GORGE_DB_MODE: ${GORGE_DB_MODE:-enable} GORGE_MAILER_MODE: disable GORGE_MAILER_KEY: ${GORGE_MAILER_KEY:-gorge-mailer} GORGE_SEARCH_MODE: disable command: ["/bin/true"] phorge: <<: *phorge-runtime container_name: phorge-web restart: unless-stopped depends_on: phorge-migrate: condition: service_completed_successfully # Optional profiles must publish their configuration before PHP loads # the shared deployment document. They do not depend on PHP readiness. phorge-mailer-config: condition: service_completed_successfully required: false phorge-search-config: condition: service_completed_successfully required: false gorge-render: condition: service_started gorge-conduit: condition: service_started gorge-notification: condition: service_started gorge-file-storage: condition: service_started gorge-webhook: condition: service_started gorge-taskqueue: condition: service_started gorge-worker: condition: service_started gorge-db-api: condition: service_started environment: <<: *phorge-environment PHORGE_CONTAINER_ROLE: web ports: - "${PHORGE_HTTP_PORT:-8088}:80" healthcheck: test: - CMD - php - -r - >- exit(@file_get_contents("http://127.0.0.1/status/", false, stream_context_create(array("http" => array("timeout" => 4, "header" => "Host: ".(parse_url(getenv("PHORGE_BASE_URI") ?: "http://127.0.0.1/", PHP_URL_HOST) ?: "127.0.0.1"))))) === false ? 1 : 0); interval: 15s timeout: 5s start_period: 60s retries: 5 phorge-daemon: <<: *phorge-runtime # Apache uses SIGWINCH; the daemon foreground wrapper handles SIGTERM. stop_signal: SIGTERM # Native phd stop can use 15s SIGINT plus 15s SIGTERM before returning. stop_grace_period: 45s container_name: phorge-daemon restart: unless-stopped depends_on: phorge-migrate: condition: service_completed_successfully gorge-taskqueue: condition: service_started gorge-worker: condition: service_healthy phorge: condition: service_healthy environment: <<: *phorge-environment PHORGE_CONTAINER_ROLE: daemon command: ["/usr/local/bin/phd-foreground"] healthcheck: test: ["CMD-SHELL", "su -s /bin/sh www-data -c '/opt/phorge/phorge/bin/phd status' >/dev/null 2>&1"] interval: 15s timeout: 5s start_period: 30s retries: 3 phorge-mailer-config: <<: *phorge-runtime profiles: ["mailer"] restart: "no" depends_on: phorge-migrate: condition: service_completed_successfully gorge-mailer: condition: service_started environment: <<: *phorge-environment PHORGE_CONTAINER_ROLE: migrate PHORGE_AUTO_UPGRADE: "0" PHORGE_PRODUCT_PROFILE: ${PHORGE_PRODUCT_PROFILE:-auto} GORGE_MAILER_MODE: enable GORGE_MAILER_EXCLUSIVE: ${GORGE_MAILER_EXCLUSIVE:-true} GORGE_MAILER_URI: ${GORGE_MAILER_URI:-http://gorge-mailer:8110} GORGE_MAILER_TOKEN: ${GORGE_MAILER_TOKEN:-} GORGE_MAILER_KEY: ${GORGE_MAILER_KEY:-gorge-mailer} GORGE_MAILER_TIMEOUT: ${GORGE_MAILER_TIMEOUT:-30} GORGE_MAILER_PRIORITY: ${GORGE_MAILER_PRIORITY:-} GORGE_MAILER_SUPPORTS_MESSAGE_ID: ${GORGE_MAILER_SUPPORTS_MESSAGE_ID:-0} command: ["/bin/true"] phorge-search-config: <<: *phorge-runtime profiles: ["search"] restart: "no" depends_on: phorge-migrate: condition: service_completed_successfully gorge-search: condition: service_started environment: <<: *phorge-environment PHORGE_CONTAINER_ROLE: migrate PHORGE_AUTO_UPGRADE: "0" PHORGE_PRODUCT_PROFILE: ${PHORGE_PRODUCT_PROFILE:-auto} GORGE_SEARCH_MODE: enable GORGE_SEARCH_EXCLUSIVE: ${GORGE_SEARCH_EXCLUSIVE:-true} GORGE_SEARCH_HOST: ${GORGE_SEARCH_HOST:-gorge-search} GORGE_SEARCH_PORT: ${GORGE_SEARCH_PORT:-8120} GORGE_SEARCH_PROTOCOL: ${GORGE_SEARCH_PROTOCOL:-http} GORGE_SEARCH_TOKEN: ${GORGE_SEARCH_TOKEN:-} GORGE_SEARCH_KEEP_MYSQL: ${GORGE_SEARCH_KEEP_MYSQL:-0} command: ["/bin/true"] volumes: phorge-db: phorge-conf: phorge-repo: phorge-files: