[Guide] Setup Production on Coolify (Supports Multitenant)

This guide is for setting up production on Coolify.

Looking for something similar on Dokploy? Check this.

Steps

  1. Create a new Docker Compose Empty resource on your preferred server.
  2. Copy the compose template below
  3. Edit the Traefik routing rule in the frontend service labels to match your domain (see examples below)
  4. Fill in SITE_NAME at the Environment Variables section (must match your Traefik Labels)
  5. Deploy (Deployment duration depends on the image. ERPNext usually takes around 3-4 minutes)
  6. Login to SITE_NAME with Administrator (password is auto-generated in environment variables)

Note: DO NOT add your domain to any service. Routing is configured via the Traefik labels in the compose file.


Compose File

services:
  configure:
    image: '${FRAPPE_IMAGE:-frappe/erpnext}:${FRAPPE_VERSION:-develop}'
    deploy:
      restart_policy:
        condition: none
    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; echo \"Configuration Complete\";\n"
    volumes:
      - 'sites:/home/frappe/frappe-bench/sites'
      - 'logs:/home/frappe/frappe-bench/logs'

  create-site:
    image: '${FRAPPE_IMAGE:-frappe/erpnext}:${FRAPPE_VERSION:-develop}'
    deploy:
      restart_policy:
        condition: none
    volumes:
      - 'sites:/home/frappe/frappe-bench/sites'
      - 'logs:/home/frappe/frappe-bench/logs'
    environment:
      SITE_NAME: '${SITE_NAME}'
      ADMIN_PASSWORD: '${SERVICE_PASSWORD_ADMIN}'
      DB_ROOT_PASSWORD: '${SERVICE_PASSWORD_DB}'
    entrypoint:
      - bash
      - '-c'
    command:
      - "wait-for-it -t 120 db:3306; wait-for-it -t 120 redis-cache:6379; if [ ! -d \"sites/$${SITE_NAME}\" ]; then\n  echo \"Site $${SITE_NAME} does not exist. Creating now...\";\n  bench new-site \"$${SITE_NAME}\" \\\n    --mariadb-root-password \"$${DB_ROOT_PASSWORD}\" \\\n    --db-root-username \"root\" \\\n    --admin-password \"$${ADMIN_PASSWORD}\" \\\n    --mariadb-user-host-login-scope '%' \\\n    --install-app erpnext \\\n    --set-default;\n  echo \"Site created successfully.\";\nelse\n  echo \"Site $${SITE_NAME} already exists. Skipping creation.\";\nfi\n"
    depends_on:
      configure:
        condition: service_completed_successfully
      db:
        condition: service_healthy

  backend:
    image: '${FRAPPE_IMAGE:-frappe/erpnext}:${FRAPPE_VERSION:-develop}'
    deploy:
      restart_policy:
        condition: on-failure
    volumes:
      - 'sites:/home/frappe/frappe-bench/sites'
      - 'logs:/home/frappe/frappe-bench/logs'
    depends_on:
      configure:
        condition: service_completed_successfully
      db:
        condition: service_healthy
      redis-cache:
        condition: service_started
      redis-queue:
        condition: service_started

  frontend:
    image: '${FRAPPE_IMAGE:-frappe/erpnext}:${FRAPPE_VERSION:-develop}'
    deploy:
      restart_policy:
        condition: on-failure
    command:
      - nginx-entrypoint.sh
    environment:
      BACKEND: 'backend:8000'
      SOCKETIO: 'websocket:9000'
      UPSTREAM_REAL_IP_ADDRESS: 127.0.0.1
      UPSTREAM_REAL_IP_HEADER: X-Forwarded-For
      UPSTREAM_REAL_IP_RECURSIVE: 'off'
      PROXY_READ_TIMEOUT: 300
      CLIENT_MAX_BODY_SIZE: 50m
    volumes:
      - 'sites:/home/frappe/frappe-bench/sites'
      - 'logs:/home/frappe/frappe-bench/logs'
    labels:
      - traefik.enable=true
      - traefik.http.routers.frappe-router.rule=HostRegexp(`^.+\.example\.com$`)
      - traefik.http.routers.frappe-router.entryPoints=https
      - traefik.http.routers.frappe-router.tls=true
      - traefik.http.routers.frappe-router.tls.certresolver=letsencrypt
      - traefik.http.services.frontend.loadbalancer.server.port=8080
    depends_on:
      backend:
        condition: service_started

  websocket:
    image: '${FRAPPE_IMAGE:-frappe/erpnext}:${FRAPPE_VERSION:-develop}'
    deploy:
      restart_policy:
        condition: on-failure
    command:
      - node
      - /home/frappe/frappe-bench/apps/frappe/socketio.js
    volumes:
      - 'sites:/home/frappe/frappe-bench/sites'
      - 'logs:/home/frappe/frappe-bench/logs'
    depends_on:
      configure:
        condition: service_completed_successfully

  queue-long:
    image: '${FRAPPE_IMAGE:-frappe/erpnext}:${FRAPPE_VERSION:-develop}'
    deploy:
      restart_policy:
        condition: on-failure
    command:
      - bench
      - worker
      - '--queue'
      - 'long,default,short'
    volumes:
      - 'sites:/home/frappe/frappe-bench/sites'
      - 'logs:/home/frappe/frappe-bench/logs'
    depends_on:
      configure:
        condition: service_completed_successfully

  queue-short:
    image: '${FRAPPE_IMAGE:-frappe/erpnext}:${FRAPPE_VERSION:-develop}'
    deploy:
      restart_policy:
        condition: on-failure
    command:
      - bench
      - worker
      - '--queue'
      - 'short,default'
    volumes:
      - 'sites:/home/frappe/frappe-bench/sites'
      - 'logs:/home/frappe/frappe-bench/logs'
    depends_on:
      configure:
        condition: service_completed_successfully

  scheduler:
    image: '${FRAPPE_IMAGE:-frappe/erpnext}:${FRAPPE_VERSION:-develop}'
    deploy:
      restart_policy:
        condition: on-failure
    command:
      - bench
      - schedule
    volumes:
      - 'sites:/home/frappe/frappe-bench/sites'
      - 'logs:/home/frappe/frappe-bench/logs'
    depends_on:
      configure:
        condition: service_completed_successfully

  db:
    image: '${MARIADB_IMAGE:-mariadb:10.6}'
    command:
      - '--character-set-server=utf8mb4'
      - '--collation-server=utf8mb4_unicode_ci'
      - '--skip-character-set-client-handshake'
      - '--skip-innodb-read-only-compressed'
    environment:
      MYSQL_ROOT_PASSWORD: '${SERVICE_PASSWORD_DB}'
      MARIADB_ROOT_PASSWORD: '${SERVICE_PASSWORD_DB}'
    volumes:
      - 'db-data:/var/lib/mysql'
    healthcheck:
      test:
        - CMD
        - mysqladmin
        - ping
        - '-h'
        - localhost
        - '--password=${SERVICE_PASSWORD_DB}'
      interval: 5s
      retries: 20

  redis-cache:
    image: '${REDIS_IMAGE:-redis:6.2-alpine}'
    healthcheck:
      test:
        - CMD
        - redis-cli
        - ping
      interval: 5s
      retries: 5

  redis-queue:
    image: '${REDIS_IMAGE:-redis:6.2-alpine}'
    healthcheck:
      test:
        - CMD
        - redis-cli
        - ping
      interval: 5s
      retries: 5

