Finestra

A window into your servers.

Finestra is Italian for window. A desktop environment that runs in your browser and drives a headless Linux server: windows, a taskbar, a terminal on a real PTY, a file manager, a system manager — and real Linux GUI applications, drawn on the server and rendered in a window.

A browser showing Finestra on a headless server: the System Manager with live CPU, memory and network graphs, a terminal running neofetch on Ubuntu on a t3.medium, and GNOME Disks — a real Linux application — in its own window.
A headless EC2 instance, in a browser tab: the system manager, a terminal on a real PTY, and GNOME Disks — an ordinary Linux application, drawn on the server.

Install

On the server, over SSH:

$ curl -fsSL BASE/get.sh | sudo bash

That downloads the current release, checks it against its published SHA-256, and runs the installer inside it. No toolchain, no npm, no third-party apt repository — the release carries its own Node runtime.

It asks one question: who the desktop runs as. The default is you — your home directory, the journal, and sudo, which is the same reach as the SSH session you are typing it in and nothing more. The alternatives are the same account without privilege, or a system account with no home at all: read-only observability. Answer it in advance if you are not there to type:

curl -fsSL BASE/get.sh | sudo bash -s -- --system-account

The answer is recorded and survives upgrades, and it can be changed later — see Afterwards.

Ubuntu 24.04 or newer, x86_64. The native pieces are built against 24.04's glibc; the installer refuses an older machine rather than failing later and less clearly.
Want native Linux applications? They need two shared libraries that most server images do not carry, and the installer deliberately does not fetch them for you: nothing else in the desktop links them, so a machine that will only ever use the terminal, the files and the system tools does not acquire a Wayland library because a desktop was installed on it. Everything else works without them. Install them whenever you want the feature — sudo apt install libwayland-server0 libxkbcommon0 on Debian and Ubuntu, sudo dnf install libwayland-server libxkbcommon on Fedora and RHEL — and the applications window picks them up the moment they appear, with no restart. It also prints the right command for your distribution if you would rather be told.
Already running web desktop? Run the same one-liner. It takes the old install over: your access token, your settings and your answer to the who-runs-as question all survive, so the URL you have bookmarked keeps working. The old service, /opt/web-desktop and the web-desktop system account are removed, and the installer prints exactly what it took over and what it removed. If the new service does not come up, the old one is put back untouched.

Connect

It listens on 127.0.0.1:7070 and nothing else, so there is no port to open. Forward it over the SSH you already have:

ssh -L 7070:127.0.0.1:7070 you@server

Then open the URL the installer printed — it carries the access token. That tunnel is the authentication boundary: reaching the desktop means you already got onto the machine with a key you own, and the token is a second latch on top of it.

What you get

Terminal

A real PTY on the host. Reopens where you left it, and only asks before closing when something is running.

Files

Browse, rename, upload, download. Double-click opens whichever app handles that file.

System Manager

Processes, systemd units, the journal, disks, listening ports and certificates — live, from /proc and journalctl.

Native applications*

Real Linux GUI apps in a window, through a Wayland compositor written for this.

Several machines

One shell, one connection per server, straight from your browser. The servers never learn about each other.

Session restore

Windows come back after a reload — geometry, stacking, focus, and where each app was.

* Applications installed from a distribution package are what this is tested against. Snaps are experimental: simple ones run, more complex ones do not yet — a snap draws through the same compositor, but it also brings its own confinement, its own runtime and its own expectations about the desktop session around it. Treat a snap that does not work as expected rather than as a surprise, and prefer the .deb where a package offers both. What is known, and why, is written down in docs/wayland.md.

What changed

Every release, newest first — what now works that did not. Rendered from CHANGELOG.md, so the page and the repository cannot disagree.

