Skip to content

Install on a VM

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

Install on a VM (no Docker)

A standard Laravel deployment: nginx + php-fpm + MySQL. There is no installer and no frontend build step — the assets are precompiled and committed.

Examples below assume Ubuntu 22.04/24.04 and /opt/cortendesk. Adjust paths and package names for your distribution.

Requirements

  • PHP 8.3 or newer (8.4 recommended) with pdo_mysql, mbstring, intl, bcmath, xml, curl, zip, gd, opcache
  • Composer
  • MySQL 8 or MariaDB 10.6+ (SQLite works for evaluation only)
  • nginx (or any Laravel-capable web server)
  • A running open-source RustDesk server (hbbs/hbbr) reachable from your devices
  • For the in-browser client: TLS, and a proxy that bridges wss:// to the hbbs/hbbr WebSocket ports 21118 and 21119
sudo apt update
sudo apt install -y nginx mysql-server git unzip \
  php8.4-fpm php8.4-mysql php8.4-mbstring php8.4-intl php8.4-bcmath \
  php8.4-xml php8.4-curl php8.4-zip php8.4-gd

If your distribution ships an older PHP, add a PPA (Ubuntu: ondrej/php) or use the Docker image instead.

The RustDesk server is a prerequisite, not part of this install. If you do not have one, or yours is only half-working, see RustDesk server in Docker — hbbs and hbbr from scratch, every port, and where the key comes from.

1. Database

CREATE DATABASE cortendesk CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'cortendesk'@'localhost' IDENTIFIED BY 'use-a-real-password';
GRANT ALL PRIVILEGES ON cortendesk.* TO 'cortendesk'@'localhost';
FLUSH PRIVILEGES;

2. Get the code

sudo mkdir -p /opt/cortendesk
sudo chown "$USER" /opt/cortendesk
git clone https://github.com/marcpope/cortendesk.git /opt/cortendesk
cd /opt/cortendesk
composer install --no-dev --optimize-autoloader

main always equals the latest release. To pin a version:

git checkout v0.9.0

3. Configure

cp .env.example .env
php artisan key:generate

Edit .env:

APP_NAME=CortenDesk
APP_ENV=production
APP_DEBUG=false
APP_URL=https://console.example.com

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=cortendesk
DB_USERNAME=cortendesk
DB_PASSWORD=use-a-real-password

# Your RustDesk server
CORTENDESK_ID_SERVER=hbbs.example.com:21116
CORTENDESK_RELAY_SERVER=hbbs.example.com:21117
CORTENDESK_PUBLIC_KEY=<contents of id_ed25519.pub>

# Native web client — wss endpoints your proxy exposes
CORTENDESK_NATIVE_WEBCLIENT=true
CORTENDESK_WS_ID_URL=wss://console.example.com/ws/id
CORTENDESK_WS_RELAY_URL=wss://console.example.com/ws/relay

# Required when served over HTTPS
SESSION_SECURE_COOKIE=true

APP_KEY encrypts the SMTP password, the OIDC client secret and 2FA secrets. Back it up with the database — one without the other is not a usable restore.

4. Migrate and cache

php artisan migrate --seed
php artisan config:cache
php artisan route:cache
php artisan view:cache

--seed creates admin / changeme. Change it at first sign-in.

5. Permissions

The web server user needs to write two directories and nothing else:

sudo chown -R www-data:www-data /opt/cortendesk/storage /opt/cortendesk/bootstrap/cache
sudo find /opt/cortendesk/storage -type d -exec chmod 775 {} \;

If you run artisan by hand afterwards, run it as www-data, or it will leave root-owned cache files that php-fpm cannot write:

sudo -u www-data php artisan config:cache

6. nginx

server {
    listen 443 ssl http2;
    server_name console.example.com;

    ssl_certificate     /etc/letsencrypt/live/console.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/console.example.com/privkey.pem;

    root /opt/cortendesk/public;
    index index.php;

    client_max_body_size 64m;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.4-fpm.sock;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }

    # Web client: bridge wss to the RustDesk server. Browsers refuse ws:// from
    # an HTTPS page, which is the whole reason these two blocks exist.
    location = /ws/id {
        proxy_pass http://127.0.0.1:21118/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
    }

    location = /ws/relay {
        proxy_pass http://127.0.0.1:21119/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
    }
}

server {
    listen 80;
    server_name console.example.com;
    return 301 https://$host$request_uri;
}

Point proxy_pass at wherever hbbs/hbbr actually listen — 127.0.0.1 only if they run on this same VM.

sudo nginx -t && sudo systemctl reload nginx

Skip the two /ws/ blocks if you are not using the in-browser client. The console itself works without them.

7. Scheduler

Log retention, pruning and other maintenance run through Laravel's scheduler. Without this they never run.

sudo crontab -u www-data -e
* * * * * cd /opt/cortendesk && php artisan schedule:run >> /dev/null 2>&1

8. Point your devices at it

On each RustDesk client, Settings → Network: ID Server, Relay Server, Key, and API Server = https://console.example.com. Devices appear within about fifteen seconds. The console's Settings screen shows copy-paste values for all four fields.

For unattended deployment, create an API token under Settings → Security and use the client's --assign flag.

Behind a reverse proxy

If something else terminates TLS and forwards to this VM over plain HTTP, the app honours X-Forwarded-* headers already. Set APP_URL to the public HTTPS URL and SESSION_SECURE_COOKIE=true. If your proxy sits outside the private address ranges, set TRUSTED_PROXIES in .env to its address — see Reverse proxy and TLS.

Upgrading

cd /opt/cortendesk
sudo -u www-data git pull
sudo -u www-data composer install --no-dev --optimize-autoloader
sudo -u www-data php artisan migrate --force
sudo -u www-data php artisan optimize:clear
sudo -u www-data php artisan config:cache
sudo -u www-data php artisan route:cache
sudo -u www-data php artisan view:cache

Back up the database first. See How to Upgrade.

Backups

mysqldump -u cortendesk -p cortendesk > cortendesk-$(date +%F).sql
cp /opt/cortendesk/.env /somewhere/safe/     # holds APP_KEY

A database backup without APP_KEY cannot decrypt the SMTP password, the OIDC client secret, or anyone's 2FA secret. Keep both.

Migrating from lejianwen/rustdesk-api

php artisan cortendesk:import-lejianwen /path/to/rustdeskapi.db --dry-run
php artisan cortendesk:import-lejianwen /path/to/rustdeskapi.db

Users keep their original password hashes, devices are deduplicated, and address books, share rules and audit history come across. Address-book entry passwords encrypted by the Go API cannot be decrypted and must be re-saved by users.

Troubleshooting

500 on every page. Almost always permissions. Check storage/logs/laravel.log, then confirm storage and bootstrap/cache are owned by www-data.

Changes to .env do nothing. The config is cached. Run sudo -u www-data php artisan config:cache after every edit.

No devices appear. The clients' API Server is not pointing here. Confirm the value in the client, and that this host is reachable from the device.

Web client will not connect. The /ws/ blocks are missing, point at the wrong host, or CORTENDESK_WS_*_URL is not the wss:// URL the browser sees.

Sign-in loops back to the login page. SESSION_SECURE_COOKIE=true without HTTPS, or an APP_URL that does not match the address in the browser.

Locked out by SSO or 2FA. Set CORTENDESK_OIDC_DISABLED=true in .env, or run sudo -u www-data php artisan cortendesk:2fa-reset <username>.

Clone this wiki locally