Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Headless Pi Linux Desktop

This project runs a complete XFCE Linux desktop in an ARM64 Docker container on a headless Raspberry Pi OS host. The desktop is streamed to any modern browser, so the Pi does not need a monitor, keyboard, or desktop OS installation.

The target hardware is a Raspberry Pi 5 with 8 GB RAM. The host should be Raspberry Pi OS Lite 64-bit. The container is LinuxServer Webtop, using its current Alpine XFCE default image and Selkies browser streaming.

This is a desktop userland sharing the Pi host kernel, not a virtual machine with a separate guest kernel. That is the right trade-off for a lightweight remote desktop; use QEMU/KVM instead if a separate kernel is a hard requirement.

Result

The access path is:

Other device browser
        |
        | HTTPS :3001
        v
Raspberry Pi OS Lite 64-bit
        |
        | Docker user-defined bridge
        v
LinuxServer Webtop: Alpine XFCE

After setup, open:

https://PI_IP_ADDRESS:3001

Use the browser authentication username and password created by scripts/setup.sh. The first visit will show a self-signed certificate warning because direct Webtop HTTPS is intended for trusted LAN or VPN access.

How It Works

The Raspberry Pi is the server. The computer used to connect only needs a modern web browser; it does not need Docker, XFCE, or a Linux installation.

The host is a headless 64-bit ARM Linux system. Docker runs the LinuxServer Webtop container, and the container supplies the desktop userland:

Raspberry Pi host kernel
        |
        v
Docker Engine
        |
        v
LinuxServer Webtop container
  Alpine Linux userland
  XFCE desktop
  virtual display
  Selkies browser streaming
        |
        v
HTTPS connection to a PC browser

Webtop draws XFCE onto a virtual display because there is no physical monitor. Selkies encodes the display and sends it to the browser. Keyboard, mouse, clipboard, audio, and file-transfer events travel back to the Pi. Applications opened in the desktop therefore execute on the Pi, not on the client computer.

This is a container, not a virtual machine. The container has its own filesystem and desktop processes, but shares the Raspberry Pi host kernel. A terminal opened inside Webtop is a terminal inside the container. SSH access to the Pi is separate host access.

Why This Design

The supplied deep-research-report.md reaches the correct high-level conclusion, but it surveys more architectures than this requirement needs.

  • Webtop is the appropriate choice when the required result is a full graphical desktop in a browser.
  • SSH plus docker exec, OpenSSH containers, ttyd, Portainer, Guacamole, VNC, and QEMU solve different problems and add no value for one remote desktop here.
  • Pi 5 should use a 64-bit OS. On 64-bit Raspberry Pi OS, use Docker's Debian arm64 installation path rather than old 32-bit Raspberry Pi OS instructions.
  • The container uses a persistent /config directory and /workspace bind mount, so desktop settings and working files survive container recreation.
  • Webtop port 3001 is direct HTTPS. Port 3000 is plain HTTP and is only for a reverse proxy that terminates TLS; it is intentionally not published here.
  • Basic authentication is acceptable only for a trusted LAN or private VPN. Webtop includes a terminal with passwordless sudo inside the container, so anyone who gets access should be treated as an administrator of that desktop.
  • No privileged flag, Docker socket, host network, or GPU device is enabled by default. This is the safer and more reliable CPU baseline for a headless Pi.
  • The report's recommendation to start without GPU flags is important. Pi 5 graphics device compatibility should be tested on the actual host before adding /dev/dri.

The image tag is configurable. latest currently means Alpine XFCE and is the lightest starting point. Change WEBTOP_IMAGE in .env to lscr.io/linuxserver/webtop:debian-xfce or lscr.io/linuxserver/webtop:ubuntu-xfce when distribution-specific package compatibility matters more than image size.

Hardware And Host Prerequisites

  • Raspberry Pi 5, preferably with the official 27 W USB-C power supply and active cooling.
  • Raspberry Pi OS Lite 64-bit, installed with Raspberry Pi Imager.
  • Ethernet is preferred for a smoother remote desktop stream; Wi-Fi also works.
  • SSD or NVMe storage is strongly preferred over a small, slow microSD card for the Webtop image and persistent writes.
  • SSH enabled during imaging or enabled on the host after first boot.
  • Docker Engine and the Docker Compose plugin.

Verify the host before deploying:

uname -m
dpkg --print-architecture
cat /etc/os-release

Expected architecture output is aarch64 and arm64.

Host Setup

