Introduction
I’ve been building out my homelab to make managing my infrastructure easier. One thing I wanted was a dedicated Linux workstation that I could access from my browser, without having to sit at my desk or install a remote desktop client on every device.
My solution was to run Ubuntu Desktop as a virtual machine on Proxmox and access it through Apache Guacamole.
The goal wasn’t just to get a remote desktop working. I wanted to access the same GNOME desktop session that I see in the Proxmox console, with proper certificate validation and restricted network access.
I also wanted to keep the setup relatively simple. Since GNOME already includes its own remote desktop functionality, I decided to use that instead of maintaining a separate XRDP configuration.
In this guide, I’ll walk through the architecture, configuration, security decisions, and a few troubleshooting steps from the setup.
Architecture
My setup consists of three main components:
- Proxmox VE - hosts the Ubuntu Desktop virtual machine.
- Ubuntu Desktop VM (
ubuntu-desktop) - runs GNOME and GNOME Remote Desktop. - Docker LXC - hosts Apache Guacamole,
guacd, and PostgreSQL.
The Tailscale LXC is also part of my homelab management setup, providing restricted SSH access to the Ubuntu VM.
| Component | IP address | Purpose |
|---|---|---|
| Proxmox VE | PROXMOX_HOST | Virtualisation host |
| Ubuntu Desktop VM | UBUNTU_VM_IP | GNOME desktop |
| Docker LXC | GUACAMOLE_HOST_IP | Apache Guacamole |
| Tailscale LXC | TAILSCALE_HOST_IP | Restricted SSH access |
The connection flow looks like this:
My browser
|
v
Apache Guacamole
GUACAMOLE_HOST_IP:8080
|
v
guacd
|
| RDP over TCP 3390
v
Ubuntu Desktop VM
UBUNTU_VM_IP
|
v
Existing GNOME / Wayland session
Guacamole provides the browser-based interface, while guacd handles the remote desktop protocol connection to Ubuntu.
I kept Guacamole on my existing Docker LXC rather than installing it inside the desktop VM. This separates the remote access service from the machine I’m connecting to and keeps the desktop VM focused on being a workstation.
Why Apache Guacamole?
Apache Guacamole is a clientless remote desktop gateway. Instead of installing an RDP client on my laptop, I can open a browser and connect to my desktop through its web interface.
I already had Guacamole running in my homelab, so reusing it made more sense than introducing another remote access service.
The main advantages for my setup are:
- Browser-based access from different devices.
- Centralised remote desktop connections.
- No need to expose RDP directly to every device on my LAN.
- Separation between the remote access gateway and the desktop VM.
My Guacamole instance is available at:
http://GUACAMOLE_HOST_IP:8080/guacamole/
This is an internal homelab address, not a public-facing endpoint.
Why GNOME Remote Desktop instead of XRDP?
Initially, I experimented with XRDP and XFCE while troubleshooting the connection. However, I realised that wasn’t what I wanted long term.
XRDP commonly creates a separate remote graphical session. Depending on its configuration, that session may use a different desktop environment or behave differently from the session displayed in the VM console.
I wanted to connect to my existing GNOME session running on Wayland, so I could work with the same desktop whether I accessed it through Proxmox or Guacamole.
Ubuntu’s GNOME Remote Desktop supports this use case.
The final configuration uses:
- Ubuntu Desktop 26.04.1 LTS.
- GNOME on Wayland.
- GNOME Remote Desktop’s built-in RDP server.
- TCP port
3390.
I chose port 3390 to keep this connection separate from the conventional RDP port 3389, which I had used during the earlier XRDP experiments.
Changing the port doesn’t make RDP inherently more secure. The actual security comes from controlling which hosts can connect and validating the server certificate.
Step 1: Enable GNOME Remote Desktop
First, I configured GNOME Remote Desktop to share the existing graphical session.
On the Ubuntu desktop VM, open the GNOME Settings application and navigate to System → Remote Desktop. Depending on the exact Ubuntu release, the menu labels may differ slightly.
Enable Remote Login or the applicable RDP sharing option, configure the credentials, and set the RDP port to 3390 where supported.
GNOME Remote Desktop has different modes for remote login and desktop sharing. For this setup, the important requirement is that the connection uses the existing logged-in GNOME session rather than creating a separate desktop.
I then checked the service:
systemctl --user is-enabled gnome-remote-desktop.service
systemctl --user is-active gnome-remote-desktop.service
I also checked the RDP configuration:
grdctl status
The relevant output looked like this:
Unit status: active
RDP:
Status: enabled
Port: 3390
Authentication methods: credentials
The username and password were configured locally and are intentionally not included here.
Why share the existing session?
This was the main reason for choosing GNOME Remote Desktop.
I didn’t want to maintain a separate XFCE desktop just for remote access. Sharing the existing session means that the desktop I interact with through Guacamole is the GNOME desktop I use locally through the VM console.
There is one important limitation: the graphical session must be logged in. Starting Ubuntu does not automatically mean that the existing GNOME desktop is ready for remote access.
I’ll cover that in the reboot section below.
Step 2: Configure the TLS certificate
Once the RDP connection was working, I addressed certificate validation.
My first attempt used GNOME Remote Desktop’s self-signed certificate. Guacamole could reach the RDP server, but the connection failed because the certificate wasn’t trusted.
The error indicated a certificate validation failure, which is different from a network connectivity problem.
Rather than leave certificate validation disabled in Guacamole, I replaced the certificate with a new self-signed certificate containing the correct IP address and hostname in its Subject Alternative Name (SAN).
Generate the certificate
On the Ubuntu VM:
cd ~/.local/share/gnome-remote-desktop
cp tls.crt tls.crt.backup
cp tls.key tls.key.backup
openssl req -new -newkey rsa:2048 -nodes -x509 -days 365 \
-subj "/CN=UBUNTU_VM_IP" \
-addext "subjectAltName=IP:UBUNTU_VM_IP,DNS:ubuntu-desktop" \
-keyout tls-new.key \
-out tls-new.crt
chmod 600 tls-new.key
chmod 644 tls-new.crt
The certificate includes:
- Common Name:
UBUNTU_VM_IP - IP SAN:
UBUNTU_VM_IP - DNS SAN:
ubuntu-desktop - Validity: 365 days
The SAN is important because clients generally validate the hostname or IP address against the certificate’s SAN entries. Including the address used by Guacamole avoids a name mismatch when connecting to the VM by IP.
The private key must remain on the Ubuntu VM and must not be copied into the Guacamole container.
Tell GNOME Remote Desktop to use the new certificate
Still on Ubuntu:
grdctl rdp set-tls-key \
"$HOME/.local/share/gnome-remote-desktop/tls-new.key"
grdctl rdp set-tls-cert \
"$HOME/.local/share/gnome-remote-desktop/tls-new.crt"
systemctl --user restart gnome-remote-desktop.service
Check the resulting configuration:
grdctl status
Confirm that the new certificate and key paths appear in the output.
Note: A self-signed certificate is not automatically trusted just because it has the correct SAN. We still need to establish trust on the client side.
Step 3: Trust the certificate in the Guacamole container
Guacamole’s guacd component is responsible for establishing the RDP connection. In my setup, it runs inside a Docker container on the Docker LXC.
I copied the public certificate from Ubuntu to the Docker LXC. Only the certificate is needed; the private key stays on the Ubuntu VM.
The following commands were run from the Ubuntu VM, using the Docker LXC as the destination:
scp ~/.local/share/gnome-remote-desktop/tls-new.crt \
root@GUACAMOLE_HOST_IP:/tmp/ubuntu-desktop-tls.crt
On the Docker LXC, I backed up the Compose file and created a directory for the certificate:
cd /opt/guacamole
cp compose.yaml compose.yaml.backup
mkdir -p certs
install -m 644 /tmp/ubuntu-desktop-tls.crt \
certs/ubuntu-desktop.crt
The certificate is public information, so it doesn’t need the same restrictive permissions as the private key.
Build a custom guacd image
The original guacd image didn’t have my desktop’s certificate in its trusted certificate bundle. Rather than modify a running container manually, I created a small custom image that adds the certificate to the system trust store.
Create /opt/guacamole/Dockerfile.guacd:
FROM guacamole/guacd:1.6.0
USER root
RUN apk add --no-cache ca-certificates openssl
COPY certs/ubuntu-desktop.crt \
/usr/local/share/ca-certificates/ubuntu-desktop.crt
RUN update-ca-certificates
USER guacd
This image uses the existing Guacamole version and adds only the certificate trust configuration and OpenSSL for verification.
The certificate is installed under /usr/local/share/ca-certificates, and update-ca-certificates updates the system certificate bundle.
Update the Compose configuration
In /opt/guacamole/compose.yaml, I changed only the guacd service to build the custom image:
services:
guacd:
build:
context: .
dockerfile: Dockerfile.guacd
image: guacamole/guacd:1.6.0-trusted
container_name: guacamole-guacd
restart: unless-stopped
networks:
- guac_internal
- guac_frontend
The other services, including PostgreSQL and the Guacamole web application, remained unchanged.
I then validated the Compose configuration and rebuilt only guacd:
docker compose config --quiet
docker compose up -d --build guacd
This approach makes the certificate installation reproducible. If I need to recreate the container, the certificate is included in the image build rather than relying on a manual change inside a running container.
Verify the certificate
Inside the Docker LXC, I checked the certificate:
docker exec guacamole-guacd \
openssl verify -CAfile /etc/ssl/cert.pem \
/usr/local/share/ca-certificates/ubuntu-desktop.crt
The expected output is:
/usr/local/share/ca-certificates/ubuntu-desktop.crt: OK
This verifies that the certificate can be validated against the container’s configured CA bundle. The final confirmation is the actual RDP connection, since certificate verification can also depend on how the client handles the server certificate.
Disable the certificate-validation workaround
During troubleshooting, I temporarily enabled Ignore server certificate in the Guacamole connection settings. This helped confirm that the RDP service was reachable and that the certificate trust issue was the blocker.
After installing the certificate into the guacd trust store, I disabled that option again.
The connection worked with certificate validation enabled.
This is the outcome I wanted: a working remote desktop connection without leaving certificate validation disabled.
Because the certificate is self-signed, this configuration relies on the explicitly installed trust anchor. Anyone replacing the certificate should verify its fingerprint and update the trusted certificate accordingly.
Step 4: Restrict access with UFW
I didn’t want the RDP port to be reachable by every device on the local network.
On Ubuntu, UFW is enabled with a default-deny incoming policy. The final rules allow only the hosts that need access.
Check the current rules:
sudo ufw status numbered
My relevant rules are:
22/tcp ALLOW IN TAILSCALE_HOST_IP
UBUNTU_VM_IP 3390/tcp ALLOW IN GUACAMOLE_HOST_IP
These allow:
- SSH from the Tailscale LXC at
TAILSCALE_HOST_IP. - RDP on port
3390from the Docker LXC atGUACAMOLE_HOST_IP.
The second rule is restricted to the Ubuntu VM’s address and the Guacamole host.
The exact command used to add the RDP rule was:
sudo ufw allow from GUACAMOLE_HOST_IP \
to UBUNTU_VM_IP port 3390 proto tcp
I also checked that the old XRDP port wasn’t listening:
sudo ss -ltnp | grep -E ':(3389|3390)\b'
The relevant output showed only GNOME Remote Desktop listening on port 3390.
Why restrict the source address?
Guacamole is the gateway through which I want to access the desktop. There is no reason for other LAN devices to connect directly to the RDP service.
Restricting the source address reduces the number of systems that can reach the service. It also keeps the network flow predictable when troubleshooting.
These rules assume that the Docker LXC connects directly from GUACAMOLE_HOST_IP. If the network topology or container networking changes, the source address may need to be reviewed.
Step 5: Keep manual login after reboot
I wanted the VM to remain secure rather than automatically logging into an unlocked graphical desktop after every reboot.
I therefore left automatic login disabled.
The GNOME Remote Desktop user service was enabled:
systemctl --user is-enabled gnome-remote-desktop.service
It returned:
enabled
I also enabled lingering for the your-user user:
sudo loginctl enable-linger your-user
Then I checked:
loginctl show-user your-user -p Linger
The output was:
Linger=yes
What lingering does - and doesn’t do
Lingering allows the user’s systemd manager to run without requiring a normal login session to remain open. This is useful for user services that need to persist independently of a terminal session.
However, lingering does not automatically log the user into GNOME, nor does it guarantee that the graphical desktop session exists after a reboot.
GNOME Remote Desktop is tied to the GNOME session in this configuration. The graphical session must be available before the intended desktop can be accessed.
My post-reboot workflow
After a reboot, my workflow is:
- Open the Proxmox web interface.
- Select the
ubuntu-desktopVM. - Open the VM console.
- Log in to Ubuntu through GDM.
- Wait for GNOME to finish starting.
- Connect through Apache Guacamole.
This gives me browser-based access during normal use while retaining manual login after a restart.
The Proxmox console remains the recovery path if Guacamole isn’t available yet.
Step 6: Remove the unused XRDP setup
Since I chose GNOME Remote Desktop, I no longer needed XRDP or XFCE as an alternative desktop environment.
I verified the listening ports:
sudo ss -ltnp | grep -E ':(3389|3390)\b'
Only port 3390 was listening for the desktop connection.
I also checked the package state:
dpkg-query -W \
-f='${binary:Package} ${Status}\n' \
xrdp xorgxrdp xfce4 2>/dev/null
XFCE was not installed, and XRDP was no longer present as an installed package.
Keeping the setup focused on one remote desktop implementation makes it easier to understand which service handles incoming connections and avoids maintaining an alternative session that I don’t intend to use.
Troubleshooting
Guacamole reports a certificate validation error
Check the certificate configured in GNOME Remote Desktop:
grdctl status
Verify that the public certificate on the Docker LXC matches the certificate used by the Ubuntu VM:
openssl x509 -in /tmp/ubuntu-desktop-tls.crt \
-noout -subject -fingerprint -sha256
Then check the certificate installed inside guacd:
docker exec guacamole-guacd \
openssl verify -CAfile /etc/ssl/cert.pem \
/usr/local/share/ca-certificates/ubuntu-desktop.crt
If verification fails, check that the certificate is installed in the expected directory, the CA bundle was updated, and the container was rebuilt.
If the certificate was replaced on Ubuntu, the trusted copy in the Guacamole image must also be updated.
Guacamole cannot connect to port 3390
On Ubuntu, check the service:
systemctl --user status gnome-remote-desktop.service
grdctl status
Check whether the port is listening:
sudo ss -ltnp | grep ':3390'
Check UFW:
sudo ufw status numbered
On the Docker LXC, inspect the Guacamole daemon logs:
docker logs --since 5m guacamole-guacd
This helps distinguish a network or firewall problem from an RDP or TLS problem.
Guacamole connects but the desktop isn’t available after reboot
With manual login enabled, this can be expected if the GNOME graphical session hasn’t started yet.
Use the Proxmox console to log into Ubuntu and allow GNOME to finish starting before reconnecting through Guacamole.
Lingering alone doesn’t create a logged-in graphical session.
Final result
I now have a browser-accessible Ubuntu workstation running on Proxmox, using Apache Guacamole as the gateway and GNOME Remote Desktop to share the existing desktop session.
The final setup gives me:
- Access to my existing GNOME/Wayland desktop.
- A browser-based remote desktop connection through Guacamole.
- A trusted TLS certificate rather than an ignored-certificate workaround.
- Firewall rules restricted to the required hosts.
- Manual login after reboot.
- No unnecessary XRDP or XFCE configuration to maintain.
For my homelab, this is a practical balance between convenience, security, and maintainability. I can use the workstation during normal operations without installing an RDP client on every device, while retaining the Proxmox console as a recovery path.
The main lesson from this setup is that getting a remote desktop to connect is only one part of the job. The certificate trust, firewall rules, graphical session lifecycle, and recovery process all matter just as much.