CortenDesk

Install

Up and running

Since 1.5 the image contains the RustDesk server — ID server, relay and console in one container. Nothing to set up first.


Already running hbbs/hbbr?

Move them into the container

Running separate RustDesk servers next to CortenDesk is no longer the recommended setup. The bundled server is CortenDesk Server, our fork of the open-source server with the signalling key exchange that signed-in clients 1.4.1 and newer require. Against stock hbbs, those clients fail with Failed to secure tcp.

Migrating keeps your key pair, device IDs and peer database, so no client needs reconfiguring. Four steps:

  1. Find the hbbs data directory, the one holding id_ed25519, id_ed25519.pub and db_v2.sqlite3. Back it up. id_ed25519 cannot be regenerated without locking every client out.
  2. Stop the old hbbs and hbbr containers. They hold the ports CortenDesk is about to take.
  3. Mount that directory at /data/rustdesk in the CortenDesk container, publish the server ports, and remove CORTENDESK_ID_SERVER, CORTENDESK_RELAY_SERVER and CORTENDESK_PUBLIC_KEY. APP_URL now supplies all three.
  4. Start it and watch the log for Private key comes from id_ed25519. That line means your key was adopted. If you see generated the server key pair instead, the mount is wrong. Fix it before any client connects.
services:
  cortendesk:
    image: ghcr.io/marcpope/cortendesk
    environment:
      APP_URL: https://rd.example.com      # the address CLIENTS reach
    ports:
      - "8080:8080"
      - "21115:21115"
      - "21116:21116"
      - "21116:21116/udp"
      - "21117:21117"
    volumes:
      - cortendesk-data:/data
      - /home/you/rustdesk/data:/data/rustdesk   # the old hbbs data

Devices come back online within a heartbeat. The full walkthrough, including rollback, is in Upgrading to 1.5.

Have to keep them separate? Set CORTENDESK_EMBEDDED_SERVER=false and supply CORTENDESK_ID_SERVER, CORTENDESK_RELAY_SERVER and CORTENDESK_PUBLIC_KEY (the contents of id_ed25519.pub, pasted with no surrounding whitespace; a trailing newline fails like a wrong key). Run those servers on the CortenDesk Server image, not the stock one, or signed-in clients cannot connect.

Step 1

Compose, with MySQL

One stack: the console with its bundled ID server and relay, and a MySQL database. MariaDB works the same way.

services:
  cortendesk:
    image: ghcr.io/marcpope/cortendesk
    ports:
      - "8080:8080"
      - "21115:21115"
      - "21116:21116"
      - "21116:21116/udp"
      - "21117:21117"
    environment:
      APP_URL: https://console.example.com   # the address CLIENTS reach
      SESSION_SECURE_COOKIE: "true"

      DB_CONNECTION: mysql
      DB_HOST: db
      DB_DATABASE: cortendesk
      DB_USERNAME: cortendesk
      DB_PASSWORD: use-a-real-password
    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

The entrypoint waits for the database, runs migrations, creates the first administrator and rebuilds every cache on boot. Upgrading is a new image tag and a restart.

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

APP_URL has to be the address your clients reach, not localhost. The ID server, the relay address handed to clients and the public key are all derived from it, and a key pair is generated on first boot.

Keep the /data volume. It holds the generated APP_KEY and the server key pair. Without the first, the encrypted values in your database — SMTP password, SSO client secret, 2FA secrets — cannot be decrypted, even from a good backup. Without the second, every client is locked out.

Step 2

Put TLS in front

The container serves plain HTTP on 8080 and expects a TLS-terminating proxy. With Caddy that is three lines:

console.example.com {
    reverse_proxy localhost:8080
}

Set APP_URL to the public HTTPS address and SESSION_SECURE_COOKIE=true. Traefik and nginx examples, plus the trusted-proxy settings that keep client IPs accurate, are in the reverse proxy guide.


Step 3

Point your clients at it

In a normal RustDesk client: Settings → Network. Set ID Server, Relay Server, Key, and set API Server to your console URL. The console's Settings screen shows all four ready to copy, including the generated key.

Devices appear within one heartbeat, about fifteen seconds.

For unattended rollout, issue an API token and use the client's --assign flag to place a machine in the right folder, with the right owner and policy, on first run.

Prefer to install it without Docker?

It is a standard Laravel deployment — PHP 8.4, MySQL or MariaDB, nginx and php-fpm.