A Wayland desktop per account on your server, over RDP.
One apt install on Debian 13 or Ubuntu 26.04 (amd64 or arm64). Every account that logs in over RDP gets its own headless sway session with a shell, kept alive across disconnects, with memory ceilings and a refusal when the machine is full. H.264 over RDP, no GPU needed. Free software, early. Tested with FreeRDP and the Windows App on macOS. Source on GitHub.
$ curl -fsSLO https://waydesk.dev/apt/waydesk-repo.deb
$ sudo apt install ./waydesk-repo.deb
$ sudo apt install waydesk
$ sudo waydesk-setup-admin
$ sudo waydesk-admin status
Memory 7.6 GB in all, 504 MB available
Swap zram0 7.6 GB (7.5 GB used)
Load 1.01 1.26 1.29 (4 cores)
Sessions 7 running; a new one would be REFUSED: 504 MB available, 1161 MB needed
ACCOUNT SESSION MEMORY SWAP CPU LIMIT PROGRAMS (resident)
u1 running 416 MB 1.0 GB 3% 3.8 GB code 194 MB, qs 132 MB, chrome 120 MB
u2 running 472 MB 1.0 GB 4% 3.8 GB code 263 MB, qs 133 MB, chrome 126 MB
u3 running 438 MB 1.1 GB 3% 3.8 GB code 186 MB, chrome 176 MB, qs 134 MB
u4 running 455 MB 1.1 GB 3% 3.8 GB chrome 219 MB, code 206 MB, qs 138 MB
u5 running 477 MB 1.0 GB 4% 3.8 GB chrome 277 MB, code 252 MB, qs 138 MB
u6 running 858 MB 672 MB 7% 3.8 GB code 529 MB, chrome 437 MB, qs 173 MB
u7 running 1.5 GB 95 MB 4% 3.8 GB code 862 MB, chrome 793 MB, qs 265 MB
Real output: Ubuntu 26.04, 4 vCPU, 7.6 GB, seven accounts each with Chromium, LibreOffice Calc and VS Code open, 2026-10-24. Accounts that were idle for a while have most of their memory in compressed swap, which is why the eighth one is refused only now.
Install
Run as an administrator of the server. The port listens on 127.0.0.1 only; see reaching it.
1. Add the repository
One small package: the signing key, the apt source for your release (trixie or resolute, chosen from VERSION_CODENAME) and a pin so these packages win over third-party builds of the same names.
$ curl -fsSLO https://waydesk.dev/apt/waydesk-repo.deb
$ sudo apt install ./waydesk-repo.deb
2. Check the key
This first download cannot be verified by the repository itself. Compare the key you got with this fingerprint (ed25519, expires 2029-10-21):
$ gpg --show-keys --with-fingerprint /usr/share/keyrings/waydesk-archive-keyring.gpg
If it differs, stop and tell us.
3. Install
$ sudo apt update
$ sudo apt install waydesk
Installs the RDP daemon, the root helper, waydesk-admin, our sway build, zram swap and the desktop package. waydesk-server alone is enough for a session with a plain terminal (a minimal sway config ships with it).
4. Shell and first administrator
$ sudo waydesk-setup # wizard: shell, Chromium, LibreOffice, VS Code, ...
$ sudo waydesk-setup-admin # the account that logs in over RDP and manages the others
There is no default account and no default password. waydesk-setup-admin prints the certificate fingerprint to expect on the first connection.
Support
| Distribution | amd64 | arm64 | Notes |
|---|---|---|---|
| Debian 13 (trixie) | tested | tested | clean install from the published repository, RDP session with the shell, Chromium, LibreOffice, VS Code |
| Ubuntu 26.04 LTS (resolute) | tested | tested | sway, quickshell and pcmanfm-qt are our builds under /usr/lib/waydesk; Chromium and Firefox come from Flathub (no Chromium deb) |
| Ubuntu 24.04, Fedora, Arch | not yet | different Qt and wlroots; no rpm yet | |
"Tested": a clean server, SSH hardened first, then exactly the four steps above, an RDP login from FreeRDP and a look at the session. Ubuntu's own xfreerdp3 (3.32) cannot decode our H.264 stream; Debian 13's client (3.15) can. The Windows App on macOS was also used; Microsoft Remote Desktop and mstsc on Windows were not tested.
Architecture
| Part | Runs as | Does | Never does |
|---|---|---|---|
waydesk | waydesk | TLS, RDP protocol, channel framing, rate limits per address | touch user files, call PAM, keep passwords after the check |
waydesk-helper | root | checks the password through PAM, starts and finds sessions, admits or refuses by memory | parse RDP or TLS, listen on the network |
waydesk-session@UID | the account | its sway, capture, encoder, input, audio, clipboard, hooks | anything as another user |
A session is a systemd unit of the account, so it survives a disconnect, sits in user-UID.slice (limits there apply) and is killed with its control group. A second login from the same account takes the running session over. Design notes: docs/MULTIUSER-DESIGN.md in the repository.
Measurements
All figures are from our own servers (Hetzner cloud, no GPU), with the method next to them. Full tables and the failed attempts are in docs/MEASUREMENTS.md in the repository.
Heavy users on 8 GB, 4 vCPU (x86_64)
Available memory (free's figure) after each user has logged in over RDP and opened Chromium (Wikipedia, BBC News, debian.org), LibreOffice Calc with a 300,000 x 10 CSV and VS Code on a source tree.
| users | Debian 13 | Ubuntu 26.04 |
|---|---|---|
| 1 | 5.4 GB | 5.2 GB |
| 2 | 3.9 GB | 3.4 GB |
| 3 | 2.4 GB | 2.1 GB |
| 4 | 1.6 GB | 2.2 GB |
| 5 | 1.7 GB | 2.0 GB |
| 6 | 1.6 GB | 1.7 GB |
| 7 | 1.6 GB | 810 MB, next refused |
| 8 | 1.1 GB | refused |
| 9 | refused |
Method: one account at a time, 70 s after its last application started, applications left idle (an upper bound for idle load, no figure for active work). The server refuses a session when less than about 1.1 GB is available; the refusal reaches the client as ERRINFO_OUT_OF_MEMORY. No out-of-memory kill in any run; memory pressure ("some", 60 s average) 0.3 to 2.4 %. One heavy user costs 1.85 GB (Debian) to 2.0 GB (Ubuntu, Flatpak builds of Chromium and VS Code, a 30 MB tree instead of 25 MB).
Compressed swap
zram with zstd compresses this load about 4.5 to 5 : 1 (Ubuntu: 6.6 GB of data in 1.3 GB of RAM at seven users). The package sizes the device like the RAM (at most 16 GiB). With Debian's default of half the RAM, six heavy users fit instead of eight.
Other figures
| What | Result | Method |
|---|---|---|
| idle session with the shell | 430 to 600 MB | cgroup of the account, Debian 13 arm64 and Ubuntu 26.04 x86, nothing else open |
| browser users on arm64, 3.7 GB | 4 fit, the 5th refused (754 MB available, 900 MB needed) | Chromium, same three pages, one account at a time |
| moving screens, 2 vCPU | two busy screens use about 90 % of the CPU, three drop to a choppy frame rate | video in a window, software H.264 (no hardware encoder) |
| stalled client | connection closed 80 s after the freeze (TCP_USER_TIMEOUT 60 s) | client frozen but answering zero-window probes, Debian 13 arm64 |
Not measured: CPU under active work (a build, a recalculation), typing and scrolling latency, frame rate on x86 with moving screens.
Security
Reaching it
Do not put the RDP port on the internet. The daemon binds 127.0.0.1:3389 and the login is the account's own password, checked by PAM, sent inside TLS with a self-signed certificate (PAM cannot do NLA). Anyone who can intercept an unchecked certificate can read passwords, so check the fingerprint on first use.
$ ssh -N -L 13389:127.0.0.1:3389 you@server # then connect the client to localhost:13389
For several people: WireGuard, a key per device, RDP bound to the VPN address, only UDP 51820 open. docs/WIREGUARD.md has the configuration (installed as /usr/share/doc/waydesk-server/WIREGUARD.md).
Threat model
- Attacker on the network: sees only TLS. Can guess passwords; failures are counted per account and per address, delayed, and answered identically for unknown and known accounts.
- Compromised daemon: runs as an unprivileged user with a system call filter and a narrow capability set. It can read the streams of live sessions and type into them, and sees passwords in transit. It cannot start a session for an account that did not authenticate on the same connection, and the helper takes no program, path or arguments from it.
- Malicious account: has its own sway, its own runtime directory (0700) and its own clipboard; it cannot capture or inject into another account's session. Memory, tasks and CPU ceilings apply per account.
- Out of scope: drive, printer and smartcard redirection, RD Gateway, running exposed to the internet, GPU sharing.
Open items
- Plain TLS login (above); a second factor is not available.
- The helper trusts the daemon's claim of the client's source address, so a compromised daemon can poison the per-address counters (the per-account counter still holds).
- Passwords are not wiped from memory; daemon and helper are one binary.
- Many failed logins from many addresses are logged, not stopped.
pam_faillockin the PAM stack of the service does it, with the trade-off that a stranger can then lock the owner out. - Chromium from Flathub runs with
--password-store=basic: nothing in a Waydesk session unlocks a keyring, so saved passwords and cookies sit under a fixed key, protected by the home directory's permissions only. - No fuzzing of the RDP side, no independent review. Four internal passes are written down in
docs/SECURITY-REVIEW.md; an outside review should start atsrc/helper.rs,src/auth.rs,src/link.rs,src/proto.rsand the unit files.
Limits
- Early software. Tested with FreeRDP and the Windows App on macOS; Microsoft Remote Desktop (Windows) and mstsc were not tested.
- No GPU: applications that insist on one do not start. Zed fails with "Failed to create surface"; VS Code and LibreOffice work.
- Sizing: on an 8 GB, 4-core server count 7 to 8 users with Chromium, Calc and VS Code open if most are idle. Someone compiling or recalculating needs more, and the server refuses a new session instead of slowing everyone down.
- First run of the Flathub Chromium: a seeded
First Run/EULA Acceptedskips its terms window, whose text is only a placeholder in that build.
Reference
Packages
waydeskmetapackage: server and desktop
waydesk-serverdaemon, root helper, waydesk-admin, waydesk-setup-admin, PAM services, units, zram and sysctl drop-ins, minimal sway config
waydesk-desktopsway config, setup wizard (waydesk-setup), per-account shell setup, administrator panels
waydesk-shell, waydesk-quickshellDankMaterialShell 2.0.0 built by us with our patches; quickshell 0.3.0, built per distribution
waydesk-swaysway 1.11 with wlroots 0.19.3 linked statically and the title-bar button patch, under /usr/lib/waydesk/bin; built per distribution (+deb13, +ubuntu26.04)
pcmanfm-qtfile manager with our patches, built per distribution
waydesk-repothe key, the apt source template and the pin
Commands: administration
waydesk-admin status [--json]memory, swap, load, running sessions and what each account uses
waydesk-admin users list [--json]accounts that may log in
waydesk-admin users add NAME [--admin] [--sudo] [--no-rdp]create an account; prints a generated password
waydesk-admin users passwd NAME [--stdin]set a password (read from stdin, never from an argument)
waydesk-admin users remove NAME [--delete-home]remove an account
waydesk-admin users enable NAME | disable NAME [--keep-session]allow or stop RDP logins for an account
waydesk-admin users admin NAME on|offFlatpak installs without a password
waydesk-admin users sudo NAME on|offmembership of the sudo group
waydesk-admin users limit NAME [show | reset | --memory SIZE] [--swap SIZE] [--cpu-weight N] [--tasks N]per-account ceilings
waydesk-admin sessions list [--json] | end NAMErunning sessions; ending one closes that account's desktop
Commands: setup
waydesk-setup [--list | --check]choose and install shell, browsers, office, developer and creative apps
waydesk-setup-admin [--name NAME [--password-stdin]] [--existing NAME]the first administrator: RDP access, panels, Flatpak installs without a password
Units and ports
waydesk.servicethe daemon: TLS, RDP, framing (user waydesk)
waydesk-helper.servicethe root helper: password check, session start, memory admission
waydesk-session@UID.serviceone per account: its sway, capture, encoder and shell
127.0.0.1:3389RDP over TLS; reach it through a tunnel or a VPN
Files
/etc/default/waydeskdaemon and session options
/etc/pam.d/waydesk-serverPAM service for the login check
/etc/pam.d/waydesk-sessionPAM service that gives the session a logind session
/etc/xdg/waydesk/sway.confthe session's sway config (fallback: /usr/share/waydesk/sway-minimal.conf)
/usr/lib/waydesk/binprivate sway, swaymsg and swaybar; first in the session's PATH
/run/waydesk/helper and session sockets
/etc/apt/sources.list.d/waydesk.sourcesthe repository, written for your release
/etc/apt/preferences.d/waydeskthe pin: these packages win over third-party builds of the same names
/usr/lib/systemd/zram-generator.conf.d/50-waydesk.confzram as large as the RAM, at most 16 GiB
Key
Packages and indexes are signed with one ed25519 key, fingerprint C64672B25ECD4773196C39589D8E8874E67617F4, expires 2029-10-21. The public key is /apt/waydesk-archive-keyring.gpg.
Changelog
- 2026-10-24
- Ubuntu 26.04 checked from the repository;
dms.shno longer adds Debian backports when the Waydesk repository is configured (it brokeapt updateon Ubuntu); a client that stops reading is dropped after 60 s;waydesk-serveralone starts a session with a minimal sway config; the Flathub Chromium opens without its terms window and keyring prompt; seven heavy users fit on 8 GB. - 2026-10-23
- Two suites,
trixieandresolute, from one pool; private sway and quickshell under/usr/lib/waydesk; Flathub fallback for browsers; heavy-user capacity on Debian; fix for a publishing tool that deleted live packages. - 2026-10-22
- Signed apt repository,
waydesk-repoandwaydesk-setup-admin; our own build of the shell as a package; refused logins tell the client why. - 2026-10-21
- Memory admission, account ceilings and zram; patched sway with title-bar buttons; dock; Chromium as the default browser; third internal security pass.
- 2026-10-20
- Renamed to Waydesk. Desktop icons, themed Qt and Flatpak apps, logind session per account.
- 2026-10-15 to 19
- RDP server for sway, then multi-user daemon, helper and sessions; DankMaterialShell on sway.