Note
This guide is for setting up Development instance on Dokploy.
For Production instance on Dokploy check this.To setup development containers locally on your PC use this.
Setting up Dokploy
To setup Dokploy on your server, follow the official guide.
Personally, I like to create a project called Frappe which includes two environments Production & Development. However you can setup a separate project for development.
Then add a service to your environment, and choose compose. Name doesn’t matter, call it bench or whatever project/app you are working on.
Steps
- Fill in the compose file.
- Fill in your environment variables
- Add in your domains
- If you are planning to spin up multiple development instances (services) then Enable Isolated Deployment from Advanced tab.
- Deploy
- From logs wait until init-bench container finishes the automated setup (5-10minutes).
- Access your development instance on the configured domain.
What it does
- Pulls from frappe/bench:latest
- Only Frappe Framework is installed. If you want ERPNext (or other Frappe app) as part of your development you can install it later via bench.
- Initializes development setup
- Initializes bench
- Administrator user, password as per environment variable.
- Creates a site (as per environment variable)
- Enables developer mode
- Allows CORS
- Ignores CSRF (warning)
- Enables
CHOKIDAR_USEPOLLINGfor file watching in Docker
- Sets up additional development tools
- Github CLI
- ZSH + theme
- Patches frappe’s realtime (socket)
- There might be better solutions, but this is the only solution I figured.
- Patching is required for socket to work correctly behind Dokploy’s reverse proxy (Traefik).
- How it works:
- Backs up original files on first run (`.original` files)
- Restores and re-applies patches on each deployment
- What it patches:
utils.js: Forces API calls to use internal service name (`http://backend:8000`) instead of external domainauthenticate.js: Disables strict origin/host matching to allow requests through reverse proxy
Compose File
services:
mariadb:
image: mariadb:10.6
restart: always
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_ROOT_HOST: '%'
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --skip-character-set-client-handshake
- --skip-innodb-read-only-compressed
volumes:
- mariadb-data:/var/lib/mysql
networks:
- frappe-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-p${DB_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 10
redis-cache:
image: redis:6.2-alpine
restart: always
volumes:
- redis-cache-data:/data
networks:
- frappe-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
redis-queue:
image: redis:6.2-alpine
restart: always
volumes:
- redis-queue-data:/data
networks:
- frappe-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
redis-socketio:
image: redis:6.2-alpine
restart: always
volumes:
- redis-socketio-data:/data
networks:
- frappe-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
init-bench:
image: frappe/bench:latest
command: >
bash -c "
if [ -f /home/frappe/frappe-bench/sites/common_site_config.json ]; then
echo 'Bench already initialized';
exit 0;
fi;
echo 'Initializing bench...';
cd /home/frappe;
bench init --skip-redis-config-generation --frappe-branch ${FRAPPE_BRANCH} frappe-bench;
cd frappe-bench;
bench set-config -g db_host mariadb;
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-socketio:6379;
bench set-config -gp socketio_port 9000;
bench new-site ${SITE_NAME} --mariadb-root-password ${DB_ROOT_PASSWORD} --admin-password ${ADMIN_PASSWORD} --no-mariadb-socket --db-root-username root;
bench --site ${SITE_NAME} set-config developer_mode 1;
bench --site ${SITE_NAME} set-config ignore_csrf 1;
bench --site ${SITE_NAME} set-config allow_cors '*';
bench use ${SITE_NAME};
echo 'Initialization complete';
"
environment:
- FRAPPE_BRANCH=${FRAPPE_BRANCH}
- SITE_NAME=${SITE_NAME}
- DB_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
volumes:
- frappe-home:/home/frappe
depends_on:
mariadb:
condition: service_healthy
redis-cache:
condition: service_healthy
redis-queue:
condition: service_healthy
redis-socketio:
condition: service_healthy
networks:
- frappe-network
restart: "no"
patch-socketio:
image: frappe/bench:latest
command:
- bash
- -c
- |
echo 'Waiting for bench initialization...'
until [ -f /home/frappe/frappe-bench/apps/frappe/socketio.js ]; do
sleep 5
done
echo 'Applying socket.io patches for Dokploy reverse proxy...'
cd /home/frappe/frappe-bench/apps/frappe/realtime
# backup original files on first run
if [ ! -f utils.js.original ]; then
cp utils.js utils.js.original
fi
if [ ! -f middlewares/authenticate.js.original ]; then
cp middlewares/authenticate.js middlewares/authenticate.js.original
fi
# always restore from originals before patching
cp utils.js.original utils.js
cp middlewares/authenticate.js.original middlewares/authenticate.js
# patch utils.js to use internal service
echo 'Patching utils.js to use internal backend service...'
cat > utils.js << 'EOFPATCH'
const { get_conf } = require("../node_utils");
const conf = get_conf();
function get_url(socket, path) {
if (!path) {
path = "";
}
// Dokploy patch: Always use internal service name
return "http://backend:8000" + path;
}
module.exports = {
get_url,
};
EOFPATCH
echo 'Patching authenticate.js to allow reverse proxy...'
sed -i '/if (get_hostname(socket.request.headers.host) != get_hostname(socket.request.headers.origin))/,+3 s|^\([^/]\)|// Dokploy: \1|' middlewares/authenticate.js
echo 'Socket.io patches applied successfully!'
volumes:
- frappe-home:/home/frappe
depends_on:
init-bench:
condition: service_completed_successfully
networks:
- frappe-network
restart: "no"
backend:
image: frappe/bench:latest
restart: always
user: frappe
entrypoint: []
environment:
- CHOKIDAR_USEPOLLING=true
command: >
bash -c "
sudo bash -c '
if [ ! -f /tmp/.devtools-installed ]; then
echo \"Installing dev tools...\";
apt-get update;
apt-get install -y zsh curl git;
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg;
echo \"deb [arch=amd64 signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main\" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null;
apt-get update;
apt-get install -y gh;
touch /tmp/.devtools-installed;
echo \"Dev tools installed!\";
fi
';
if [ ! -f /home/frappe/.zshrc ]; then
echo \"Installing oh-my-zsh...\";
sh -c \"\$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)\" \"\" --unattended;
sudo chsh -s \$(which zsh) frappe;
echo \"oh-my-zsh installed!\";
fi;
if [ ! -f /home/frappe/.starship-installed ]; then
echo \"Installing Starship prompt...\";
sudo bash -c 'curl -sS https://starship.rs/install.sh | sh -s -- -y';
echo 'eval \"\$(starship init zsh)\"' >> /home/frappe/.zshrc;
touch /home/frappe/.starship-installed;
echo \"Starship installed!\";
fi;
until [ -f /home/frappe/frappe-bench/sites/common_site_config.json ]; do
echo 'Waiting for initialization...';
sleep 5;
done;
cd /home/frappe/frappe-bench;
source env/bin/activate;
exec bench serve --port 8000;
"
volumes:
- frappe-home:/home/frappe
depends_on:
patch-socketio:
condition: service_completed_successfully
redis-cache:
condition: service_healthy
redis-queue:
condition: service_healthy
redis-socketio:
condition: service_healthy
networks:
- frappe-network
labels:
- "traefik.enable=true"
- "traefik.http.routers.vite.rule=Host(`vite.${SITE_NAME}`)"
- "traefik.http.services.vite.loadbalancer.server.port=8080"
socketio:
image: frappe/bench:latest
restart: always
command: >
bash -c "
until [ -f /home/frappe/frappe-bench/apps/frappe/socketio.js ]; do
echo 'Waiting for bench initialization...';
sleep 5;
done;
cd /home/frappe/frappe-bench;
echo 'Starting Socket.IO server...';
node apps/frappe/socketio.js;
"
volumes:
- frappe-home:/home/frappe
depends_on:
patch-socketio:
condition: service_completed_successfully
redis-socketio:
condition: service_healthy
networks:
- frappe-network
worker-short:
image: frappe/bench:latest
restart: always
command: >
bash -c "
until [ -f /home/frappe/frappe-bench/sites/common_site_config.json ]; do
echo 'Waiting for initialization...';
sleep 5;
done;
cd /home/frappe/frappe-bench;
source env/bin/activate;
exec bench worker --queue short;
"
volumes:
- frappe-home:/home/frappe
depends_on:
patch-socketio:
condition: service_completed_successfully
redis-queue:
condition: service_healthy
networks:
- frappe-network
worker-default:
image: frappe/bench:latest
restart: always
command: >
bash -c "
until [ -f /home/frappe/frappe-bench/sites/common_site_config.json ]; do
echo 'Waiting for initialization...';
sleep 5;
done;
cd /home/frappe/frappe-bench;
source env/bin/activate;
exec bench worker --queue default;
"
volumes:
- frappe-home:/home/frappe
depends_on:
patch-socketio:
condition: service_completed_successfully
redis-queue:
condition: service_healthy
networks:
- frappe-network
worker-long:
image: frappe/bench:latest
restart: always
command: >
bash -c "
until [ -f /home/frappe/frappe-bench/sites/common_site_config.json ]; do
echo 'Waiting for initialization...';
sleep 5;
done;
cd /home/frappe/frappe-bench;
source env/bin/activate;
exec bench worker --queue long;
"
volumes:
- frappe-home:/home/frappe
depends_on:
patch-socketio:
condition: service_completed_successfully
redis-queue:
condition: service_healthy
networks:
- frappe-network
scheduler:
image: frappe/bench:latest
restart: always
command: >
bash -c "
until [ -f /home/frappe/frappe-bench/sites/common_site_config.json ]; do
echo 'Waiting for initialization...';
sleep 5;
done;
cd /home/frappe/frappe-bench;
source env/bin/activate;
exec bench schedule;
"
volumes:
- frappe-home:/home/frappe
depends_on:
patch-socketio:
condition: service_completed_successfully
redis-queue:
condition: service_healthy
networks:
- frappe-network
volumes:
mariadb-data:
redis-cache-data:
redis-queue-data:
redis-socketio-data:
frappe-home:
networks:
frappe-network:
Environment Variables
SITE_NAME=frappe.dev.base-dokploy-domain.com # or whatever domain you added into domains
FRAPPE_BRANCH=develop # or version-15
DB_ROOT_PASSWORD=StrongPassword123
ADMIN_PASSWORD=admin
Domains Configuration
- Main Site
- Service:
backend - Host:
frappe.dev.base-dokploy-domain.com~ or whatever domain (make sure it’s similar to site_name in environment) - Path:
/ - Port:
8000 - Enable SSL
- Service:
- Socket
- Service:
socketio - Host: exactly similar to your main site host.
- Path:
/socket.io - Port:
9000 - Enable SSL
- Service:
- Vite (only needed for custom app frontend development)
- Service:
backend - Host:
vite.frappe.dev.base-dokploy-domain.com - Path:
/ - Port:
8080 - Enable SSL
- Service:
Backend Development
Bench starts automatically on deployment.
Volume frappe-home persists across redeployments and includes database, sites and apps. Install apps normally via bench.
Connecting to container
- SSH to your server
- Attach to the
backendcontainer. - Run
zshfor a better terminal experience. - Setup Github CLI:
gh auth login/gh auth setup-git - Interact with bench directly to create/install apps, migrate, build, etc…
Frontend Development
This setup is also ready to develop custom frontends (react/vue) for your custom apps. Use doppio to setup SPAs and custom desk pages. Then configure hosts and hmr in your vite.config.
CHOKIDAR POLLING was added to detect file changes within docker. However, hot reloads are of-course slower than developing locally.
Remember to run
yarn dev –hostfrom your frontend directory to run vite development and access your vite domain.After building, the built version is accessible on your backend domain.
vite.config Example
export default defineConfig({
plugins: [react(), tailwindcss()],
server: {
port: 8080,
host: '0.0.0.0',
proxy: proxyOptions,
allowedHosts: [
'vite.frappe.dev.base-dokploy-domain.com',
// the vite domain you entered in domain configuration
// or simply
'base-dokploy-domain.com'
],
hmr: {
host: 'vite.frappe.dev.base-dokploy-domain.com',
// the vite domain you entered in domain configuration
protocol: 'wss',
clientPort: 443
}
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
build: {
outDir: '../your_app/public/your_frontend',
emptyOutDir: true,
target: 'es2015',
},
});