Flash current Raspberry Pi OS Lite (64-bit) with Raspberry Pi Imager. In the Imager customisation screen, set a hostname, create a user, configure networking, and enable SSH with a public key if possible.

SSH to the Pi, then install Docker from Docker's official Debian repository. These commands are for 64-bit Raspberry Pi OS:

sudo apt update
sudo apt install -y ca-certificates curl

sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg \
  -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo docker run --rm hello-world

To run Docker without sudo, add the Pi account to the docker group and start a new login session:

sudo usermod -aG docker "$USER"

Membership in the Docker group is effectively host-root access. If that is not acceptable, keep using sudo docker compose ... and adjust the commands below accordingly.

Deploy

Copy this directory to the Pi, or clone it there. For example, from another Linux/macOS/Windows machine with OpenSSH:

scp -r docker-pc pi@pi-hostname.local:/home/pi/
ssh pi@pi-hostname.local
cd ~/docker-pc

Run the setup helper on the Pi:

chmod +x scripts/setup.sh
./scripts/setup.sh

The helper:

  • rejects 32-bit or non-ARM64 hosts;
  • creates the persistent data directories;
  • prompts for a password and stores it as a local Docker secret with mode 0600;
  • creates .env from .env.example if it does not exist;
  • validates the Compose file;
  • pulls the ARM64 image selected by WEBTOP_IMAGE; and
  • starts the desktop with restart: unless-stopped.

Manual Compose commands are also available:

docker compose config --quiet
docker compose pull
docker compose up -d
docker compose ps
docker compose logs -f webtop

Find the Pi address with:

hostname -I

Then visit https://PI_IP_ADDRESS:3001 from a second device on the same network.

Connect From A PC

Same LAN

  1. Connect the PC and Pi to the same wired or wireless network.

  2. On the Pi, find its current LAN address:

    hostname -I

    Use the private IPv4 address beginning with 192.168., 10., or 172.16. through 172.31.. Ignore Docker addresses such as 172.17.0.1.

  3. Open a current version of Chrome, Edge, Firefox, or Safari on the PC.

  4. Visit:

    https://PI_LAN_IP:3001
    
  5. Accept the self-signed certificate warning. This is expected for direct Webtop HTTPS. Only accept it when the address is your own Pi on your trusted LAN.

  6. Enter the Webtop username and password. The default username is desktop.

  7. Wait for the XFCE desktop to load. The first startup after an image pull can take longer than later connections.

Tailscale VPN

Tailscale allows a PC outside the local Wi-Fi network to reach the Pi without exposing port 3001 on the public Internet.

  1. Install Tailscale on the PC and sign in to the same tailnet as the Pi.

  2. On the Pi, find its Tailscale IPv4 address:

    tailscale ip -4
  3. Visit the following address in the PC browser:

    https://TAILSCALE_IP:3001
    
  4. Accept the self-signed certificate warning and enter the Webtop credentials.

Finding The Password

The password is deliberately not stored in .env. On the Pi, display the local secret with:

cd /home/muse/docker-pc
tr -d '\n' < secrets/webtop_password

The file is mode 0600. Do not put its contents in a shell history, source repository, screenshot, or chat message.

Configuration

Copying .env.example creates these defaults:

  • WEBTOP_IMAGE=lscr.io/linuxserver/webtop:latest: Alpine XFCE.
  • WEBTOP_USER=desktop: browser basic-auth username.
  • PUID and PGID: ownership IDs for persistent files; normally 1000 on a new Pi user.
  • WEBTOP_BIND_ADDRESS=0.0.0.0: accessible on the Pi's interfaces. Set it to the Pi's LAN address for narrower binding.
  • WEBTOP_PORT=3001: host port for direct HTTPS.
  • SHM_SIZE=1gb: shared memory recommended for desktop containers.
  • AUTO_GPU=false: CPU baseline.

Do not put the Webtop password in .env. The password belongs in secrets/webtop_password, which is ignored by Git.

Persistence And Backups

The container itself is disposable. Durable files must be stored in one of the two bind-mounted directories:

Container path Host path Purpose Survives container recreation
/config data/config XFCE settings, desktop files, browser profiles, application settings, certificates Yes
/workspace data/workspace Working files and project data Yes

Files saved to the Webtop desktop, Documents folder, or browser profile normally live under /config. Files saved in /workspace are the clearest choice for work that must survive updates. Browser cookies and YouTube sessions are normally persisted in the browser profile, but services can still expire or revoke sessions and may request a new login.

