Dokploy Swarm Installation

Dokploy or Docker Swarm can host the MVP as a single-node deployment. Do not scale the VPN service beyond one replica.

Use KINTUNNEL_DRY_RUN=true for MVP evaluation. Non-dry-run reconcile is still conservative and should be treated as host-networking test work, not production WireGuard automation.

The reference Swarm stack is dry-run first. If your Dokploy or Swarm path does not provide /dev/net/tun to the engine container, keep dry-run enabled.

Constraints

  • One active node.
  • One KinTunnel engine replica.
  • One KinTunnel admin replica.
  • UDP 51820 published in host mode.
  • Persistent storage pinned to the same node.
  • Admin UI protected behind HTTPS and access control.

Label the node that owns VPN state:

docker node update --label-add kintunnel.vpn=true <node-name>

Use compose/dokploy-swarm.yml as the reference stack. Swarm does not build images from a stack file, so set KINTUNNEL_ENGINE_IMAGE and KINTUNNEL_ADMIN_IMAGE to images you have already published.

The core shape is:

services:
  engine:
    image: ${KINTUNNEL_ENGINE_IMAGE}
    environment:
      KINTUNNEL_DRY_RUN: ${KINTUNNEL_DRY_RUN:-true}
      KINTUNNEL_ENGINE_API_TOKEN_FILE: /run/secrets/kintunnel_engine_api_token
      KINTUNNEL_ENDPOINT_HOST: ${KINTUNNEL_PUBLIC_ENDPOINT}
      KINTUNNEL_ENDPOINT_PORT: ${KINTUNNEL_WG_PORT:-51820}
      KINTUNNEL_WG_INTERFACE: ${KINTUNNEL_WG_INTERFACE:-wg0}
      KINTUNNEL_WG_LISTEN_PORT: ${KINTUNNEL_WG_PORT:-51820}
      KINTUNNEL_WG_ADDRESS: ${KINTUNNEL_WG_ADDRESS:-10.44.0.1/24}
      KINTUNNEL_DNS_SERVERS: ${KINTUNNEL_WG_DNS:-1.1.1.1}
      KINTUNNEL_ALLOWED_IPS: ${KINTUNNEL_ALLOWED_IPS:-0.0.0.0/0}
      KINTUNNEL_DATA_DIR: /var/lib/kintunnel
    volumes:
      - kintunnel_config:/etc/kintunnel:ro
      - kintunnel_data:/var/lib/kintunnel
      - kintunnel_backups:/backups
    ports:
      - target: ${KINTUNNEL_WG_PORT:-51820}
        published: ${KINTUNNEL_WG_PORT:-51820}
        protocol: udp
        mode: host
    cap_add:
      - NET_ADMIN
    secrets:
      - kintunnel_engine_api_token
    sysctls:
      net.ipv4.ip_forward: "1"
      net.ipv4.conf.all.src_valid_mark: "1"
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.labels.kintunnel.vpn == true
      restart_policy:
        condition: any

  admin:
    image: ${KINTUNNEL_ADMIN_IMAGE}
    environment:
      KINTUNNEL_ADMIN_BIND: 0.0.0.0
      KINTUNNEL_ADMIN_PORT: 8080
      KINTUNNEL_ENGINE_URL: http://engine:9090
      KINTUNNEL_ENGINE_API_TOKEN_FILE: /run/secrets/kintunnel_engine_api_token
      KINTUNNEL_ADMIN_TOKEN_FILE: /run/secrets/kintunnel_admin_token
    secrets:
      - kintunnel_admin_token
      - kintunnel_engine_api_token
    volumes:
      - kintunnel_config:/etc/kintunnel:ro
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.labels.kintunnel.vpn == true
      restart_policy:
        condition: any

volumes:
  kintunnel_config:
  kintunnel_data:
  kintunnel_backups:

secrets:
  kintunnel_admin_token:
    external: true
  kintunnel_engine_api_token:
    external: true

Dokploy Notes

Use Dokploy for lifecycle management and HTTPS routing around the admin UI, not for scaling the VPN data plane.

Practical guidance:

  • Keep the WireGuard UDP port published directly.
  • Keep the admin UI behind Dokploy/Traefik HTTPS.
  • Restrict the admin UI by source IP if Dokploy routing supports it.
  • Back up the named volume before moving nodes or rebuilding the VPS.
  • Treat migration as a maintenance event.

Why Single Node

WireGuard peers and server keys are stateful. UDP port ownership, peer config, and NAT behavior all assume one active server endpoint. Swarm can restart the service, but it should not schedule multiple active copies.