Repository navigation
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.0latest and the <major>.<minor> tag (e.g. 0.9) move only on stable
releases, never on a pre-release.
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.
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 cortendeskThe 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.
| 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.
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.
- Generates the server key pair into
/data/rustdeskif there is not one already, and derives the ID server, relay address and public key fromAPP_URL. - Generates
APP_KEYinto/dataif you did not supply one. - Waits for the database, then runs migrations.
- Creates the admin account — a no-op once any user exists.
- Sizes php-fpm and nginx (see sizing) and
renders the nginx config, pointing
/ws/idand/ws/relayat the ID server and relay over loopback. - 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.
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: 65536The 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.
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.
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.
docker compose pull
docker compose up -dMigrations run automatically on start. Back up the database first — see How to Upgrade.
Two things matter:
- The database — everything except the app key.
-
/data— holds the generatedAPP_KEY, and/data/rustdeskholds 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; loseid_ed25519and 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 /dataNo 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>.
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.