Skip to content

Install with Docker

Marc Pope edited this page Oct 12, 2026 · 13 revisions

Install with Docker

The image is self-contained: the ID server (hbbs), the relay (hbbr), php-fpm, nginx, the scheduler, the native web client, and the WebSocket bridge between them. The console listens on 8080 (CORTENDESK_HTTP_PORT changes it); the servers on the usual 21115–21119.

Images are published on every release:

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

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

Before you start

Since 1.5 the image contains the ID server and the relay, so there is nothing to set up first: APP_URL is the address your clients and browsers reach, and the ID server, relay address and public key are all derived from it on first boot.

Already running hbbs/hbbr? Move them into the container. Mount the old data directory at /data/rustdesk and the key pair, device IDs and peer database carry over; no client needs reconfiguring. The steps are in Upgrading to 1.5.

Running separate servers is no longer the recommended setup. If you have to, CORTENDESK_EMBEDDED_SERVER=false plus the ID server, relay and public key settings in the table below does it. Run those servers on the CortenDesk Server image: against the stock RustDesk images, a client that signs in to the console cannot connect to anything afterwards — see why.

Install (Docker Compose with MySQL)

services:
  cortendesk:
    image: marcpope/cortendesk:1.11.0
    ports:
      - "8080:8080"
      - "21115:21115"
      - "21116:21116"
      - "21116:21116/udp"
      - "21117:21117"
    environment:
      APP_URL: https://console.example.com

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

      # The ID server, relay and key come from APP_URL and the key pair the
      # container generates on first boot. Set these three only when running
      # your own servers, alongside CORTENDESK_EMBEDDED_SERVER: "false".

      # 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
    # MySQL keeps 30 days of binary logs by default. Nothing replicates from a
    # single-server install, and they are the thing most likely to fill a small
    # disk — which stalls every write, not just the logging.
    command: --binlog-expire-logs-seconds=259200
    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

The entrypoint waits for the database, runs migrations, creates the first administrator and rebuilds every cache. 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, or hbbs hands clients a relay address that does not resolve for them and sessions hang. The container warns about this at startup.

MariaDB works the same way: swap the db image and the healthcheck command.

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 follows the request Leave unset: cookies are Secure on HTTPS requests and plain HTTP on the LAN still signs in. true refuses sign-in over HTTP.
DB_CONNECTION sqlite Set mysql. The compose file above does. Unset, the container falls back to a SQLite file in /data; see the note at the end.
DB_HOST / DB_DATABASE / DB_USERNAME / DB_PASSWORD — Standard Laravel.
CORTENDESK_EMBEDDED_SERVER true Runs hbbs and hbbr in this container. false to use servers you run yourself.
CORTENDESK_ID_SERVER <APP_URL host>:21116 host:21116. Also the default host for the ws bridge.
CORTENDESK_RELAY_SERVER <APP_URL host>:21117 host:21117. What hbbs tells clients about the relay.
CORTENDESK_PUBLIC_KEY from the key pair Contents of id_ed25519.pub. Read from /data/rustdesk when the servers are embedded.
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_DOWNLOADS_MAX_MB 512 Largest client installer you can upload. Applied to nginx and PHP at boot.
CORTENDESK_SERVER_SECRET generated Links the console and the ID server for server access control. Set it only when hbbs runs separately.
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.
CORTENDESK_BACKUPS_PATH /data/backups Where Backups writes its archives. Since 1.13.0.
CORTENDESK_RUSTDESK_DIR /data/rustdesk when embedded The ID server's data directory, so a backup carries the key pair and peer database and a restore puts them back. Empty with external servers. Since 1.13.0.
CORTENDESK_HTTP_PORT 8080 Port nginx listens on inside the container. The healthcheck and the built-in hbbs follow it. Publish it with -p <host>:<port>. Not 9000 or 21115–21119; the container refuses to start on those. Since 1.11.0.
CORTENDESK_UPDATE_CHECK true false stops the GitHub release check (every 6 hours) and hides the update badge and status line. Since 1.11.0.
CORTENDESK_LOG_LEVEL unset One level for the console and hbbs/hbbr: debug, info, notice, warning, error or critical. warning and above also drop php-fpm's line per request. See below. Since 1.11.0.
CORTENDESK_FPM_MAX_CHILDREN 24, or less under a memory limit PHP workers. See sizing. Since 1.11.0.
CORTENDESK_NGINX_WORKER_CONNECTIONS 4096 Connections per nginx worker, at least 64. See sizing. Since 1.11.0.
CORTENDESK_NOTIFY_TIMEOUT 15 Seconds (1 to 60) a scheduled notification waits for Apprise. Since 1.11.0.

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