volumes:
  db-data: null
  sites: null
  logs: null

Configuring Your Domain (Traefik Routing)

You must edit the routing rule in the frontend service labels section. Find this line:

- traefik.http.routers.frappe-router.rule=HostRegexp(`^.+\.example\.com$`)

And replace it based on your needs:

Use Case Example Rule What It Does
Single Domain Host(`erp.yourdomain.com`) Only catches traffic for exactly erp.yourdomain.com
Specific Multitenant Host(`erp.company.com`) || Host(`app.company.com`) Catches traffic for multiple specific domains
Wildcard Multitenant HostRegexp(`^.+\.yourdomain\.com$`) Catches any subdomain under yourdomain.com

Example for a single domain:

- traefik.http.routers.frappe-router.rule=Host(`erp.mycompany.com`)

Example for wildcard subdomains:

- traefik.http.routers.frappe-router.rule=HostRegexp(`^.+\.mycompany\.com$`)

Environment Variables

Variable Name Default Value Description
REQUIRED
SITE_NAME (none) Required. The name of the first site to create. This will be the URL where you access the desk initially (e.g., erp.yourdomain.com).
SERVICE_PASSWORD_DB (auto-generated) MariaDB root password. Coolify automatically generates this.
SERVICE_PASSWORD_ADMIN (auto-generated) Frappe administrator password. Coolify automatically generates this.
OPTIONAL
FRAPPE_IMAGE frappe/erpnext The Docker image repository. Change to use custom images.
FRAPPE_VERSION develop The version tag (e.g., v15, v14, latest).
MARIADB_IMAGE mariadb:10.6 MariaDB version to use.
REDIS_IMAGE redis:6.2-alpine Redis version to use.

Multiple Deployments

If you need to run multiple Frappe instances on the same Coolify server, you must change the router name to prevent conflicts:

Find this line in the frontend labels:

- traefik.http.routers.frappe-router.rule=...

Change frappe-router to something unique for each deployment:

- traefik.http.routers.frappe-v2-router.rule=...