Skip to main content

VNC

VNC serves a graphical desktop over the RFB protocol. It draws an X session, so it does not work with Wayland — the desktop and display manager must run on Xorg (see Troubleshooting).

On Ubuntu 22.04, RDP replaces VNC as the default remote-desktop protocol; VNC is still the lightest option for a headless server.

Contents

Prerequisites

sudo apt update && sudo apt dist-upgrade -y

Reaching the server: SSH tunnel vs open port

Display :1 listens on TCP 5901, :2 on 5902, and so on.

  • Preferred — bind to localhost and tunnel over SSH. Start the server with -localhost so the port is not exposed, then forward it over SSH (see Connect over SSH). Nothing extra to open in the firewall, and the session is encrypted.
  • Direct — open the port. Only if you must connect without a tunnel, and ideally restrict it to your own IP. VNC's own transport is weakly encrypted.
# Direct connections only; prefer restricting the source
sudo ufw allow from YOUR_IP to any port 5901
# Or, unrestricted (least safe)
sudo ufw allow 5901

Choosing a desktop

DesktopVerdict
XfceBest experience on Ubuntu 22.04, at the cost of more RAM. Recommended.
LXDELighter, but has a keyboard bug over VNC (arrow keys and Tab do not work) on Ubuntu 20.04/22.04.

Set up the VNC server

Xfce with TigerVNC

Works well on Ubuntu 22.04.

sudo apt install -y xfce4 xfce4-goodies xorg dbus-x11 x11-xserver-utils
sudo apt install -y tigervnc-standalone-server tigervnc-common

# Start once so ~/.vnc is created, then stop it to edit the config
vncserver :1
vncserver -kill :1

# Set the desktop that VNC launches
chmod u+x ~/.vnc/xstartup
vi ~/.vnc/xstartup # see the xstartup section below

LXDE with TightVNC

Runs on Ubuntu server 20.04/22.04 (tested on Linode and VMware). Note the keyboard bug above.

# Desktop environment (choose gdm3 if prompted). autocutsel enables copy & paste.
sudo apt install -y lxde-core xdg-utils xfonts-100dpi xfonts-75dpi lxterminal autocutsel

# X resources file
touch ~/.Xresources

# VNC server
sudo apt install -y tightvncserver
dpkg -l | grep vnc # confirm the version

# First start writes ~/.vnc/xstartup and prompts for a password.
# "Would you like to enter a view-only password (y/n)?" -> n
vncserver -localhost # bind to localhost (tunnel over SSH)
# or a specific display:
vncserver :1

vncpasswd # change or add a view-only password later
vncserver -kill :1 # stop a display

# Edit xstartup
cp ~/.vnc/xstartup ~/.vnc/xstartup.bak
chmod +x ~/.vnc/xstartup
vi ~/.vnc/xstartup

TightVNC is X-only and does not support multiple displays — you may see Xlib: extension "RANDR" missing on display. Use TigerVNC if that matters.

xstartup

~/.vnc/xstartup decides which desktop the session runs. Replace its whole contents.

Minimal (Xfce)

#!/bin/sh
unset SESSION_MANAGER
unset DBUS_SESSION_BUS_ADDRESS
exec startxfce4

General template (any desktop)

Uncomment the one session= line you want. Ends by killing the VNC server when the session exits, so a clean logout tears the display down.

#!/bin/sh
unset SESSION_MANAGER
unset DBUS_SESSION_BUS_ADDRESS
export XKL_XMODMAP_DISABLE=1

# Show the VNC config tool (clipboard) in the top-left of the window
command -v vncconfig && vncconfig -iconic &

# Load X resources if present
[ -r "$HOME/.Xresources" ] && xrdb "$HOME/.Xresources"

# Pick one desktop
#session=startxfce4 # Xfce
session=startlxde # LXDE
#session=gnome-session # GNOME
#session='gnome-session --session=gnome-classic'
#session=mate-session # MATE
#session=startkde # KDE Plasma
#session=i3 # i3wm

# Copy & paste with the remote clipboard (needs autocutsel)
command -v autocutsel && autocutsel -fork

exec "$session"

vncserver -kill "$DISPLAY"

The older GNOME-oriented variant adds export XKL_XMODMAP_DISABLE=1 and runs /etc/X11/Xsession before launching lxterminal & and /usr/bin/lxsession -s LXDE & — use it only if the template above fails to start a session.

Server options

~/.vnc/config sets defaults for every display started for this user.

# desktop=sandbox
geometry=1920x1080 # resolution
dpi=96
# localhost # listen on localhost only
# alwaysshared

Connect over SSH