Changes made only in the container writable layer, such as files under temporary paths or packages installed interactively with apt or apk, can disappear when the container is recreated. A normal stop, start, or restart does not remove the bind-mounted data.

Back up both persistent directories from the project directory:

cd /home/muse/docker-pc
tar -czf "$HOME/webtop-backup-$(date +%F).tar.gz" \
  data/config data/workspace

The backup may contain browser sessions and application credentials. Store it as carefully as the password secret.

To restore a backup, stop Webtop first, extract the archive over the project directory, then start the service again:

cd /home/muse/docker-pc
docker compose down
tar -xzf "$HOME/webtop-backup-YYYY-MM-DD.tar.gz"
docker compose up -d

To change the desktop flavor:

sed -i 's#^WEBTOP_IMAGE=.*#WEBTOP_IMAGE=lscr.io/linuxserver/webtop:debian-xfce#' .env
docker compose pull
docker compose up -d

To pin the exact image digest after the first successful pull, inspect it on the Pi:

docker image inspect lscr.io/linuxserver/webtop:latest \
  --format '{{index .RepoDigests 0}}'

Put the returned digest in WEBTOP_IMAGE if reproducible deployments are more important than automatic tag updates.

GPU Acceleration

Do not add /dev/dri until the CPU version works. LinuxServer documents GPU acceleration as optional, and a Pi 5's exposed DRM device does not by itself prove that the Webtop rendering and encoding pipeline will work.

If the CPU baseline is stable, test a temporary change in compose.yaml:

    devices:
      - /dev/dri:/dev/dri

Set AUTO_GPU=true in .env, recreate the container, and check the Webtop logs and a real workload. Revert the device mapping if the desktop becomes unstable. Do not use privileged: true as a generic fix.

Security And Remote Access

This project is designed for LAN or private-VPN use.

  • Do not forward port 3001 from the Internet to the Pi.
  • For access away from home, use a VPN such as WireGuard or Tailscale on the host and browse to the Pi through the VPN address.
  • If a public HTTPS endpoint is genuinely required, add a reverse proxy with real authentication and proxy to Webtop's internal HTTP port 3000; pass WebSocket upgrades and use long read/send timeouts.
  • Do not publish port 3000 directly.
  • Do not publish or mount /var/run/docker.sock.
  • Docker-published ports can bypass firewall rules users expect UFW/firewalld to enforce. Test from a second device and use Docker-aware DOCKER-USER rules where host firewall policy is required.
  • Disable sharing in the Compose file so a generated collaboration link cannot become an accidental second access path.

If only the wired or Wi-Fi LAN address should listen, set WEBTOP_BIND_ADDRESS in .env to the Pi's reserved address, for example 192.168.1.50, then recreate the service:

docker compose up -d --force-recreate

Operations

The service is configured with restart: unless-stopped, so Docker starts it after a host reboot unless it was deliberately stopped.

Stop and start the desktop without deleting persistent data:

docker compose stop
docker compose start

Restart after changing Compose or .env settings:

docker compose up -d --force-recreate

To rotate the password, replace secrets/webtop_password with a newline-free value, set its mode to 0600, and force a recreation so the running container receives the new secret. The setup helper creates the correct format when it creates a new password.

Update the image deliberately rather than auto-updating a running desktop:

docker compose pull webtop
docker compose up -d webtop
docker image prune

Back up both data/config and data/workspace. The container itself is disposable; these directories are the durable state.

Useful diagnostics:

docker compose ps
docker compose logs --tail=200 webtop
docker stats --no-stream pi-webtop
docker system df -v
free -h
df -h

If the browser shows a blank or black desktop, first confirm the URL is https://, verify that the WebSocket connection is not blocked, and return to the default CPU configuration before testing GPU changes.

If the browser cannot connect at all, check the service and listener on the Pi:

cd /home/muse/docker-pc
docker compose ps
docker compose logs --tail=200 webtop
ss -ltn | grep ':3001'

The expected container state is Up, and port 3001 should be listening. A response of 401 Unauthorized from this command means the HTTPS service is reachable and is correctly requesting authentication:

curl -k -I https://PI_IP_ADDRESS:3001

Use the Pi's LAN address for a same-network connection and its Tailscale address for a VPN connection. Do not use localhost from the PC, and do not use the unpublished internal HTTP port 3000.

Sources Checked

About

XFCE Linux desktop inside Docker on a Raspberry Pi accessible remotely through a browser

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages