[Guide] Development Instances on Dokploy

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

  1. Fill in the compose file.
  2. Fill in your environment variables
  3. Add in your domains
  4. If you are planning to spin up multiple development instances (services) then Enable Isolated Deployment from Advanced tab.
  5. Deploy
  6. From logs wait until init-bench container finishes the automated setup (5-10minutes).
  7. 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_USEPOLLING for 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 domain
      • authenticate.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
  • Socket
    • Service: socketio
    • Host: exactly similar to your main site host.
    • Path: /socket.io
    • Port: 9000
    • Enable SSL
  • Vite (only needed for custom app frontend development)
    • Service: backend
    • Host: vite.frappe.dev.base-dokploy-domain.com
    • Path: /
    • Port: 8080
    • Enable SSL

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 backend container.
  • Run zsh for 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 –host from 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',
	},
});
3 Likes

Thank you for your awesome work! :clinking_beer_mugs: