Clientless remote desktop gateway — a unified browser-based interface for RDP, SSH, and VNC protocols. Deployed via Docker Compose with MySQL persistence, session recording, and production hardening.
Apache Guacamole is a clientless remote desktop gateway — it lets you access RDP, SSH, and VNC servers from any browser without installing a client. The entire connection runs over HTML5, making it ideal as a bastion host, centralized access portal, or auditable jump server for production infrastructure.
Six reasons it has become the gateway of choice for teams managing mixed Linux & Windows infrastructure:
The deployment uses three interconnected containers handling protocol translation, web UI, and persistent state:
The implementation was performed on a clean Linux installation with the following prerequisites:
Unlike some applications, Guacamole requires its MySQL schema to be initialized manually before the web application can connect. Generate the SQL script from the Guacamole image itself to ensure version compatibility:
docker run --rm guacamole/guacamole /opt/guacamole/bin/initdb.sh --mysql > initdb.sql
The deployment is defined in a docker-compose.yml file, ensuring the environment is reproducible and that all three services can communicate over an internal network.
In this architecture, volumes are not just for database persistence — they are the bridge for session data:
| Volume | Purpose | Access |
|---|---|---|
./mysql_data | User accounts & connection settings survive container restarts | RW (mysql) |
./recordings | Encode & save session video files | RW (guacd) |
| Locate & serve recordings to browser player | RO (guacamole) |
Guacamole has built-in support for Graphical Session Recording — perfect for auditing. It captures every visual change on the screen into a file that can be reviewed later.
Mount a folder from your host machine to the guacd container so the files survive restarts:
| Side | Path |
|---|---|
| Host folder | /home/user/guacamole/recordings |
| Container folder | /var/lib/guacamole/recordings |
Go to Settings → Connections and edit your target connection. Scroll to the Screen Recording section and fill it out:
| Field | Recommended Value | Why? |
|---|---|---|
| Recording path | /var/lib/guacamole/recordings | Where files are saved inside the container |
| Recording name | ${GUAC_USERNAME}-${GUAC_DATE}-${GUAC_TIME} | Auto-names files with user + timestamp for easy auditing |
| Automatically create path | ✓ Checked | Ensures the folder exists before recording starts |
| Exclude mouse | Unchecked | Keep mouse visible to see where the user is clicking |
| Include key events | ✓ Checked | Logs keystrokes — vital for security auditing |
When two containers share a volume but run as different UIDs, file ownership becomes a real problem. Here's how I solved it cleanly:
| Configuration Key | Purpose |
|---|---|
user: "1001:1001" | Forces the web container to run as a specific non-root user for security hardening |
group_add: ["1000"] | The fix. Adds the frontend user (1001) to GID 1000 — the guacd group — so it can read recordings without 777 |
EXTENSION_PRIORITY | Ensures the recording-storage extension loads after the database to avoid authentication errors during playback |
The complete, production-ready Compose file. Three services, dedicated volumes, recording-aware permissions:
services:
# 1. The Proxy Daemon
guacd:
image: guacamole/guacd
container_name: guacd
restart: always
volumes:
- ./recordings:/var/lib/guacamole/recordings:rw
# 2. The Database
mysql:
image: mysql:8.0
container_name: guac_db
restart: always
environment:
MYSQL_DATABASE: guacamole_db
MYSQL_USER: guacamole_user
MYSQL_PASSWORD: Guacamole123
MYSQL_ROOT_PASSWORD: RootGuacamole123
volumes:
- ./init/initdb.sql:/docker-entrypoint-initdb.d/initdb.sql:ro
- ./mysql_data:/var/lib/mysql
# 3. The Web Interface
guacamole:
image: guacamole/guacamole
container_name: guac_web
user: "1001:1001" # Non-root for security hardening
group_add:
- "1000" # The guacd user/group — enables recording read access
restart: always
depends_on:
- guacd
- mysql
ports:
- "8080:8080"
environment:
GUACD_HOSTNAME: guacd
MYSQL_HOSTNAME: mysql
MYSQL_DATABASE: guacamole_db
MYSQL_USER: guacamole_user
MYSQL_PASSWORD: Guacamole123
RECORDING_SEARCH_PATH: /var/lib/guacamole/recordings
RECORDING_ENABLED: "true"
EXTENSION_PRIORITY: "mysql, recording-storage"
volumes:
- ./recordings:/var/lib/guacamole/recordings:ro
Guacamole records in a special .guac format (saves space + CPU). These can't be played in VLC directly — there are two playback paths:
On Guacamole 1.5.0+, install the History Recording Storage extension. This adds a "View" link directly in the Guacamole History tab — play the video right in your browser, no conversion required.
For older versions, use the guacenc utility (bundled with guacd) to convert .guac files to standard .m4v for VLC playback.
Exposing port 8080 directly is insecure for remote access. In a professional deployment, a reverse proxy (Nginx or Traefik) should sit in front of the guacamole container to handle SSL/TLS termination.
To verify the implementation, create a test connection inside the Guacamole UI:
All connection metadata and user permissions live in MySQL — regular backups are mandatory. This creates a gzipped dump with the date in the filename:
docker exec guac_db /usr/bin/mysqldump -u guacamole_user -pGuacamole123 guacamole_db \
| gzip > guac_db_backup_$(date +%F).sql.gz
Update to a newer Guacamole release using the standard Compose lifecycle:
# Fetch latest images
docker compose pull
# Recreate containers with new images
docker compose up -d
Deployed at production scale, this Guacamole setup eliminated the need for VPN client installs, replaced ad-hoc RDP file sharing, and produced verifiable audit trails for compliance review.