Hi everyone,
After weeks of trial and error, I finally managed to get Frappe and a full suite of apps running on a Synology NAS using Docker and Portainer. I’d like to share my experience so others can save time and benefit from what I learned.
I now have the following apps running successfully:
frappe
erpnext
hrms
payments
crm
builder
insights
print_designer
helpdesk
wiki
webshop
lms
drive
website_leads
raven
erpnext_shipping
woocommerce_integration
My starting point:
It all began with Marius Hosting’s guide, which helped me get ERPNext up and running.
However, I needed the HRMS module, and getting that integrated was a painful experience – nothing worked despite days of tinkering.
Eventually, I found the Docker image by @knimer on Docker Hub – and everything just clicked. From there, I got motivated and began testing more and more apps. A few tweaks to the Portainer stack later – and now the full setup installs in one go with all the apps listed above.
Step 1 – Prepare folders on your Synology NAS
You’ll need to create the following folders (use File Station or SSH):
/volume1/docker/erpnext/db
/volume1/docker/erpnext/empty-default
/volume1/docker/erpnext/logs
/volume1/docker/erpnext/redis-cache
/volume1/docker/erpnext/redis-queue
/volume1/docker/erpnext/redis-socketio
/volume1/docker/erpnext/sites
Step 2 – Add required files
- Create this empty file (no extension!):
/volume1/docker/erpnext/empty-default/default
This file overrides the default nginx config. It can be completely empty (0 bytes), or you can put commented lines inside (# … - as i do so) for future needs.
##
# You should look at the following URL's in order to grasp a solid understanding
# of Nginx configuration files in order to fully unleash the power of Nginx.
# https://www.nginx.com/resources/wiki/start/
# https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/
# https://wiki.debian.org/Nginx/DirectoryStructure
#
# In most cases, administrators will remove this file from sites-enabled/ and
# leave it as reference inside of sites-available where it will continue to be
# updated by the nginx packaging team.
#
# This file will automatically load configuration files provided by other
# applications, such as Drupal or Wordpress. These applications will be made
# available underneath a path with that package name, such as /drupal8.
#
# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
##
# Default server configuration
#
#server {
# listen 80 default_server;
# listen [::]:80 default_server;
# SSL configuration
#
# listen 443 ssl default_server;
# listen [::]:443 ssl default_server;
#
# Note: You should disable gzip for SSL traffic.
# See: https://bugs.debian.org/773332
#
# Read up on ssl_ciphers to ensure a secure configuration.
# See: https://bugs.debian.org/765782
#
# Self signed certs generated by the ssl-cert package
# Don't use them in a production server!
#
# include snippets/snakeoil.conf;
# root /var/www/html;
# Add index.php to the list if you are using PHP
# index index.html index.htm index.nginx-debian.html;
# server_name _;
# location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
# try_files $uri $uri/ =404;
# }
# pass PHP scripts to FastCGI server
#
#location ~ \.php$ {
# include snippets/fastcgi-php.conf;
#
# # With php-fpm (or other unix sockets):
# fastcgi_pass unix:/run/php/php7.4-fpm.sock;
# # With php-cgi (or other tcp sockets):
# fastcgi_pass 127.0.0.1:9000;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
#}
# Virtual Host configuration for example.com
#
# You can move that to a different file under sites-available/ and symlink that
# to sites-enabled/ to enable it.
#
#server {
# listen 80;
# listen [::]:80;
#
# server_name example.com;
#
# root /var/www/example.com;
# index index.html;
#
# location / {
# try_files $uri $uri/ =404;
# }
#}
- Create your common_site_config.json file here:
/volume1/docker/erpnext/sites/common_site_config.json
Put your basic config in there
{
"db_host": "erpnext-db",
"db_port": 3306,
"default_site": "Frappe",
"developer_mode": 1,
"redis_cache": "redis://redis-cache",
"redis_queue": "redis://redis-queue",
"redis_socketio": "redis://redis-socketio",
"socketio_port": 9000
}
Step 3 – Set folder permissions
Make sure the Docker user has full access to the folders above – this is well explained in steps 13–17 of the MariusHosting guide. You must do this before proceeding.
Step 4 – Portainer: Create a new stack
Now paste my stack (I’ll share below) and make a few adjustments:
version: "3.9"
services:
redis-queue:
image: redis
container_name: ERPNext-REDIS-QUEUE
hostname: redis-queue
mem_limit: 256m
mem_reservation: 50m
cpu_shares: 768
security_opt:
- no-new-privileges:true
read_only: true
healthcheck:
test: ["CMD-SHELL", "redis-cli ping || exit 1"]
volumes:
- /volume1/docker/erpnext/redis-queue:/data:rw
- /etc/localtime:/etc/localtime:ro
restart: on-failure:5
redis-cache:
image: redis
container_name: ERPNext-REDIS-CACHE
hostname: redis-cache
mem_limit: 256m
mem_reservation: 50m
cpu_shares: 768
security_opt:
- no-new-privileges:true
read_only: true
healthcheck:
test: ["CMD-SHELL", "redis-cli ping || exit 1"]
volumes:
- /volume1/docker/erpnext/redis-cache:/data:rw
- /etc/localtime:/etc/localtime:ro
restart: on-failure:5
redis-socketio:
image: redis
container_name: ERPNext-REDIS-SOCKETIO
hostname: redis-socketio
mem_limit: 256m
mem_reservation: 50m
cpu_shares: 768
security_opt:
- no-new-privileges:true
read_only: true
healthcheck:
test: ["CMD-SHELL", "redis-cli ping || exit 1"]
volumes:
- /volume1/docker/erpnext/redis-socketio:/data:rw
- /etc/localtime:/etc/localtime:ro
restart: on-failure:5
db:
image: mariadb:10.8-jammy
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --skip-character-set-client-handshake
container_name: ERPNext-DB
hostname: erpnext-db
mem_limit: 1g
cpu_shares: 768
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -u root -pmysqlrootpassword | grep 'mysqld is alive' || exit 1"]
interval: 10s
timeout: 5s
retries: 30
start_period: 30s
volumes:
- /volume1/docker/erpnext/db:/var/lib/mysql:rw
- /etc/localtime:/etc/localtime:ro
environment:
MYSQL_ROOT_PASSWORD: mysqlrootpassword
restart: on-failure:5
configurator:
image: knimer/erpnext:v15.20250526
entrypoint:
- bash
- -c
command:
- >
ls -1 apps > sites/apps.txt;
bench set-config -g db_host erpnext-db;
bench set-config -gp db_port 3306;
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-socketio";
bench set-config -gp socketio_port 9000;
container_name: ERPNext-CONFIGURATOR
hostname: configurator
cpu_shares: 768
security_opt:
- no-new-privileges:true
volumes:
- /volume1/docker/erpnext/sites:/home/frappe/frappe-bench/sites:rw
- /volume1/docker/erpnext/logs:/home/frappe/frappe-bench/logs:rw
- /etc/localtime:/etc/localtime:ro
environment:
DB_HOST: erpnext-db
DB_PORT: 3306
REDIS_CACHE: redis-cache
REDIS_QUEUE: redis-queue
REDIS_SOCKETIO: redis-socketio
SOCKETIO_PORT: 9000
restart: "no"
depends_on:
redis-queue:
condition: service_healthy
redis-cache:
condition: service_healthy
redis-socketio:
condition: service_healthy
db:
condition: service_healthy
backend:
image: knimer/erpnext:v15.20250526
container_name: ERPNext-BACKEND
hostname: backend
mem_limit: 1g
cpu_shares: 768
security_opt:
- no-new-privileges:true
volumes:
- /volume1/docker/erpnext/sites:/home/frappe/frappe-bench/sites:rw
- /volume1/docker/erpnext/logs:/home/frappe/frappe-bench/logs:rw
- /etc/localtime:/etc/localtime:ro
restart: on-failure:5
depends_on:
configurator:
condition: service_completed_successfully
websocket:
image: knimer/erpnext:v15.20250526
command:
- node
- /home/frappe/frappe-bench/apps/frappe/socketio.js
container_name: ERPNext-WEBOSCKET
hostname: websocket
mem_limit: 1g
cpu_shares: 768
security_opt:
- no-new-privileges:true
volumes:
- /volume1/docker/erpnext/sites:/home/frappe/frappe-bench/sites:rw
- /volume1/docker/erpnext/logs:/home/frappe/frappe-bench/logs:rw
- /etc/localtime:/etc/localtime:ro
restart: on-failure:5
depends_on:
configurator:
condition: service_completed_successfully
create-site:
image: knimer/erpnext:v15.20250526
entrypoint:
- bash
- -c
command:
- >
wait-for-it -t 480 erpnext-db:3306;
wait-for-it -t 480 redis-cache:6379;
wait-for-it -t 480 redis-queue:6379;
wait-for-it -t 480 redis-socketio:6379;
export start=`date +%s`;
until [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".db_host // empty"` ]] && \
[[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_cache // empty"` ]] && \
[[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_queue // empty"` ]];
do
echo "Waiting for sites/common_site_config.json to be created";
sleep 5;
if (( `date +%s`-start > 120 )); then
echo "could not find sites/common_site_config.json with required keys";
exit 1
fi
done;
echo "sites/common_site_config.json found";
bench set-config -g developer_mode 1;
bench new-site Frappe \
--no-mariadb-socket \
--mariadb-root-password=mysqlrootpassword \
--admin-password=admin \
--install-app=erpnext \
--install-app=hrms \
--install-app=payments \
--install-app=crm \
--install-app=builder \
--install-app=insights \
--install-app=print_designer \
--install-app=helpdesk \
--install-app=wiki \
--install-app=webshop \
--install-app=lms \
--install-app=drive \
--install-app=website_leads \
--install-app=raven \
--install-app=erpnext_shipping \
--install-app=woocommerce_integration \
--set-default;
container_name: ERPNext-CREATE-SITE
hostname: create-site
security_opt:
- no-new-privileges:true
volumes:
- /volume1/docker/erpnext/sites:/home/frappe/frappe-bench/sites:rw
- /volume1/docker/erpnext/logs:/home/frappe/frappe-bench/logs:rw
- /etc/localtime:/etc/localtime:ro
restart: "no"
depends_on:
configurator:
condition: service_completed_successfully
queue-default:
image: knimer/erpnext:v15.20250526
command: bench worker --queue default
container_name: ERPNext-QUEUE-DEFAULT
hostname: queue-default
mem_limit: 1g
cpu_shares: 768
security_opt:
- no-new-privileges:true
volumes:
- /volume1/docker/erpnext/sites:/home/frappe/frappe-bench/sites:rw
- /volume1/docker/erpnext/logs:/home/frappe/frappe-bench/logs:rw
- /etc/localtime:/etc/localtime:ro
restart: on-failure:5
depends_on:
configurator:
condition: service_completed_successfully
queue-long:
image: knimer/erpnext:v15.20250526
command: bench worker --queue long
container_name: ERPNext-QUEUE-LONG
hostname: queue-long
mem_limit: 1g
cpu_shares: 768
security_opt:
- no-new-privileges:true
volumes:
- /volume1/docker/erpnext/sites:/home/frappe/frappe-bench/sites:rw
- /volume1/docker/erpnext/logs:/home/frappe/frappe-bench/logs:rw
- /etc/localtime:/etc/localtime:ro
restart: on-failure:5
depends_on:
configurator:
condition: service_completed_successfully
queue-short:
image: knimer/erpnext:v15.20250526
command: bench worker --queue short
container_name: ERPNext-QUEUE-SHORT
hostname: queue-short
mem_limit: 1g
cpu_shares: 768
security_opt:
- no-new-privileges:true
volumes:
- /volume1/docker/erpnext/sites:/home/frappe/frappe-bench/sites:rw
- /volume1/docker/erpnext/logs:/home/frappe/frappe-bench/logs:rw
- /etc/localtime:/etc/localtime:ro
restart: on-failure:5
depends_on:
configurator:
condition: service_completed_successfully
scheduler:
image: knimer/erpnext:v15.20250526
command: bench schedule
container_name: ERPNext-SCHEDULER
hostname: scheduler
mem_limit: 1g
cpu_shares: 768
security_opt:
- no-new-privileges:true
volumes:
- /volume1/docker/erpnext/sites:/home/frappe/frappe-bench/sites:rw
- /volume1/docker/erpnext/logs:/home/frappe/frappe-bench/logs:rw
- /etc/localtime:/etc/localtime:ro
restart: on-failure:5
depends_on:
configurator:
condition: service_completed_successfully
frontend:
image: knimer/erpnext:v15.20250526
command:
- nginx-entrypoint.sh
container_name: ERPNext-FRONTEND
hostname: frontend
mem_limit: 1g
cpu_shares: 768
security_opt:
- no-new-privileges:true
ports:
- 8344:8080
volumes:
- /volume1/docker/erpnext/sites:/home/frappe/frappe-bench/sites:rw
- /volume1/docker/erpnext/logs:/home/frappe/frappe-bench/logs:rw
- /volume1/docker/erpnext/empty-default/default:/etc/nginx/sites-enabled/default:ro
- /etc/localtime:/etc/localtime:ro
environment:
BACKEND: backend:8000
FRAPPE_SITE_NAME_HEADER: Frappe
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_TIMOUT: 120
CLIENT_MAX_BODY_SIZE: 50m
restart: on-failure:5
depends_on:
backend:
condition: service_started
websocket:
condition: service_started
a) Select the apps you need:
Remove lines like --install-app=xyz from the ERPNext-CREATE-SITE service if you don’t want that app.
Make sure there are no blank lines left – they will cause build failures.
Note: Some apps depend on others – if something fails, the logs will tell you what’s missing.
b) Set the MySQL root password:
- In the ERPNext-DB container, update the password in both MYSQL_ROOT_PASSWORD and the healthcheck section (after -p).
- In the ERPNext-CREATE-SITE container, update mariadb-root-password.
c) Ready? Click “Deploy the stack” and grab a coffee
Step 5 – Installation time
On my slow Synology NAS, the full install took about 1.5 hours.
You can monitor progress via the container log for ERPNext-CREATE-SITE.
The installation order matters because of dependencies:
frappe
erpnext
hrms
payments
crm
builder
insights
print_designer
helpdesk
wiki
webshop
lms
drive
website_leads
raven
erpnext_shipping
woocommerce_integration
You should see dashboard updates and no tracebacks, merge conflicts, or errors .
At the end, you should see:
*** Scheduler is disabled ***
Current Site set to Frappe
Step 6 – Access ERPNext
Open:
http://:8344
Login:
- User: Administrator
- Pass: admin
Step 7 – Final cleanup
Once everything is working:
- You can stop the ERPNext-CREATE-SITE and ERPNext-CONFIGURATOR containers (they’ll show “exited 0”).
- You can optionally remove them from the stack or just comment them out with # so they’re not started again.
Final Thoughts
I’m really happy with how it turned out. Huge thanks to Marius and to knimer for the working Docker image – your work made this possible.
If anyone has questions or wants to share their own improvements, let’s collaborate!
Developer Mode
At the end, you can optionally disable developer mode again.
To do this, simply open the file at /volume1/docker/erpnext/sites/common_site-config.json and remove the line
“developer_mode”: 1,