0.3.72026-09-07
  • A terminal survives losing its browser. Close the tab, shut the laptop, drop off the VPN: the shell keeps running and the terminal that comes back — the same page reconnecting, or a reload restoring the session — picks it up where it was, showing what it missed. Before, every disconnect hung the shell up and killed whatever was in it, the way a dropped SSH session does. A shell nobody comes back for is hung up after an hour; WD_TERMINAL_GRACE changes that, and 0 restores the old behaviour. Closing the window still ends the shell, and so does restarting the service.
  • A terminal can be kept for as long as it takes. *Shell → Keep running while I am away* exempts a shell from that hour — for the training run that finishes tomorrow. Opening the desktop later, from any browser, brings up a terminal onto every shell still running that no window is showing; *Shell → Pick up a terminal* lists the rest, with what each is running and how long it has been alone.
  • The terminal is drawn by xterm.js 6. Under the hood only; it should look and behave exactly as before. If something in the terminal renders differently than it did in 0.3.6, this is the change to suspect. The build tooling moved to TypeScript 7 and Vite 8 at the same time, which changes nothing that ships.
0.3.62026-08-24
  • VS Code — and Electron apps generally — now start. Two faults, either one fatal. VS Code's codium command is a script that starts the editor in the background and exits at once, and the compositor read that exit as "the application closed" — tearing the session down under an editor that was still starting, and calling it a clean exit. Sessions now live as long as something is connected to the display, not as long as the first process. Separately, Electron apps look for X11 and give up on a machine that has none; every launch now carries the environment hint that points them at Wayland instead. No app is named anywhere — anything shaped like either problem is covered.
  • Pasting text copied outside this desktop into the terminal works again. Broken by 0.3.5's double-paste fix, which suppressed the browser's own paste — the only channel outside text has into a plain-http page. If Ctrl+Shift+V answered "there is nothing to paste" to a full clipboard, this is that. The double paste stays fixed.
  • An app that fails right after starting is reported for what it did. "Reached the display and closed before showing anything" now, when that is what happened — previously every silent exit read as the application never arriving, which pointed at the environment when the app itself had decided to stop.
  • Closing a window cannot leave the app running behind it. Applications started through a launcher ended up outside the compositor's reach; closing the session killed the launcher's remains and missed the app. They are now asked to stop by the identity their display connection carries, however they were started.
0.3.52026-08-20
  • Ctrl+Shift+V in the terminal pasted everything twice. New in 0.3.4, and the reason is that the browser reads that combination as its own paste-as-plain-text: the terminal pasted what you asked for, and the browser pasted it again underneath. Once now.
  • Pasting into the terminal is a paste, not typing. Both the desktop clipboard and the browser's own now go through the terminal's paste path, so the text carries the brackets a program asks for when it wants to tell the two apart. In practice that means a block of YAML pasted into vim or nano arrives as it left, rather than auto-indented into a staircase.
  • Switching to the light theme no longer leaves the desktop icons unreadable. The icons sit on the wallpaper rather than on anything the theme controls, so choosing Light over a dark wallpaper printed dark names on a dark picture. Two changes: the theme now moves the wallpaper with it, remembering separately what you chose under each, and icon labels take their colour from the wallpaper instead of the theme — so a light wallpaper under the dark theme, which was equally broken, is readable too. An install already sitting in that state fixes itself when the page reloads.
  • Two more light wallpapers, since "light" used to mean one grey wash. *Linen* is quiet and neutral; *Horizon* is a colourful one in the style current Macs use, kept pale so dark labels stay readable across it.
  • The desktop's right-click menu offered Settings twice. The shortcuts at the top are the first three apps by name, and Settings sorted into them right above the menu's own Settings entry. Apps that only ever have one window are no longer offered as "New …", which is true of Settings and the System Manager both.
0.3.42026-08-20
  • Copy and paste work between the desktop's own windows again. The browser only shares the clipboard with a page served over https, and this desktop is plain http behind an SSH tunnel — so it refused, and every app said so and dropped the text. What was easy to miss is that this took *internal* copying with it: a path copied out of Files and pasted into the terminal, a command line from the process list, a line copied out of a Linux application in the applications window. None of that involves your own machine's clipboard, and all of it was failing.

