Remote Access · Docker Compose

Apache Guacamole

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 Docker Compose MySQL 8.0 RDP · SSH · VNC Session Recording
What is Apache Guacamole?

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.

Why Use Guacamole?

Six reasons it has become the gateway of choice for teams managing mixed Linux & Windows infrastructure:

Clientless Access
Pure HTML5 — works in any modern browser. No agents, no plugins, no client installs to maintain.
Multi-Protocol
Single UI for RDP, SSH, VNC, Telnet, and Kubernetes — switch protocols without switching tools.
Bastion Host Pattern
Sits at the edge so target servers stay isolated. Only one host needs public exposure.
Session Recording
Built-in graphical session recording captures every screen change + key events for audit trails.
Centralized Auth
User permissions, connection profiles, and 2FA — all stored in MySQL. LDAP & AD support out of the box.
Device Agnostic
Same experience on laptop, tablet, or phone — anywhere a browser runs, the gateway runs.
Architecture Overview

The deployment uses three interconnected containers handling protocol translation, web UI, and persistent state:

// guacamole/guacd
The Muscle
Proxy daemon. Handles RDP/SSH/VNC heavy lifting and translates them into the Guacamole protocol.
// guacamole/guacamole
The Brain
Java web app on Tomcat. Serves the HTML5 UI and handles authentication.
// mysql:8.0
The Memory
Persistent store for user credentials, connection profiles, and permission settings.
HTTPS Guac proto RDP·SSH·VNC Browser HTML5 client Guacamole Web Tomcat · auth · UI guacd protocol proxy Target Servers Windows · Linux MySQL users · connections Recordings session .guac files
User session (request → target) Auth / config lookup Session recording
Figure 1 — Live connection flow. A browser session traverses the Guacamole web app and guacd proxy to reach the target; auth is checked against MySQL and every session is recorded to disk.
Environment Prerequisites

The implementation was performed on a clean Linux installation with the following prerequisites:

  • Host OS — Linux (Ubuntu/Debian or RHEL-based)
  • Container Runtime — Docker Engine installed
  • Orchestration — Docker Compose (links the three services via a dedicated virtual network)
Phase A Initializing the Database
01 Generate MySQL Schema

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:

bash
docker run --rm guacamole/guacamole /opt/guacamole/bin/initdb.sh --mysql > initdb.sql
What this does:
  • Runs the Guacamole image in a one-shot container
  • Pipes the generated SQL schema into initdb.sql on the host
  • The MySQL container will auto-apply this file on first boot via /docker-entrypoint-initdb.d/
Phase B Service Orchestration
02 Docker Compose Strategy

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.

Persistent Storage & Volume Strategy

In this architecture, volumes are not just for database persistence — they are the bridge for session data:

VolumePurposeAccess
./mysql_dataUser accounts & connection settings survive container restartsRW (mysql)
./recordingsEncode & save session video filesRW (guacd)
Locate & serve recordings to browser playerRO (guacamole)
Phase C Session Recording Setup
03 Prepare Storage on the Host

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:

SidePath
Host folder/home/user/guacamole/recordings
Container folder/var/lib/guacamole/recordings
⚠ Permissions: The folder inside the container must be writable by the user running guacd (usually UID 1000). Quick fix on the host:
chmod 2777 /home/user/guacamole/recordings
04 Configure Recording in the Web UI

Go to Settings → Connections and edit your target connection. Scroll to the Screen Recording section and fill it out:

FieldRecommended ValueWhy?
Recording path/var/lib/guacamole/recordingsWhere 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✓ CheckedEnsures the folder exists before recording starts
Exclude mouseUncheckedKeep mouse visible to see where the user is clicking
Include key events✓ CheckedLogs keystrokes — vital for security auditing
Deep Dive The Recording Permissions Challenge
Cross-Container Permission Mapping

When two containers share a volume but run as different UIDs, file ownership becomes a real problem. Here's how I solved it cleanly:

Engineering Trade-off
The Problem — Cross-Container Permission Denied
By default, the guacd container ("Server") creates recording files on the host disk owned by UID 1000. When the guacamole container ("Frontend") tries to access these files to provide playback, it fails — the Frontend runs as UID 1001 and has no permission to read files owned by UID 1000.
The Solution — Group ID (GID) Mapping
Instead of falling back to insecure chmod 777, I implemented a precise Linux permission mapping directly inside docker-compose.yml — adding the frontend user to the guacd group so it inherits read access through group membership.
Configuration KeyPurpose
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_PRIORITYEnsures the recording-storage extension loads after the database to avoid authentication errors during playback
Final docker-compose.yml

The complete, production-ready Compose file. Three services, dedicated volumes, recording-aware permissions:

yaml
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
Key Environment Variables (Frontend)
  • RECORDING_SEARCH_PATH — Points the web app to the shared volume
  • RECORDING_ENABLED — Explicitly activates the playback functionality in the UI
  • GUACD_HOSTNAME — Uses Docker's internal DNS to route protocol requests to the proxy daemon
  • EXTENSION_PRIORITY — Controls extension load order to prevent auth race conditions
  Bring it up: docker compose up -d
How to View the Recordings

Guacamole records in a special .guac format (saves space + CPU). These can't be played in VLC directly — there are two playback paths:

Option A — In-Browser Playback (Recommended)

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.

Option B — Manual Conversion

For older versions, use the guacenc utility (bundled with guacd) to convert .guac files to standard .m4v for VLC playback.

Security & Production Hardening

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.

Reverse Proxy Strategy:
  • Encryption — TLS encrypts all traffic between the client's browser and the gateway
  • Certificates — Use Let's Encrypt for free, auto-renewing SSL certificates
  • Routing — Configure Nginx to forward requests to http://localhost:8080/guacamole/
  • Headers — Forward X-Forwarded-For and X-Forwarded-Proto so Guacamole knows it's behind HTTPS
Post-Install — Your First Connection

To verify the implementation, create a test connection inside the Guacamole UI:

  • Navigate — Click your username (top-right) → SettingsConnections
  • New Connection — Select "New Connection"
  • Protocol — Choose RDP (Windows) or SSH (Linux)
  • Network — Hostname (target IP) + Port (3389 for RDP, 22 for SSH)
  • Recording — Recording path: /var/lib/guacamole/recordings + Create recording path: ✓
Maintenance & Backups
Database Backups

All connection metadata and user permissions live in MySQL — regular backups are mandatory. This creates a gzipped dump with the date in the filename:

bash
docker exec guac_db /usr/bin/mysqldump -u guacamole_user -pGuacamole123 guacamole_db \
  | gzip > guac_db_backup_$(date +%F).sql.gz
Updating the Stack

Update to a newer Guacamole release using the standard Compose lifecycle:

bash
# Fetch latest images
docker compose pull

# Recreate containers with new images
docker compose up -d
⚠ Before major version jumps: back up the database and check the Guacamole release notes for schema migrations. Some versions require running an upgrade script against the DB.
Real-World Impact

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.

Outcomes:
  • Browser-only access slashed onboarding time for new engineers — no client setup overhead
  • Every privileged session is recorded with key events — feeds directly into SOC 2 evidence
  • Single bastion endpoint reduced public attack surface across the estate
  • MySQL-backed RBAC integrates cleanly with existing identity providers via LDAP
Related Documentation
Apache Guacamole Gateway  ·  Docker Compose · MySQL · Session Recording