# ERPNext production stack for Coolify. # Domain: assign in Coolify UI → service `frontend` → port 8080. # Image: CUSTOM_IMAGE + CUSTOM_TAG=main (latest Jenkins build on Forgejo). # Host must have the image before deploy — Jenkins preloads :main after each push. # Manual once: bash scripts/coolify/sync-main-from-forgejo.sh x-customizable-image: &customizable_image image: ${CUSTOM_IMAGE:-git.aexoradao.com/epistemophiliac/erpnext}:${CUSTOM_TAG:-main} pull_policy: ${PULL_POLICY:-if_not_present} restart: ${RESTART_POLICY:-unless-stopped} x-frappe-platform: &frappe_platform platform: linux/amd64 x-sites-volume: &sites_volume volumes: - sites:/home/frappe/frappe-bench/sites x-depends-on-configurator: &depends_on_configurator depends_on: configurator: condition: service_completed_successfully x-backend-defaults: &backend_defaults <<: [*depends_on_configurator, *customizable_image, *frappe_platform, *sites_volume] services: db: image: mariadb:11.8 restart: unless-stopped command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci - --skip-character-set-client-handshake - --skip-innodb-read-only-compressed environment: - 'MYSQL_ROOT_PASSWORD=${DB_PASSWORD:-changeme}' - 'MARIADB_AUTO_UPGRADE=1' healthcheck: test: ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized'] start_period: 5s interval: 5s timeout: 5s retries: 10 volumes: - db-data:/var/lib/mysql redis-cache: image: redis:8.6-alpine restart: unless-stopped healthcheck: test: ['CMD', 'redis-cli', 'ping'] interval: 10s timeout: 5s retries: 5 redis-queue: image: redis:8.6-alpine restart: unless-stopped volumes: - redis-queue-data:/data healthcheck: test: ['CMD', 'redis-cli', 'ping'] interval: 10s timeout: 5s retries: 5 configurator: <<: *backend_defaults exclude_from_hc: true restart: 'no' entrypoint: ['bash', '-c'] command: - > ls -1 apps > sites/apps.txt; bench set-config -g db_host $$DB_HOST; bench set-config -gp db_port $$DB_PORT; bench set-config -g redis_cache "redis://$$REDIS_CACHE"; bench set-config -g redis_queue "redis://$$REDIS_QUEUE"; bench set-config -g redis_socketio "redis://$$REDIS_QUEUE"; bench set-config -gp socketio_port $$SOCKETIO_PORT; bench set-config -g chromium_path /usr/bin/chromium-headless-shell; environment: - 'DB_HOST=db' - 'DB_PORT=3306' - 'REDIS_CACHE=redis-cache:6379' - 'REDIS_QUEUE=redis-queue:6379' - 'SOCKETIO_PORT=9000' depends_on: db: condition: service_healthy redis-cache: condition: service_healthy redis-queue: condition: service_healthy create-site: <<: *customizable_image exclude_from_hc: true restart: 'no' platform: linux/amd64 entrypoint: ['bash', '-c'] command: - > export CI=1; B=/usr/local/bin/bench; SITE=$$SITE_NAME; if [ -z "$$SITE" ]; then echo "[create-site] ERROR: SITE_NAME empty — assign domain to frontend:8080 in Coolify (SERVICE_FQDN_FRONTEND)"; exit 1; fi; wait-for-it -t 120 db:3306; wait-for-it -t 120 redis-cache:6379; wait-for-it -t 120 redis-queue:6379; if [ -d "sites/$$SITE" ]; then echo "[create-site] exists"; $$B use "$$SITE"; else echo "[create-site] creating (10-20 min first time — verbose progress suppressed)"; INSTALL_ARGS=""; IFS=',' read -r -a apps <<< "$$INSTALL_APPS"; for app in "$${apps[@]}"; do INSTALL_ARGS="$$INSTALL_ARGS --install-app $$app"; done; set -o pipefail; $$B new-site "$$SITE" --mariadb-user-host-login-scope='%' --admin-password "$$ADMIN_PASSWORD" --db-root-password "$$DB_PASSWORD" $$INSTALL_ARGS --set-default 2>&1 | grep -vE '^Updating DocTypes for |^Creating Workspace|^Creating Desktop Icons|^Updating Dashboard for |^\* Installing |^Patching Existing|^rename_field:|^Thank you for installing|^Setting up Frappe HR'; fi environment: - 'SITE_NAME=${SITE_NAME:-${SERVICE_FQDN_FRONTEND}}' - 'ADMIN_PASSWORD=${ADMIN_PASSWORD:-changeme}' - 'DB_PASSWORD=${DB_PASSWORD:-changeme}' - 'INSTALL_APPS=${INSTALL_APPS:-erpnext,payments,hrms,lending,lms}' volumes: - sites:/home/frappe/frappe-bench/sites depends_on: configurator: condition: service_completed_successfully db: condition: service_healthy migrator: <<: *backend_defaults exclude_from_hc: true restart: 'no' entrypoint: ['bash', '-c'] command: - > if [ "$$MIGRATE_SITES" != "true" ]; then echo "[migrator] disabled"; exit 0; fi; if [ -z "$$(find sites -mindepth 2 -maxdepth 2 -name site_config.json 2>/dev/null)" ]; then echo "[migrator] no sites"; exit 0; fi; echo "[migrator] migrating all sites"; set -o pipefail; bench --site all migrate 2>&1 | grep -vE '^Updating DocTypes for |^Creating Workspace|^Creating Desktop Icons|^Updating Dashboard for '; environment: - 'MIGRATE_SITES=${MIGRATE_SITES:-true}' depends_on: create-site: condition: service_completed_successfully backend: <<: *backend_defaults environment: - 'GUNICORN_THREADS=${GUNICORN_THREADS:-4}' - 'GUNICORN_WORKERS=${GUNICORN_WORKERS:-2}' - 'GUNICORN_TIMEOUT=${GUNICORN_TIMEOUT:-120}' depends_on: configurator: condition: service_completed_successfully create-site: condition: service_completed_successfully migrator: condition: service_completed_successfully healthcheck: test: ['CMD-SHELL', 'curl -sf http://localhost:8000/api/method/ping || exit 1'] interval: 15s timeout: 10s retries: 10 start_period: 120s websocket: <<: [*depends_on_configurator, *customizable_image, *frappe_platform, *sites_volume] command: - node - /home/frappe/frappe-bench/apps/frappe/socketio.js depends_on: create-site: condition: service_completed_successfully frontend: <<: *customizable_image platform: linux/amd64 command: - nginx-entrypoint.sh environment: - SERVICE_URL_FRONTEND_8080 - SERVICE_FQDN_FRONTEND - 'BACKEND=backend:8000' - 'SOCKETIO=websocket:9000' - 'FRAPPE_SITE_NAME_HEADER=${FRAPPE_SITE_NAME_HEADER:-${SERVICE_FQDN_FRONTEND}}' - 'UPSTREAM_REAL_IP_ADDRESS=${UPSTREAM_REAL_IP_ADDRESS:-127.0.0.1}' - 'UPSTREAM_REAL_IP_HEADER=${UPSTREAM_REAL_IP_HEADER:-X-Forwarded-For}' - 'UPSTREAM_REAL_IP_RECURSIVE=${UPSTREAM_REAL_IP_RECURSIVE:-off}' - 'PROXY_READ_TIMEOUT=${PROXY_READ_TIMEOUT:-120}' - 'CLIENT_MAX_BODY_SIZE=${CLIENT_MAX_BODY_SIZE:-50m}' volumes: - sites:/home/frappe/frappe-bench/sites depends_on: backend: condition: service_healthy websocket: condition: service_started healthcheck: test: ['CMD-SHELL', 'curl -sf http://localhost:8080/ || exit 1'] interval: 15s timeout: 10s retries: 15 start_period: 90s queue-short: <<: *backend_defaults command: - bench - worker - --queue - short,default depends_on: create-site: condition: service_completed_successfully migrator: condition: service_completed_successfully queue-long: <<: *backend_defaults command: - bench - worker - --queue - long,default,short depends_on: create-site: condition: service_completed_successfully migrator: condition: service_completed_successfully scheduler: <<: *backend_defaults command: - bench - schedule depends_on: create-site: condition: service_completed_successfully migrator: condition: service_completed_successfully volumes: sites: db-data: redis-queue-data: