This guide is for setting up production on Coolify.
Steps
- Create a new Docker Compose Empty resource on your preferred server.
- Copy the compose template below
- Edit the Traefik routing rule in the
frontendservice labels to match your domain (see examples below) - Fill in SITE_NAME at the Environment Variables section (must match your Traefik Labels)
- Deploy (Deployment duration depends on the image. ERPNext usually takes around 3-4 minutes)
- 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=...