services: # ── Database ──────────────────────────────────────────────────────────────── db: image: mariadb:10.6 restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-changeme123} MYSQL_DATABASE: _sys volumes: - db-data:/var/lib/mysql networks: - frappe-net command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci - --skip-character-set-client-handshake - --skip-innodb-read-only-compressed # ── Redis ─────────────────────────────────────────────────────────────────── redis-cache: image: redis:7-alpine restart: unless-stopped networks: - frappe-net volumes: - redis-cache-data:/data redis-queue: image: redis:7-alpine restart: unless-stopped networks: - frappe-net volumes: - redis-queue-data:/data # ── Site configurator (runs once, then exits) ─────────────────────────────── configurator: image: docker.io/frappe/erpnext:${ERPNEXT_VERSION:-version-15} restart: "no" entrypoint: ["bash", "-c"] command: - > ls -1 apps > sites/apps.txt; bench set-config -g db_host db; bench set-config -gp db_port 3306; bench set-config -g redis_cache "redis://redis-cache:6379"; bench set-config -g redis_queue "redis://redis-queue:6379"; bench set-config -g redis_socketio "redis://redis-queue:6379"; bench set-config -gp socketio_port 9000; environment: DB_HOST: db DB_PORT: "3306" REDIS_CACHE: redis://redis-cache:6379 REDIS_QUEUE: redis://redis-queue:6379 SOCKETIO_PORT: "9000" volumes: - sites:/home/frappe/frappe-bench/sites networks: - frappe-net depends_on: - db # ── Site creator (runs once to create site + install ERPNext) ─────────────── create-site: image: docker.io/frappe/erpnext:${ERPNEXT_VERSION:-version-15} restart: "no" volumes: - sites:/home/frappe/frappe-bench/sites - logs:/home/frappe/frappe-bench/logs entrypoint: ["bash", "-c"] command: - > wait-for-it -t 120 db:3306; wait-for-it -t 120 redis-cache:6379; wait-for-it -t 120 redis-queue:6379; export start=`date +%s`; until [[ -n `grep -hs ^ sites/common_site_config.json | python -c "import sys, json; cfg = json.load(sys.stdin); print(cfg.get('db_host', ''))"` ]]; do echo "Waiting for configurator to finish..."; sleep 5; if (( `date +%s`-start > 120 )); then echo "Timed out"; break; fi done; echo "Starting site creation..."; bench new-site ${SITE_NAME:-erp.localhost} \ --no-mariadb-socket \ --db-root-password=${DB_ROOT_PASSWORD:-changeme123} \ --admin-password=${ADMIN_PASSWORD:-admin123} \ --install-app erpnext; bench --site ${SITE_NAME:-erp.localhost} set-config allow_cors 1; echo "Site created successfully."; environment: DB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-changeme123} ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin123} SITE_NAME: ${SITE_NAME:-erp.localhost} networks: - frappe-net depends_on: configurator: condition: service_completed_successfully # ── Main backend ───────────────────────────────────────────────────────────── backend: image: docker.io/frappe/erpnext:${ERPNEXT_VERSION:-version-15} restart: unless-stopped volumes: - sites:/home/frappe/frappe-bench/sites - logs:/home/frappe/frappe-bench/logs networks: - frappe-net depends_on: create-site: condition: service_completed_successfully # ── WebSocket server ───────────────────────────────────────────────────────── websocket: image: docker.io/frappe/erpnext:${ERPNEXT_VERSION:-version-15} restart: unless-stopped command: ["node", "/home/frappe/frappe-bench/apps/frappe/socketio.js"] volumes: - sites:/home/frappe/frappe-bench/sites - logs:/home/frappe/frappe-bench/logs networks: - frappe-net depends_on: create-site: condition: service_completed_successfully # ── Queue workers ───────────────────────────────────────────────────────────── queue-short: image: docker.io/frappe/erpnext:${ERPNEXT_VERSION:-version-15} restart: unless-stopped command: ["bench", "worker", "--queue", "short,default"] volumes: - sites:/home/frappe/frappe-bench/sites - logs:/home/frappe/frappe-bench/logs networks: - frappe-net depends_on: create-site: condition: service_completed_successfully queue-long: image: docker.io/frappe/erpnext:${ERPNEXT_VERSION:-version-15} restart: unless-stopped command: ["bench", "worker", "--queue", "long,default,short"] volumes: - sites:/home/frappe/frappe-bench/sites - logs:/home/frappe/frappe-bench/logs networks: - frappe-net depends_on: create-site: condition: service_completed_successfully # ── Scheduler ───────────────────────────────────────────────────────────────── scheduler: image: docker.io/frappe/erpnext:${ERPNEXT_VERSION:-version-15} restart: unless-stopped command: ["bench", "schedule"] volumes: - sites:/home/frappe/frappe-bench/sites - logs:/home/frappe/frappe-bench/logs networks: - frappe-net depends_on: create-site: condition: service_completed_successfully # ── Nginx (reverse proxy + static files) ───────────────────────────────────── frontend: image: docker.io/frappe/erpnext:${ERPNEXT_VERSION:-version-15} restart: unless-stopped command: ["nginx-entrypoint.sh"] environment: BACKEND: backend:8000 SOCKETIO: websocket:9000 FRAPPE_SITE_NAME_HEADER: ${SITE_NAME:-erp.localhost} UPSTREAM_REAL_IP_ADDRESS: 127.0.0.1 UPSTREAM_REAL_IP_HEADER: X-Forwarded-For UPSTREAM_REAL_IP_RECURSIVE: "off" PROXY_READ_TIMEOUT: "120" CLIENT_MAX_BODY_SIZE: "50m" volumes: - sites:/home/frappe/frappe-bench/sites - logs:/home/frappe/frappe-bench/logs ports: - "${HTTP_PORT:-8080}:8080" networks: - frappe-net depends_on: backend: condition: service_started websocket: condition: service_started volumes: db-data: redis-cache-data: redis-queue-data: sites: logs: networks: frappe-net: driver: bridge