# Base compose file — used by both local dev and staging. # Local dev: docker compose up -d (auto-loads docker-compose.override.yml) # Staging: docker compose -f docker-compose.yml -f docker-compose.staging.yml up -d services: wp: build: context: . dockerfile: docker/wp/Dockerfile restart: unless-stopped security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - NET_BIND_SERVICE - SETUID - SETGID - DAC_OVERRIDE - CHOWN - FOWNER environment: WORDPRESS_DB_HOST: wp-db WORDPRESS_DB_USER: wordpress WORDPRESS_DB_PASSWORD: ${WP_DB_PASSWORD} WORDPRESS_DB_NAME: wordpress WP_ADMIN_PASSWORD: ${WP_ADMIN_PASSWORD:?WP_ADMIN_PASSWORD not set in .env} WPOJS_BASE_URL: ${WPOJS_BASE_URL:?WPOJS_BASE_URL not set in .env} # WordPress's own config (Bedrock reads these through env()). Passed as # named variables rather than mounting .env, which would put all 82 of # this project's secrets — OJS admin and DB-root passwords among them — # inside a public-facing web container to supply the 17 below. # DB_PREFIX and DISABLE_WP_CRON are deliberately absent: application.php # falls back to 'wp_' and false, which is the behaviour today. DB_HOST: ${DB_HOST:?DB_HOST not set in .env} DB_NAME: ${DB_NAME:?DB_NAME not set in .env} DB_USER: ${DB_USER:?DB_USER not set in .env} DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD not set in .env} WP_HOME: ${WP_HOME:?WP_HOME not set in .env} WP_SITEURL: ${WP_SITEURL:?WP_SITEURL not set in .env} WP_ENV: ${WP_ENV:?WP_ENV not set in .env} WP_DEBUG_LOG: ${WP_DEBUG_LOG:-} WPOJS_API_KEY: ${WPOJS_API_KEY:?WPOJS_API_KEY not set in .env} AUTH_KEY: ${AUTH_KEY:?AUTH_KEY not set in .env} SECURE_AUTH_KEY: ${SECURE_AUTH_KEY:?SECURE_AUTH_KEY not set in .env} LOGGED_IN_KEY: ${LOGGED_IN_KEY:?LOGGED_IN_KEY not set in .env} NONCE_KEY: ${NONCE_KEY:?NONCE_KEY not set in .env} AUTH_SALT: ${AUTH_SALT:?AUTH_SALT not set in .env} SECURE_AUTH_SALT: ${SECURE_AUTH_SALT:?SECURE_AUTH_SALT not set in .env} LOGGED_IN_SALT: ${LOGGED_IN_SALT:?LOGGED_IN_SALT not set in .env} NONCE_SALT: ${NONCE_SALT:?NONCE_SALT not set in .env} volumes: - ./wordpress:/var/www/html - ./plugins/wpojs-sync:/var/www/html/web/app/plugins/wpojs-sync - wp_uploads:/var/www/html/web/app/uploads - ./scripts:/var/www/html/scripts:ro - ./docker/data/test-users.csv:/data/test-users.csv:ro depends_on: wp-db: condition: service_healthy networks: - sea-net wp-db: image: mariadb:10.11 restart: unless-stopped security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - SETUID - SETGID - DAC_OVERRIDE - CHOWN - FOWNER environment: MYSQL_ROOT_PASSWORD: ${WP_DB_ROOT_PASSWORD} MYSQL_DATABASE: wordpress MYSQL_USER: wordpress MYSQL_PASSWORD: ${WP_DB_PASSWORD} volumes: - wp_db_data:/var/lib/mysql healthcheck: test: ["CMD-SHELL", "healthcheck.sh --connect --innodb_initialized"] interval: 10s timeout: 5s retries: 5 networks: - sea-net ojs: build: context: . dockerfile: docker/ojs/Dockerfile # Extends pkpofficial/ojs:3_5_0-3 — adds gettext for envsubst platform: linux/amd64 # OJS base image is amd64 only restart: unless-stopped security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - NET_BIND_SERVICE - SETUID - SETGID - DAC_OVERRIDE - CHOWN - FOWNER entrypoint: /etc/ojs/entrypoint.sh environment: OJS_DB_HOST: ojs-db OJS_DB_USER: ojs OJS_DB_PASSWORD: ${OJS_DB_PASSWORD} OJS_DB_NAME: ojs OJS_CLI_INSTALL: "0" OJS_BASE_URL: ${OJS_BASE_URL:?OJS_BASE_URL not set in .env} OJS_TIMEZONE: ${OJS_TIMEZONE:?OJS_TIMEZONE not set in .env} OJS_ADMIN_USER: ${OJS_ADMIN_USER:?OJS_ADMIN_USER not set in .env} OJS_ADMIN_PASSWORD: ${OJS_ADMIN_PASSWORD:?OJS_ADMIN_PASSWORD not set in .env} OJS_ADMIN_EMAIL: ${OJS_ADMIN_EMAIL:?OJS_ADMIN_EMAIL not set in .env} OJS_API_KEY_SECRET: ${OJS_API_KEY_SECRET:?OJS_API_KEY_SECRET not set in .env} WPOJS_API_KEY_SECRET: ${WPOJS_API_KEY_SECRET:?WPOJS_API_KEY_SECRET not set in .env} WPOJS_ALLOWED_IPS: ${WPOJS_ALLOWED_IPS:?WPOJS_ALLOWED_IPS not set in .env} WPOJS_WP_MEMBER_URL: ${WPOJS_WP_MEMBER_URL:?WPOJS_WP_MEMBER_URL not set in .env} WPOJS_SUPPORT_EMAIL: ${WPOJS_SUPPORT_EMAIL:-} # Archive mode (plugins/archive-mode): the journal's new home. Empty = off. OJS_ARCHIVE_MOVED_TO: ${OJS_ARCHIVE_MOVED_TO:-} # UI messages (written to plugin_settings DB by setup-ojs.sh, not config.inc.php) WPOJS_DEFAULT_LOGIN_HINT: ${WPOJS_DEFAULT_LOGIN_HINT:-} WPOJS_DEFAULT_PAYWALL_HINT: ${WPOJS_DEFAULT_PAYWALL_HINT:-} WPOJS_DEFAULT_FOOTER_MESSAGE: ${WPOJS_DEFAULT_FOOTER_MESSAGE:-} WPOJS_DEFAULT_PASSWORD_RESET_HINT: ${WPOJS_DEFAULT_PASSWORD_RESET_HINT:-} # Journal metadata (used by setup-ojs.sh) OJS_JOURNAL_PATH: ${OJS_JOURNAL_PATH:?OJS_JOURNAL_PATH not set in .env} OJS_JOURNAL_NAME: ${OJS_JOURNAL_NAME:-} OJS_JOURNAL_ACRONYM: ${OJS_JOURNAL_ACRONYM:-} OJS_INLINE_ORG_NAME: ${OJS_INLINE_ORG_NAME:-} OJS_JOURNAL_CONTACT_NAME: ${OJS_JOURNAL_CONTACT_NAME:-} OJS_JOURNAL_CONTACT_EMAIL: ${OJS_JOURNAL_CONTACT_EMAIL:-} OJS_JOURNAL_PUBLISHER: ${OJS_JOURNAL_PUBLISHER:-} OJS_JOURNAL_PUBLISHER_URL: ${OJS_JOURNAL_PUBLISHER_URL:-} OJS_JOURNAL_PRINT_ISSN: ${OJS_JOURNAL_PRINT_ISSN:-} OJS_JOURNAL_ONLINE_ISSN: ${OJS_JOURNAL_ONLINE_ISSN:-} OJS_JOURNAL_COUNTRY: ${OJS_JOURNAL_COUNTRY:-} OJS_JOURNAL_DESCRIPTION: ${OJS_JOURNAL_DESCRIPTION:-} OJS_JOURNAL_ABOUT: ${OJS_JOURNAL_ABOUT:-} OJS_JOURNAL_SUBSCRIPTION_INFO: ${OJS_JOURNAL_SUBSCRIPTION_INFO:-} OJS_LOGO_ALT_TEXT: ${OJS_LOGO_ALT_TEXT:-} OJS_HOMEPAGE_IMAGE_ALT_TEXT: ${OJS_HOMEPAGE_IMAGE_ALT_TEXT:-} OJS_BANNER_ALT_TEXT: ${OJS_BANNER_ALT_TEXT:-Events} OJS_FOOTER_LOGO_ALT_TEXT: ${OJS_FOOTER_LOGO_ALT_TEXT:-} OJS_SUB_TYPES: ${OJS_SUB_TYPES:-} OJS_PURCHASE_ARTICLE_FEE: ${OJS_PURCHASE_ARTICLE_FEE:-} OJS_PURCHASE_ISSUE_FEE: ${OJS_PURCHASE_ISSUE_FEE:-} OJS_STRIPE_SECRET_KEY: ${OJS_STRIPE_SECRET_KEY:-} OJS_STRIPE_PUBLISHABLE_KEY: ${OJS_STRIPE_PUBLISHABLE_KEY:-} OJS_STRIPE_TEST_SECRET_KEY: ${OJS_STRIPE_TEST_SECRET_KEY:-} OJS_STRIPE_TEST_PUBLISHABLE_KEY: ${OJS_STRIPE_TEST_PUBLISHABLE_KEY:-} OJS_STRIPE_TEST_WEBHOOK_SECRET: ${OJS_STRIPE_TEST_WEBHOOK_SECRET:-} OJS_STRIPE_WEBHOOK_SECRET: ${OJS_STRIPE_WEBHOOK_SECRET:-} OJS_STRIPE_TEST_MODE: ${OJS_STRIPE_TEST_MODE:-} QA_NOSUB_PASSWORD: ${QA_NOSUB_PASSWORD:-} QA_SUB_PASSWORD: ${QA_SUB_PASSWORD:-} OJS_DOI_PREFIX: ${OJS_DOI_PREFIX:-} OJS_DOI_DEPOSITOR_NAME: ${OJS_DOI_DEPOSITOR_NAME:-} OJS_DOI_DEPOSITOR_EMAIL: ${OJS_DOI_DEPOSITOR_EMAIL:-} OJS_CROSSREF_USERNAME: ${OJS_CROSSREF_USERNAME:-} OJS_CROSSREF_PASSWORD: ${OJS_CROSSREF_PASSWORD:-} OJS_SMTP_ENABLED: ${OJS_SMTP_ENABLED:-} OJS_SMTP_HOST: ${OJS_SMTP_HOST:-} OJS_SMTP_PORT: ${OJS_SMTP_PORT:-} OJS_SMTP_AUTH: ${OJS_SMTP_AUTH:-} OJS_SMTP_USER: ${OJS_SMTP_USER:-} OJS_SMTP_PASSWORD: ${OJS_SMTP_PASSWORD:-} OJS_MAIL_FROM: ${OJS_MAIL_FROM:-} BETTERSTACK_HB_OJS_CRON: ${BETTERSTACK_HB_OJS_CRON:-} volumes: - ojs_data:/var/www/html - ojs_files:/var/www/files - ./plugins/wpojs-subscription-api:/var/www/html/plugins/generic/wpojsSubscriptionApi - ./plugins/wpojs-subscription-api/api/v1/wpojs:/var/www/html/api/v1/wpojs:ro - ./plugins/ojs-inline-html-galley:/var/www/html/plugins/generic/inlineHtmlGalley - ./plugins/stripe-payment:/var/www/html/plugins/paymethod/stripe - ./plugins/archive-checker:/var/www/html/plugins/generic/archiveChecker - ./plugins/archive-checker/api/v1/archive-checker:/var/www/html/api/v1/archive-checker:ro - ./plugins/smarter-similar-articles:/var/www/html/plugins/generic/smarterSimilarArticles - ./plugins/umami-analytics:/var/www/html/plugins/generic/umamiAnalytics - ./plugins/archive-mode:/var/www/html/plugins/generic/archiveMode - ./docker/ojs/entrypoint.sh:/etc/ojs/entrypoint.sh:ro - ./docker/ojs/config.inc.php.tmpl:/etc/ojs/config.inc.php.tmpl:ro - ./docker/ojs/php-overrides.ini:/usr/local/etc/php/conf.d/zz-overrides.ini:ro - ./docker/ojs/apache.conf:/etc/apache2/conf-enabled/sea-cgipassauth.conf:ro - ./scripts:/scripts:ro - "./private/editorial-roles.json:/data/editorial-roles.json:ro" depends_on: ojs-db: condition: service_healthy networks: - sea-net ojs-db: image: mariadb:10.11 restart: unless-stopped security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - SETUID - SETGID - DAC_OVERRIDE - CHOWN - FOWNER environment: MYSQL_ROOT_PASSWORD: ${OJS_DB_ROOT_PASSWORD} MYSQL_DATABASE: ojs MYSQL_USER: ojs MYSQL_PASSWORD: ${OJS_DB_PASSWORD} volumes: - ojs_db_data:/var/lib/mysql healthcheck: test: ["CMD-SHELL", "healthcheck.sh --connect --innodb_initialized"] interval: 10s timeout: 5s retries: 5 networks: - sea-net adminer: image: adminer:4.8.1-standalone restart: unless-stopped security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - NET_BIND_SERVICE environment: ADMINER_DEFAULT_SERVER: wp-db depends_on: - wp-db - ojs-db networks: - sea-net volumes: wp_uploads: wp_db_data: ojs_data: ojs_files: ojs_db_data: networks: # EXTERNAL since 2026-08-16, and no longer this project's to create. # # This project used to own the network every container on the box attached to # — Harbour, Outline and the shared Caddy included — so `docker compose down` # here would have taken the whole box's networking with it, on the one day # nobody could afford the surprise. `sea-net` is now created by hand and # belongs to no compose project: docker network create sea-net sea-net: external: true name: sea-net