Erpnextv15-SSH-setup-241111
FORCETOC
ERPNext v15 Deployment Guide: Portainer + ZeroTier + Nginx Proxy Manager
This tutorial covers setting up a production-ready ERPNext environment using a local Host Machine for processing power and a Cloud VM for public accessibility, connected securely via ZeroTier.
1. Architectural Overview & Requirements
The "Cloud VM" (Gateway)
Providers: Contabo or Hetzner.
Cost: ~4,000–6,000 PHP/year (Confirmed: €4.50–€6.00/mo).
Purpose: Runs Nginx Proxy Manager (NPM) in Docker. It acts as the public face with a Static IP.
Network: Connected to your ZeroTier network.
The "Host Machine" (ERPNext Engine)
Recommended Hardware: Dell 3070 Micro (i5 8th Gen, 16GB RAM, 256GB SSD).
OS: Ubuntu 24.04 LTS or Debian 12 (Clean install).
Network: Connected to the same ZeroTier network.
2. Host Machine: Remote Access & Preparation
To operate the machine "headless" (without a monitor), you must set up SSH for command-line control and RDP for graphical access.
A. SSH Access (Command Line)
SSH is the primary way to manage your server. From your Windows or Mac laptop, you can connect using a terminal.
Update the system
sudo apt update && sudo apt upgrade -y
Install OpenSSH Server
sudo apt install openssh-server -y
Check if SSH is running
sudo systemctl status ssh
To connect from your main computer: ssh username@your-machine-ip
B. Remote Desktop & Remmina (Graphical UI)
If you prefer a visual interface, use xRDP on the server and Remmina on your client machine (if using Linux) or Microsoft Remote Desktop (if using Windows).
On the Host Machine (Server):
Install xRDP
sudo apt install xrdp -y
sudo systemctl enable --now xrdp
Add xrdp to ssl-cert group to avoid permission issues
sudo adduser xrdp ssl-cert
On your Client Machine (using Remmina):
Open Remmina.
Create a new connection profile.
Select RDP as the protocol.
Enter the IP address of the Host Machine.
Set the color depth to "High Color (16bpp)" for better performance over ZeroTier.
C. Screen for Persistent Sessions
Since ERPNext installation can take time, use screen so the process doesn't die if your SSH connection drops.
Install screen
sudo apt install screen -y
To start a session:
screen -S erp-install
To detach (if you want to close the window): press Ctrl+A, then D
To re-attach later:
screen -r erp-install
3. ZeroTier Networking
Install ZeroTier on both the Cloud VM and the Local Host to create a virtual "local" bridge.
curl -s https://www.google.com/search?q=https://install.zerotier.com | sudo bash
sudo zerotier-one -q join [YOUR_NETWORK_ID]
Go to the ZeroTier Central dashboard and Authorize both devices. Note the Managed IP of your Local Host (e.g., 10.147.17.5).
4. Install Docker & Portainer CE
On your Host Machine, run the following to install the engine and the management UI.
A. Docker Engine & Compose
Install dependencies
sudo apt install apt-transport-https ca-certificates curl gnupg lsb-release -y
Add Docker’s official GPG key
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://www.google.com/search?q=https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
Set up the repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin -y
B. Portainer CE Installation
sudo docker volume create portainer_data
sudo docker run -d -p 8000:8000 -p 9443:9443 --name portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/var/data portainer/portainer-ce:latest
Access Portainer at: https://[Your-Host-ZeroTier-IP]:9443
5. Deploying the ERPNext Stack
Log into Portainer.
Go to Stacks > Add stack.
Name it erpnext-v15.
Paste the following configuration into the Web editor:
version: "3"
services:
backend:
image: frappe/erpnext:v15.51.1
restart: always
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
configurator:
image: frappe/erpnext:v15.51.1
restart: always
entrypoint:
- bash
- -c
command:
- >
ls -1 apps > sites/apps.txt;
bench set-config -g db_host $$DB_HOST;
bench set-config -gp db_port $$DB_PORT;
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_QUEUE";
bench set-config -gp socketio_port $$SOCKETIO_PORT;
environment:
DB_HOST: db
DB_PORT: "3306"
REDIS_CACHE: redis-cache:6379
REDIS_QUEUE: redis-queue:6379
SOCKETIO_PORT: "9000"
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
create-site:
image: frappe/erpnext:v15.51.1
restart: always
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 | 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 new-site --no-mariadb-socket --admin-password=admin --db-root-password=admin --install-app erpnext --set-default erpnv15.comfac-it.net;
db:
image: mariadb:10.6
healthcheck:
test: mysqladmin ping -h localhost --password=admin
interval: 1s
retries: 15
restart: always
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --skip-character-set-client-handshake
- --skip-innodb-read-only-compressed
environment:
MYSQL_ROOT_PASSWORD: admin
volumes:
- db-data:/var/lib/mysql
frontend:
image: frappe/erpnext:v15.51.1
depends_on:
- websocket
restart: always
command:
- nginx-entrypoint.sh
environment:
BACKEND: backend:8000
FRAPPE_SITE_NAME_HEADER: erpnv15.comfac-it.net
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: 120
CLIENT_MAX_BODY_SIZE: 50m
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
ports:
- "8715:8080"
queue-long:
image: frappe/erpnext:v15.51.1
restart: always
command: [bench, worker, --queue, "long,default,short"]
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
queue-short:
image: frappe/erpnext:v15.51.1
restart: always
command: [bench, worker, --queue, "short,default"]
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
redis-queue:
image: redis:6.2-alpine
restart: always
volumes:
- redis-queue-data:/data
redis-cache:
image: redis:6.2-alpine
restart: always
volumes:
- redis-cache-data:/data
scheduler:
image: frappe/erpnext:v15.51.1
restart: always
command: [bench, schedule]
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
websocket:
image: frappe/erpnext:v15.51.1
restart: always
command: [node, /home/frappe/frappe-bench/apps/frappe/socketio.js]
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
volumes:
db-data:
redis-queue-data:
redis-cache-data:
sites:
logs:
Click Deploy the stack.
6. Finalizing with Nginx Proxy Manager
On your Cloud VM (where NPM is running):
Add a new Proxy Host.
Domain Names: erpnv15.comfac-it.net
Scheme: http
Forward IP: Enter the ZeroTier IP of your Host Machine (e.g., 10.147.17.5).
Forward Port: 8715 (as defined in your frontend service).
Enable Websockets Support.
In the SSL tab, request a new Let's Encrypt Certificate.
Important Notes
Permissions: If you run into folder permission issues on the host, ensure the sites volume is owned by the user running docker or set to 775.
Site Creation: The create-site container will run once to initialize the database. Monitor its logs in Portainer to ensure the --new-site command completes successfully.