Log levels

CORTENDESK_LOG_LEVEL sets Laravel's LOG_LEVEL and the RUST_LOG that hbbs and hbbr read. LOG_LEVEL or RUST_LOG set on their own win over it.

If you set RUST_LOG directly, set it on the container (-e or Compose environment:), and use a level the servers know: off, error, warn, info, debug or trace. Laravel's warning is not one of them, and an unknown word silences hbbs and hbbr. RUST_LOG also takes the filter syntax, such as RUST_LOG=info,hbbs=debug,hbbr=debug. A plain RUST_LOG=debug also logs the HTTP client hbbs uses to poll the console every few seconds.

What happens on first boot

  1. Generates the server key pair into /data/rustdesk if there is not one already, and derives the ID server, relay address and public key from APP_URL.
  2. Generates APP_KEY into /data if you did not supply one.
  3. Waits for the database, then runs migrations.
  4. Creates the admin account — a no-op once any user exists.
  5. Sizes php-fpm and nginx (see sizing) and renders the nginx config, pointing /ws/id and /ws/relay at the ID server and relay over loopback.
  6. Starts hbbs, hbbr, php-fpm, nginx, and the Laravel scheduler.

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

The image's healthcheck calls /health/live. Point uptime monitors and load balancers at /health/ready instead. See Health checks.

Sizing for larger fleets

Since 1.11.0. Before that the image ran 5 PHP workers and 1,024 nginx connections, which took the console and client API down at around 700 devices.

Every heartbeat, inventory upload and console page takes a PHP worker while it runs. The image starts up to 24 workers. Under a container memory limit it starts fewer, about one per 80 MB of the limit and never fewer than 5. A worker uses about 35 MB, most of it shared. If the log shows server reached pm.max_children setting, raise CORTENDESK_FPM_MAX_CHILDREN. With MySQL, keep it below the database's max_connections.

nginx runs one worker per CPU with 4,096 connections each. Each proxied request holds two open files, so the container needs a high open-file limit. Many Docker hosts start containers at 1,024. The container raises its own limit as far as the host's hard limit allows (up to 65,536), and logs a warning when it is still under 4,096:

[cortendesk] WARNING: only 1024 open files allowed (hard limit 1024).
[cortendesk]          Larger fleets need more: docker run --ulimit nofile=65536:65536

If you see it, start the container with --ulimit nofile=65536:65536, or in Compose:

services:
  cortendesk:
    ulimits:
      nofile: 65536

The startup log shows what was applied:

[cortendesk] php-fpm 24 workers, nginx 4096 connections per worker, 65536 open files

A reverse proxy in front has its own connection, open-file and timeout limits. Raise those too, or it becomes the bottleneck.

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. The session cookie is marked Secure on HTTPS requests on its own.

The in-browser client needs wss://. The container bridges /ws/id and /ws/relay to the ID server and relay over loopback, so your proxy only has to forward port 8080 with WebSocket upgrade headers. 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, and /data/rustdesk holds the server key pair. Lose the app key and encrypted values (SMTP password, OIDC client secret, 2FA secrets) cannot be decrypted even with a good database backup; lose id_ed25519 and every existing client is locked out of connecting, with no way to accept both an old and a new key.
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.

Console link: policy pull failed ... Connection refused at start. Harmless when Console link: reachable again follows a few seconds later: hbbs started before the console was answering. See Troubleshooting.

Console slow or timing out with many devices. See sizing.

Sign-in redirects back to the login page over HTTPS. Make sure APP_URL uses https:// and your proxy sends X-Forwarded-Proto.

419 Page Expired when signing in over HTTP. SESSION_SECURE_COOKIE=true makes the browser drop the session cookie on plain HTTP. Remove it, or use the https:// address. The login page shows a warning when this applies.

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>.

SQLite

With DB_CONNECTION unset, the container uses a SQLite file in the /data volume. It exists so the image starts with no database at all, which is convenient for a throwaway look at the UI. It is not a supported setup: SQLite commits one write at a time, every heartbeat is a write, and nothing here is tuned or tested for it beyond the test suite. Use MySQL or MariaDB.

Clone this wiki locally