Skip to content

Install with Docker

Marc Pope edited this page Jul 26, 2026 · 5 revisions

Install with Docker

The image is self-contained: php-fpm, nginx, the scheduler, the native web client, and a WebSocket bridge to your RustDesk server. It listens on 8080.

Images are published on every release:

docker pull marcpope/cortendesk:0.9.6
docker pull ghcr.io/marcpope/cortendesk:0.9.6

latest and the <major>.<minor> tag (e.g. 0.9) move only on stable releases, never on a pre-release.

Before you start

You need a running open-source RustDesk server (hbbs/hbbr) and three values from it:

Value Where it comes from
ID server hbbs.example.com:21116
Relay server hbbs.example.com:21117
Public key contents of id_ed25519.pub on the server

CortenDesk does not replace hbbs/hbbr. It runs alongside them and speaks the same HTTP API the RustDesk clients already use.

Do not have one yet, or not sure yours is set up correctly? RustDesk server in Docker builds both services from scratch and covers the three things that usually go wrong: the key, UDP 21116, and an unreachable relay.

Quick trial (SQLite, no database to set up)

docker run -d --name cortendesk \
  -p 8080:8080 \
  -v cortendesk-data:/data \
  -e CORTENDESK_ID_SERVER=hbbs.example.com:21116 \
  -e CORTENDESK_RELAY_SERVER=hbbs.example.com:21117 \
  -e CORTENDESK_PUBLIC_KEY="<contents of id_ed25519.pub>" \
  marcpope/cortendesk:0.9.6

Open http://localhost:8080 and sign in as admin / changeme. Change that password immediately.

Good enough to evaluate. For anything real, use MySQL.

Production (Docker Compose with MySQL)

services:
  cortendesk:
    image: marcpope/cortendesk:0.9.6
    ports:
      - "8080:8080"
    environment:
      APP_URL: https://console.example.com
      SESSION_SECURE_COOKIE: "true"

      DB_CONNECTION: mysql
      DB_HOST: db
      DB_DATABASE: cortendesk
      DB_USERNAME: cortendesk
      DB_PASSWORD: use-a-real-password

      CORTENDESK_ID_SERVER: hbbs.example.com:21116
      CORTENDESK_RELAY_SERVER: hbbs.example.com:21117
      CORTENDESK_PUBLIC_KEY: "<contents of id_ed25519.pub>"

      # Web client endpoints as the BROWSER sees them (your TLS proxy):
      CORTENDESK_NATIVE_WEBCLIENT: "true"
      CORTENDESK_WS_ID_URL: wss://console.example.com/ws/id
      CORTENDESK_WS_RELAY_URL: wss://console.example.com/ws/relay
    volumes:
      - cortendesk-data:/data
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: mysql:8.4
    environment:
      MYSQL_DATABASE: cortendesk
      MYSQL_USER: cortendesk
      MYSQL_PASSWORD: use-a-real-password
      MYSQL_RANDOM_ROOT_PASSWORD: "1"
    volumes:
      - cortendesk-db:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"]
      interval: 5s
      timeout: 3s
      retries: 20
    restart: unless-stopped

volumes:
  cortendesk-data:
  cortendesk-db:
docker compose up -d
docker compose logs -f cortendesk

Environment variables

Variable Default Notes
APP_URL http://localhost:8080 Your public URL. Generated links and the OIDC callback depend on it.
APP_KEY generated Left unset, one is generated on first boot and kept in /data/.app_key. Keep that volume.
SESSION_SECURE_COOKIE false Set true when served over HTTPS.
DB_CONNECTION sqlite mysql for anything real.
DB_HOST / DB_DATABASE / DB_USERNAME / DB_PASSWORD Standard Laravel.
CORTENDESK_ID_SERVER host:21116. Also the default host for the ws bridge.
CORTENDESK_RELAY_SERVER host:21117.
CORTENDESK_PUBLIC_KEY Contents of id_ed25519.pub.
CORTENDESK_NATIVE_WEBCLIENT true Enables the in-browser client. Set false to hide it.
CORTENDESK_WS_ID_URL / CORTENDESK_WS_RELAY_URL wss:// URLs your proxy exposes. Browsers refuse ws:// from an HTTPS page.
RUSTDESK_WS_HOST host of CORTENDESK_ID_SERVER Override only if hbbs/hbbr are reachable at a different address from inside the container.
CORTENDESK_ADMIN_USER admin First-boot account only.
CORTENDESK_ADMIN_PASSWORD changeme First-boot account only.
TRUSTED_PROXIES private ranges Override if your proxy sits outside RFC1918.

Most settings can also be changed later in Settings without restarting.

What happens on first boot

  1. Generates APP_KEY into /data if you did not supply one.
  2. Waits for the database, then runs migrations.
  3. Creates the admin account — a no-op once any user exists.
  4. Renders the nginx config, pointing /ws/id and /ws/relay at your RustDesk server.
  5. Starts php-fpm, nginx, and the Laravel scheduler.

Migrations run on every start, so upgrading is just a new image tag.

TLS and the web client

Put a TLS-terminating proxy in front (Traefik, Caddy, nginx, Cloudflare). The app honours X-Forwarded-*, so no extra configuration is needed beyond setting APP_URL and SESSION_SECURE_COOKIE=true.

The in-browser client needs wss://. The container already bridges /ws/id and /ws/relay to hbbs/hbbr — your proxy just has to forward those two paths with WebSocket upgrade headers, and CORTENDESK_WS_*_URL must be the wss:// URLs as the browser sees them, not the container's internal address.

Caddy, which needs nothing else:

console.example.com {
    reverse_proxy localhost:8080
}

Traefik labels, an nginx server block, and what TRUSTED_PROXIES is actually for: Reverse proxy and TLS.

Pointing devices at the console

On each RustDesk client, Settings → Network: set ID Server, Relay Server, Key, and API Server to your console URL. Devices appear within one heartbeat (about fifteen seconds). The console's Settings screen shows copy-paste values for all four.

For unattended deployment, use an API token and the client's --assign flag.

Upgrading

docker compose pull
docker compose up -d

Migrations run automatically on start. Back up the database first — see How to Upgrade.

Backups

Two things matter:

  • The database — everything except the app key.
  • /data — holds the generated APP_KEY. Lose it and encrypted values (SMTP password, OIDC client secret, 2FA secrets) cannot be decrypted, even with a good database backup.
docker compose exec db mysqldump -u cortendesk -p cortendesk > cortendesk.sql
docker run --rm -v cortendesk-data:/data -v "$PWD":/backup alpine \
  tar czf /backup/cortendesk-data.tar.gz /data

Troubleshooting

No devices appear. The clients' API Server is not set to the console, or they cannot reach it. Check Logs → Connections and the container log.

Web client will not connect. CORTENDESK_WS_*_URL must be wss:// and must be reachable from the browser. Check the browser console for a blocked mixed-content or upgrade error.

"Please provide a valid cache path" or permission errors on start. The /data volume is not writable. Recreate it rather than chowning by hand.

Sign-in redirects back to the login page over HTTPS. Set SESSION_SECURE_COOKIE=true and make sure APP_URL uses https://.

Locked out of SSO or 2FA. Break-glass paths exist: CORTENDESK_OIDC_DISABLED=true in the environment, and docker compose exec cortendesk php artisan cortendesk:2fa-reset <username>.

Clone this wiki locally