Forward the remote VNC port to a local port through an SSH tunnel, then point the viewer at localhost:59001.

# ssh -L <local port>:127.0.0.1:<remote port> -N -f -l <user> <remote ip>
ssh -L 59001:127.0.0.1:5901 -N -f -l root 129.112.197.165

-N runs no remote command, -f backgrounds the tunnel after authentication.

Tabby profile (Windows)

To launch the tunnel from a Tabby profile using PowerShell 7:

Program: C:\Program Files\PowerShell\7\pwsh.exe
Arguments: -Command & ssh -L 59001:127.0.0.1:5901 -N -f -l root <remote ip>

One-line equivalent:

'C:\Program Files\PowerShell\7\pwsh.exe' -nologo -Command '& ssh -L 59001:127.0.0.1:5901 -N -f -l root <remote ip>'

On Windows, kill leftover ssh.exe processes if a previous tunnel did not disconnect cleanly, or the local port stays busy.

Run as a systemd service

Start the server automatically at boot (example for TigerVNC).

vncserver -kill :1
sudo vi /etc/systemd/system/[email protected]

/etc/systemd/system/[email protected]:

[Unit]
Description=Start vnc server at startup
After=syslog.target network.target

[Service]
Type=forking
User=root
Group=root
WorkingDirectory=/root

# Some systemd versions no longer need PIDFile
#PIDFile=/root/.vnc/%H:%i.pid
ExecStartPre=-/usr/bin/vncserver -kill :%i > /dev/null 2>&1
ExecStart=/usr/bin/vncserver -depth 24 -geometry 1280x800 -localhost :%i
ExecStop=/usr/bin/vncserver -kill :%i

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
# The number after @ is the display, so vncserver@1 serves :1 on port 5901.
sudo systemctl enable [email protected]
sudo systemctl start vncserver@1
sudo systemctl status vncserver@1

Manage sessions

# List running sessions / listeners
vncserver -list
ps -ef | grep vnc | grep -v grep
sudo netstat -lnp | grep -i vnc

# Read a session log
less ~/.vnc/HOSTNAME:1.log

# Kill a display
vncserver -kill :1

Removing the systemd service completely:


systemctl stop [email protected] # may take a moment to respond
systemctl disable [email protected]
ls /etc/systemd/system/vnc*
sudo rm /etc/systemd/system/$VNC_SERVICE # and any related symlinks
sudo rm /usr/lib/systemd/system/$VNC_SERVICE # and any related symlinks
systemctl daemon-reload
systemctl reset-failed

Desktop apps and fixes

Locale and fonts

sudo apt install locales
sudo dpkg-reconfigure locales # generate the locales you need

# Default language
echo 'export LANG=en_US.UTF-8' >> ~/.bashrc

# CJK fonts
sudo apt install -y fonts-wqy-zenhei xfonts-wqy

Firefox

sudo apt install -y firefox
firefox

If it fails with cannot open display or Client is not authorized to connect to Server, X access control is blocking it. Grant access, preferring the narrower form:

xhost +localhost # allow local connections only (safer)
# xhost + # disables access control entirely — avoid

# Or run Firefox with the right X authority cookie
alias firefox='XAUTHORITY=$HOME/.Xauthority firefox'

xhost + turns off X access control for everyone — use +localhost or +SI:localuser:USER instead.

Other

# xdg-settings: not found
sudo apt install xdg-utils

# pCloud desktop needs FUSE
sudo apt install libfuse2

Viewer tip: colour depth

Washed-out colours are usually the viewer down-sampling. In the client: Properties ▸ Options ▸ General ▸ Picture quality: High.

Implementations

ServerNotes
TigerVNCFork of TightVNC; the usual choice on Linux. Supports RANDR / multiple displays.
TightVNCLightweight, X-only, single display.
TurboVNCAdds VirtualGL support for GPU-accelerated / 3D workloads.
RemoteVNCNAT and firewall pass-through.
RealVNCCommercial, from VNC's original authors.

Troubleshooting

Wayland is not supported

VNC draws an X session and cannot attach to Wayland. Force Xorg by editing /etc/gdm3/custom.conf and uncommenting:

WaylandEnable=false

Then restart the display manager (or reboot).

Cannot start Xtightvnc / font path

Couldn't start Xtightvnc; trying default font path.
Please set correct fontPath in the vncserver script.

Usually a stale display. Kill it and restart:

vncserver -list
vncserver -kill :1
vncserver :1

Windows: local port stays busy

A previous tunnel left an ssh.exe running — end it in Task Manager (or taskkill /F /IM ssh.exe) before reconnecting.