Running Frappe + HRMS and many more apps via Portainer on Synology NAS (finally working!)

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

:wrench: 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.

:file_folder: 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

:page_facing_up: Step 2 – Add required files

  1. 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;
#       }
#}


  1. 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
}

:shield: 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.

:brick: 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 :coffee:

:clock3: 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

:earth_africa: Step 6 – Access ERPNext

Open:

http://:8344

Login:

  • User: Administrator
  • Pass: admin

:white_check_mark: 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.

:speech_balloon: 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!

:package: 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,

3 Likes

Correct = http://your_nas_ip:8344

Clamsy successfully installed Frappe and a full suite of apps, including HRMS, on a Synology NAS using Docker and Portainer after weeks of troubleshooting. The breakthrough came by using a Docker image from @knimer, which allowed all desired apps to run smoothly. They shared a step-by-step guide, including folder setup, permissions, configuration files, and a working Portainer stack. This is valuable for others because deploying ERPNext with multiple apps on non-standard hardware like Synology is often complex. Their post provides a tested solution, saving others time and frustration.

1 Like

Clamsy - I created an account just to tell you how grateful I am for what you did - not just developing the solution, but sharing it. All too often we care only about our own success, but you my friend - you went out of your way to help, and so completely too. I am about to leave stable employment for the first time in >20 years to set up my own little shop, and you have saved me $1000 a year getting a shit solution I cannot afford. I am so grateful. Honestly I would demonstrate this tangibly (like how I donated to Marius) if I knew how. Let me know. Thank you!

2 Likes

Hello Clamsy,

Thank you very much for your guide, i am able to setup the docker and able to login. i made some changes to site bench name “ebcaccount” and frappe_site_header to “ebcaccount” too and able to login.

when i create backup, it create backup successfully too.

docker exec -it ERPNext-BACKEND bash
bench --site Frappe backup --with-files

it created 4 backup files.
image

however, i have difficulties restoring the data.

bench --site ebcaccount restore \
  /home/frappe/frappe-bench/sites/ebcaccount/private/backups/20250619_015623-ebcaccount-database.sql.gz \
  --with-public-files /home/frappe/frappe-bench/sites/ebcaccount/private/backups/20250619_015623-ebcaccount-files.tar \
  --with-private-files /home/frappe/frappe-bench/sites/Frappe/private/backups/20250619_015623-ebcaccount-private-files.tar \
  --db-root-password mysqlrootpassword

whenever i try to restore data, it is stuck and does nothing. i try to restore back to my existing ebcaccount data, it is stuck.

Thus, i create another site called “elaccount” and after it is setup, i try to restore backup to the newly created site, it is stuck too . as shown in my screenshot below.

may i know where can i check the log of backup / or to check if it is even running the restore?
it has been stuck at this status for more than 30 minutes. I have check youtube that an progress bar should appear to show the progress of restore, but it is not appearing in my case.

Please assist to advice if there is anythigng i have done wrongly.

Thanks so much for your kind words – I really appreciate it!

@v23v23 But I don’t want or need a donation. What I do want is for more people to act the same way: share what they know. Things don’t always work out immediately for everyone, but with enough info and shared experiences, it becomes easier to learn and reach your goal. That’s exactly why I created my original post.

I’ve never learned programming – everything I know comes from forums and, more recently, ChatGPT. Just learning by doing. :smile:

Regarding the backup issue from @Benjamin_01 : unfortunately, I can’t help with that. I haven’t gotten that far yet. I’m doing all of this with Docker just for the fun of it. I don’t even have a productive system that I’ve tested backup/restore on.

Why ERPNext at all? There was just one reason: My wife struggles with a complicated Excel sheet to plan her work shifts – it takes her around 2 hours every week. That’s what got me thinking: maybe there’s a better solution via Docker? And then I discovered Frappe HRMS. The next challenge was: how do I get it running on my Synology NAS? And that’s how the whole project began. :wink:

Backups don’t matter to me at this stage, since I’m only playing around with the scheduling topic.

But if you find a solution for the backup problem, please be kind and share it here in the forum so that others can find and benefit from it too. Don’t keep your knowledge to yourself – that’s the spirit! :+1:

1 Like

Yes, please share - now I am abit worried about building my small business’ accounting/HRMS on a platform that i cannot backup or restore!

By the way – I’ve also been experimenting a bit with building custom Docker images that can include or exclude various Frappe repositories, depending on what you need. That way, you can tailor what’s available in your Synology Docker environment.

I’m working on a Mac – you should have at least some basic Docker knowledge to follow along. I’ll share how things are set up on my end (including Dockerfile, nginx-entrypoint.sh, nginx-template, and the necessary folder structure).

I start everything with this command:

cd ~/Downloads/Docker/myerpnext && docker buildx build --platform linux/amd64 --no-cache -t clamsy1111/erpnext:v15-20250616 --push .

This assumes you’ve placed everything in ~/Downloads/Docker/myerpnext. If not, you’ll need to adjust the path accordingly.

Please don’t rely on my Dockerfile at https://hub.docker.com/r/clamsy1111/erpnext just yet – I’m still actively tinkering with it and regularly changing the contents. Like I said: it’s all a fun side project for me :smile:

Files: Docker-Compose-Files (Dockerfile and nginx)

1 Like

@v23v23

I’m sure there will be a solution eventually – just needs a bit of testing and patience.

@Benjamin_01

I’m not sure exactly how you’ve been approaching the restore process, but I’d suggest trying the following step-by-step tests:


:white_check_mark:

Test 1 – Basic Roundtrip

  • Do a fresh installation of ERPNext.
  • Log in and create something small, like a user or an item – anything that lets you later verify if the restore worked.
  • Then perform a backup and an immediate restore within the same setup.
  • If that works, then the restore process in general is functional, and the issue must lie somewhere else.

:white_check_mark:

Test 2 – Reinstall and Restore

  • Again, start with a fresh installation, log in, create a test user or item.
  • Perform a backup only.
  • Now delete the entire installation, start over from scratch, and try restoring into this new environment.
  • If this also works: good! Continue.

:white_check_mark:

Test 3 – Changing App Context

  • Do a clean install with only ERPNext (no HRMS, no extra apps).
  • Log in, create a test entry, then backup and delete everything.
  • Now do a new full install, but this time with additional apps like HRMS already included.
  • Try restoring the previous “ERPNext-only” backup into this setup.

My guess:

This could be the point where the restore fails, because the app landscape is different between backup and restore.


:brain:

Conclusion (if test 3 fails)

If your backup was created from a system that only had ERPNext, and you try to restore it into a setup that already includes extra apps (like HRMS), it might break due to missing DB structure or schema mismatches.

In this case, try doing the following:

  1. Install the new system exactly like the old one (e.g., ERPNext only).
  2. Run the restore first .
  3. Only after the restore, install extra apps like HRMS via the backend container using:
bench --site elaccount --install-app hrms

The reasoning:

Some additional apps might modify or extend the database structure upon install, and those changes might conflict with what your backup expects or contains.

Just an idea – but give it a shot and let us know what happens!