A modular system to add VNC + XFCE4 desktop to ANY existing container image.
Wraps any container with:
- TigerVNC Server - Remote desktop access
- XFCE4 Desktop - Lightweight GUI environment
- GPU Support - NVIDIA GPU passthrough for 3D apps
- Original Functionality Preserved - Container still works as before + VNC
cd ~/vnc-extension
# Wrap ROS2 Humble
./build-with-vnc.sh \
--base-image osrf/ros:humble-desktop-full \
--output ros-humble:vnc
# Wrap your custom image
./build-with-vnc.sh \
--base-image my-robot:latest \
--output my-robot:vnc# Run with default settings (port 5910)
./run-with-vnc.sh ros-humble:vnc
# Run with custom port and workspace mount
./run-with-vnc.sh my-robot:vnc \
--port 5911 \
--workspace ~/my_workspaceStep 1: Create SSH tunnel
ssh -L 5910:localhost:5910 benz@robovision.csres.utexas.eduKeep this terminal window open.
Step 2: Connect Screen Sharing on Mac
Open Finder → Go → Connect to Server (⌘K), then enter:
vnc://localhost:5910
Or use any VNC client and connect to:
localhost:5910
Password: vncpass
# Build VNC-wrapped ROS2
./build-with-vnc.sh \
--base-image osrf/ros:humble-desktop-full \
--output ros-humble:vnc
# Run with workspace mounted
./run-with-vnc.sh ros-humble:vnc \
--name rviz-container \
--port 5910 \
--workspace ~/ros_ws
# On your Mac: Create SSH tunnel
ssh -L 5910:localhost:5910 benz@robovision.csres.utexas.edu
# Connect Screen Sharing to: vnc://localhost:5910
# Password: vncpass
# In VNC desktop, open terminal and run:
# source /opt/ros/humble/setup.bash
# source /root/ros_ws/install/setup.bash
# rviz2# First, build your Jetson container normally
cd ~/cobot_ws/docker
podman build -t cobot:jetson -f Dockerfile .
# Wrap it with VNC
cd ~/vnc-extension
./build-with-vnc.sh \
--base-image cobot:jetson \
--output cobot:jetson-vnc
# Run with GPU and workspace
./run-with-vnc.sh cobot:jetson-vnc \
--port 5911 \
--workspace ~/cobot_ws
# On your Mac: Create SSH tunnel
ssh -L 5911:localhost:5911 benz@robovision.csres.utexas.edu
# Connect Screen Sharing to: vnc://localhost:5911# Wrap official PyTorch image
./build-with-vnc.sh \
--base-image pytorch/pytorch:latest \
--output pytorch:vnc
# Run with data directory mounted
./run-with-vnc.sh pytorch:vnc \
--port 5913 \
--workspace ~/ml_workspace \
-- -v ~/datasets:/data:ro# Sometimes you want the image but not VNC overhead
./run-with-vnc.sh my-robot:vnc --disable-vncOriginal Container (e.g., ROS2 Humble)
↓
VNC Wrapper Build
(adds TigerVNC + XFCE4)
↓
VNC-Wrapped Container
├─ VNC Server (background)
├─ XFCE4 Desktop
└─ Original Container Functionality
The VNC wrapper uses a smart entrypoint that:
- Starts VNC server in background (if
VNC_ENABLED=1) - Chains to original container's CMD/ENTRYPOINT
- Container works normally + VNC is available
vnc-extension/
├── Containerfile.vnc-wrapper # The wrapper Dockerfile
├── entrypoint-vnc-wrapper.sh # Smart entrypoint
├── build-with-vnc.sh # Build helper
├── run-with-vnc.sh # Run helper
└── README.md # This file
Edit /root/.vnc/passwd inside container or rebuild with custom password:
# In container
podman exec -it my-container bash
echo "mynewpassword" | vncpasswd -f > /root/.vnc/passwd
chmod 600 /root/.vnc/passwd
podman restart my-container# ROS container on :5910
./run-with-vnc.sh ros:vnc --name ros-vnc --port 5910
# ML container on :5911
./run-with-vnc.sh pytorch:vnc --name ml-vnc --port 5911
# Robot container on :5912
./run-with-vnc.sh robot:vnc --name robot-vnc --port 5912
# On Mac, create separate tunnels for each:
ssh -L 5910:localhost:5910 -L 5911:localhost:5911 -L 5912:localhost:5912 benz@robovision.csres.utexas.edu
# Connect to:
# vnc://localhost:5910 (ROS)
# vnc://localhost:5911 (ML)
# vnc://localhost:5912 (Robot)podman run -d \
--name my-container \
--device nvidia.com/gpu=all \
-p 5910:5901 \
-v ~/workspace:/workspace:Z \
-e VNC_ENABLED=1 \
my-image:vncpodman run -d \
--name my-container \
-e VNC_ENABLED=0 \
my-image:vnc# View logs
podman logs -f container-name
# Open shell
podman exec -it container-name bash
# Stop/start
podman stop container-name
podman start container-name
# Remove
podman stop container-name
podman rm container-name
# Remove image
podman rmi image-name:vncFirst, verify SSH tunnel is active:
# On Mac, check if tunnel is running
ps aux | grep "ssh.*5910"
# Recreate tunnel if needed
ssh -L 5910:localhost:5910 benz@robovision.csres.utexas.eduCheck VNC server status on Linux server:
# Check if VNC is running
podman exec -it container-name ps aux | grep vnc
# Check VNC logs
podman exec -it container-name cat /var/log/vnc.log
# Restart VNC manually if needed
podman exec -it container-name vncserver -kill :1
podman exec -it container-name vncserver :1 -geometry 1920x1080# Verify GPU is accessible
podman exec -it container-name nvidia-smi
# Check GPU env vars
podman exec -it container-name env | grep NVIDIA# Check XFCE is installed
podman exec -it container-name which startxfce4
# Check xstartup
podman exec -it container-name cat /root/.vnc/xstartup
# Restart container
podman restart container-name- Universal: Works with ANY base image (ROS, PyTorch, custom)
- Non-invasive: Original Dockerfile unchanged, can still use base image
- Optional: Can disable VNC at runtime if not needed
- Reusable: One wrapper for all projects
- Maintainable: Update VNC layer separately from app code
You could install VNC directly in your Dockerfile:
FROM osrf/ros:humble-desktop-full
RUN apt-get install tigervnc-standalone-server xfce4...Trade-offs:
- ❌ Must modify every Dockerfile
- ❌ Can't use official images without rebuilding
- ✅ Single image (slightly smaller)
Run VNC in separate container sharing display.
Trade-offs:
- ❌ Complex networking/volume setup
- ❌ Harder to manage
- ✅ Complete separation
To extend this for different base OSes:
- Alpine: Replace
apt-getwithapk - Fedora/RHEL: Replace with
dnf/yum
Free to use and modify.
Created for: Streaming RViz, ROS tools, ML visualization to Mac M3
Server: robovision.csres.utexas.edu
Author: benz