The desktop now keeps its own clipboard. Every copy is kept there whatever the browser decides, and pasting anywhere in the desktop — the terminal, the editor, a native application, with Ctrl+V, Ctrl+Shift+V, middle-click or a menu — uses it. Nothing to enable, and nothing to change if you already run it behind TLS.

  • Copies now reach the machine your browser is on, too. Over plain http they never did. The desktop falls back to the pre-permissions copy path, which browsers still allow without a secure context, so a path copied in the desktop can be pasted into anything else on your own machine. Pasting *in* from your machine works as it always did, and still wins when you copied there more recently than here.

Where the desktop cannot reach your machine's clipboard at all, it says so once, in one notification, rather than complaining on every copy.

  • Still worth putting TLS in front of this if you can — a secure origin gets the browser's own clipboard with no fallbacks. This release is about the desktop being usable without one.
0.3.32026-08-18
  • It installs on Fedora, Rocky, RHEL and anything else with SELinux enforcing. It did not before, and the way it failed told you almost nothing: the install finished, reported success, and the service then restarted every two seconds with

Failed to locate executable /opt/finestra/current/runtime/bin/node: Permission denied ... status=203/EXEC

about a file that is present, executable, owned by root, and which runs perfectly if you type its path yourself. The cause is that an SELinux label comes from where a file was made rather than where it lives: the tarball is unpacked under /tmp, so everything in it carried a temporary-file type into /opt, and systemd is not permitted to execute that. The installer now runs restorecon on what it wrote.

If you hit this on 0.3.2 there is nothing to undo — install 0.3.3 over the top and it will start.

  • Updates on those systems no longer roll themselves back. update.sh put the new version in place the same way, so an upgrade installed cleanly, switched, failed its own health check and reverted — which looked like a bad release rather than a file label. Note that the updater inside a package is a copy taken when that package was built, so the *upgrade off* 0.3.2 on an enforcing system still uses the old one; installing the 0.3.3 tarball directly is the way through. Every update after this one is fine.
  • Nothing changes on Ubuntu or Debian, which carry no SELinux, or on Amazon Linux 2023, which ships it permissive. All three worked before and work now.
0.3.22026-08-13
  • It can answer on your network, not just through a tunnel. Until now the desktop bound loopback and the only way in was ssh -L. On a network you actually control — a VPN, a tailnet, a LAN behind your own router — that is a tax rather than a boundary, so the installed service can now drop it:

bash sudo /opt/finestra/current/configure.sh --bind 0.0.0.0 # answer everywhere sudo /opt/finestra/current/configure.sh --bind 100.83.0.4 # one address only sudo /opt/finestra/current/configure.sh --bind 0.0.0.0 --no-token # and no login at all sudo /opt/finestra/current/configure.sh --bind local --token # put it back

Neither is refused and neither asks you to confirm: it warns, writes the choice into the unit, and prints what it opened. Both survive upgrades and both show in --show. The same flags work on install.sh and on the one-liner, so a machine can be installed open in the first place.

Nothing changes if you do nothing. Loopback and a token remain the defaults, an existing install keeps binding loopback, and the tunnel works as before. Two things to know if you do open it: it is plain HTTP, so put TLS in front of it before it faces anything untrusted; and the terminal is a real shell, with sudo if you chose the privileged install.

  • Binding one VPN address is tighter and more fragile. That address does not exist until the tunnel is up, and a service that starts first fails with EADDRNOTAVAIL. configure.sh now warns when no interface carries the address yet. For a tailnet, either bind everything and let the firewall decide, or leave it on loopback with tailscale serve in front — that also gets you a real certificate.
  • Updates no longer roll themselves back on an opened install. update.sh and configure.sh both prove the service came up by asking it, and both used to ask loopback. Bound to one address, loopback answers nothing — so a working install reported "the service did not start" and a working update undid itself. Both now ask the address the service is actually on.
  • A hand-written drop-in is still honoured, and now said out loud. If you set WD_HOST in systemd edit before this existed, it still wins — it is merged after the unit. configure.sh now reads back what is really in force and tells you, instead of reporting a bind you are not getting. It never removes the file.
