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.
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.
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.
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
arm64installation path rather than old 32-bit Raspberry Pi OS instructions. - The container uses a persistent
/configdirectory and/workspacebind mount, so desktop settings and working files survive container recreation. - Webtop port
3001is direct HTTPS. Port3000is 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
sudoinside the container, so anyone who gets access should be treated as an administrator of that desktop. - No
privilegedflag, 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.
- 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-releaseExpected architecture output is aarch64 and arm64.
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-worldTo 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.
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-pcRun the setup helper on the Pi:
chmod +x scripts/setup.sh
./scripts/setup.shThe 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
.envfrom.env.exampleif 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 webtopFind the Pi address with:
hostname -IThen visit https://PI_IP_ADDRESS:3001 from a second device on the same network.
-
Connect the PC and Pi to the same wired or wireless network.
-
On the Pi, find its current LAN address:
hostname -I
Use the private IPv4 address beginning with
192.168.,10., or172.16.through172.31.. Ignore Docker addresses such as172.17.0.1. -
Open a current version of Chrome, Edge, Firefox, or Safari on the PC.
-
Visit:
https://PI_LAN_IP:3001 -
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.
-
Enter the Webtop username and password. The default username is
desktop. -
Wait for the XFCE desktop to load. The first startup after an image pull can take longer than later connections.
Tailscale allows a PC outside the local Wi-Fi network to reach the Pi without exposing port 3001 on the public Internet.
-
Install Tailscale on the PC and sign in to the same tailnet as the Pi.
-
On the Pi, find its Tailscale IPv4 address:
tailscale ip -4
-
Visit the following address in the PC browser:
https://TAILSCALE_IP:3001 -
Accept the self-signed certificate warning and enter the Webtop credentials.
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_passwordThe file is mode 0600. Do not put its contents in a shell history, source repository, screenshot, or chat message.
Copying .env.example creates these defaults:
WEBTOP_IMAGE=lscr.io/linuxserver/webtop:latest: Alpine XFCE.WEBTOP_USER=desktop: browser basic-auth username.PUIDandPGID: ownership IDs for persistent files; normally1000on 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.
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/workspaceThe 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 -dTo change the desktop flavor:
sed -i 's#^WEBTOP_IMAGE=.*#WEBTOP_IMAGE=lscr.io/linuxserver/webtop:debian-xfce#' .env
docker compose pull
docker compose up -dTo 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.
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/driSet 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.
This project is designed for LAN or private-VPN use.
- Do not forward port
3001from 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
3000directly. - 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-USERrules 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-recreateThe 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 startRestart after changing Compose or .env settings:
docker compose up -d --force-recreateTo 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 pruneBack 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 -hIf 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:3001Use 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.