0.3.12026-08-13
  • Windows give a band back. An application's menus now sit behind one ☰ button alongside minimize, maximize and close, instead of in a row of their own — so every window is 25px shorter. It matters most for native applications, which draw their own titlebar inside their window: that used to make three bars stacked above the content, and now makes two. Nothing moved out of reach; the same menus are one press away.
0.3.02026-08-12

Browsers run. Chrome, Chromium and anything built on them now open, draw, take input and browse. Five separate faults were stacked under one symptom ("it worked once and never again"), and all five are fixed:

  • Chromium defaults to the X11 platform and aborted on a machine with no display, before ever reaching Wayland. It is now told which platform to use, in the command line of that one launch — nothing is written to your machine and nothing changes for a desktop you also log into.
  • The compositor advertised a zero-pixel screen when a window was free to pick its own size. GTK applications ignore that; Chrome reads it and dies without a word. Windows that choose their own size now still have a screen to choose against.
  • Every application was started with SIGTERM blocked and with file descriptors it should never have been handed, including a terminal's. Closing an application therefore always killed it outright, however politely we asked.
  • Closing a window now asks the application to close and waits for it to go, instead of killing it and its display at the same instant. Applications that keep a lock file — most browsers — no longer leave one behind and refuse to start next time. A second click on the ✕ still forces a hung one.
  • Reloading the browser tab relaunched an application while the previous copy was still dying, and the new one handed its window to the old one. A launch now waits for the previous copy to be gone.

When something does not start, it says why. The reason was already on the application's own error output and was being thrown away. A window that closes without opening now quotes the application, and the "it is running in your desktop session" explanation is only offered on machines that actually have one — a headless server is no longer told to go and close a window on a screen it does not have.

0.2.32026-08-12
  • Snap applications are marked as such in the applications window, with one note explaining what that means, so the caveat arrives before the surprise.
  • The download page says snaps are experimental, and docs/wayland.md records what is known to work and what is not.
0.2.22026-08-11
  • Snaps start on a packaged install. The session bus they need is now derived from the runtime directory rather than hoped for in the environment, which a systemd unit never has.
  • The text editor no longer draws a focus ring across the top and left of its text area.
  • Publishing refuses to overwrite a version that was already released from a different build.
0.2.12026-08-11
  • The installer no longer fetches two libraries at install time, and says what it does instead.
  • The licence is back in the install tree.
  • The site counts what it served without the product reporting anything.
0.2.02026-08-11

First public release: the desktop, the terminal on a real PTY, the file manager, system and service management, native Linux applications through a Wayland compositor written for this, session restore, and a one-command install.

Afterwards

sudo systemctl status finestra              # is it up
journalctl -u finestra -f                    # follow its logs
sudo /opt/finestra/current/update.sh <tarball>    # upgrade in place
sudo /opt/finestra/current/update.sh --rollback   # undo a bad upgrade

Changing who it runs as

The install keeps the script that asked, so the answer can be changed on a machine that no longer has the release it came from. It rewrites the unit, moves the state to follow the account — carrying the token across, so the URL you already have keeps working — and restarts:

sudo /opt/finestra/current/configure.sh --show   # what it is now
sudo /opt/finestra/current/configure.sh          # ask again
sudo /opt/finestra/current/configure.sh --as-me  # or answer directly

Rather read it first, and build it yourself?

Finestra can do anything you can do on that machine — a root-capable shell, your files, your services. That is the point of it, and it is a good reason not to take anyone's word for what it does. The whole thing is on GitHub: the services are the entire surface a browser can reach, and they are in one directory, server/src/services/.

Read it, then build the same thing rather than downloading it. Needs Node 20 or newer:

sudo apt install -y build-essential pkg-config libwayland-dev libxkbcommon-dev zlib1g-dev
git clone https://github.com/carlosbravoa/finestra.git
cd finestra
npm install && npm run build
make -C compositor            # the Wayland compositor, for native applications
npm start                     # 127.0.0.1:7070, prints a URL with a token

That runs it in the foreground, as you, with no service installed and nothing changed on the machine. If you want it to survive a reboot afterwards, the installer in the tarball is what sets up the systemd unit.

release  VERSION
sha256   SHA
built    BUILT
download the tarball · get.sh