Docker

Deploy Apache Guacamole with Docker Compose

Compiling Apache Guacamole from source works, but it takes 30+ minutes of dependency wrangling and build steps. The Docker route gets you a fully working remote desktop gateway in about five minutes with three containers: the guacd proxy daemon, the Guacamole web application, and a MySQL backend. No Tomcat tuning, no library conflicts, no guessing which LDAP JAR goes where.

Original content from computingforgeeks.com - post 130575

This guide walks through deploying Apache Guacamole with Docker Compose on Ubuntu 24.04, including the Nginx reverse proxy with Let’s Encrypt SSL, TOTP two-factor authentication, and session recording. If you prefer the source install route instead, we have guides for Guacamole on Ubuntu and Guacamole on Debian.

Tested March 2026 on Ubuntu 24.04.4 LTS with Docker 29.3.1, Docker Compose v5.1.1, Guacamole 1.6.0, MySQL 8.4, Nginx 1.24.0

Prerequisites

You need an Ubuntu 24.04 (or Debian 12/13) server with at least 2 GB RAM and root or sudo access. A registered domain name pointed at your server’s public IP is required for the SSL certificate. The steps below install Docker if you don’t already have it.

Install Docker and Docker Compose

Add the official Docker repository and install the engine along with the Compose plugin:

sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Confirm both Docker and Compose are available:

docker --version
docker compose version

You should see Docker 29.x and Compose v5.x:

Docker version 29.3.1, build 3ef2b14
Docker Compose version v5.1.1

Create the Project Directory and Generate the DB Init Script

Guacamole stores connections, users, and permissions in MySQL. The database schema ships inside the Docker image, so you extract it with a one-liner before starting anything:

sudo mkdir -p /opt/guacamole/init
cd /opt/guacamole

Pull the schema from the Guacamole image:

sudo docker run --rm guacamole/guacamole /opt/guacamole/bin/initdb.sh --mysql | sudo tee init/01-schema.sql > /dev/null

Verify the SQL file was generated:

ls -lh init/01-schema.sql

The file should be around 30-40 KB. MySQL will execute it automatically on first startup because the init directory is mounted into /docker-entrypoint-initdb.d.

Write the Compose File

Create the compose.yaml in your project directory:

sudo vi /opt/guacamole/compose.yaml

Add the following configuration:

services:
  guacd:
    image: guacamole/guacd
    container_name: guacd
    restart: unless-stopped
    volumes:
      - drive:/drive:rw
      - record:/record:rw

  mysql:
    image: mysql:8.4
    container_name: guac-mysql
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: R00tStr0ng!
      MYSQL_DATABASE: guacamole_db
      MYSQL_USER: guacamole_user
      MYSQL_PASSWORD: GuacDBStr0ng!
    volumes:
      - dbdata:/var/lib/mysql
      - ./init:/docker-entrypoint-initdb.d:ro

  guacamole:
    image: guacamole/guacamole
    container_name: guacamole
    restart: unless-stopped
    depends_on:
      - guacd
      - mysql
    environment:
      GUACD_HOSTNAME: guacd
      MYSQL_HOSTNAME: mysql
      MYSQL_DATABASE: guacamole_db
      MYSQL_USER: guacamole_user
      MYSQL_PASSWORD: GuacDBStr0ng!
      TOTP_ENABLED: "true"
      RECORDING_SEARCH_PATH: /record
    ports:
      - "8080:8080"
    volumes:
      - drive:/drive:rw
      - record:/record:rw

volumes:
  dbdata:
  drive:
  record:

Here’s what each service does:

  • guacd – The Guacamole proxy daemon that handles RDP, VNC, and SSH protocol translation. It connects to your remote machines on behalf of the web client.
  • mysql – Stores user accounts, connection definitions, permissions, and session history. The init directory mount loads the Guacamole schema on first run.
  • guacamole – The Java web application (runs on embedded Tomcat) that serves the browser-based interface on port 8080. It talks to guacd over port 4822 internally.

The three named volumes (dbdata, drive, record) persist data across container restarts. The drive volume enables file transfer through the Guacamole interface, and record stores session recordings.

Change the passwords in the compose file before deploying. Use strong, unique passwords for both MYSQL_ROOT_PASSWORD and MYSQL_PASSWORD. The MYSQL_PASSWORD value must match in both the mysql and guacamole service definitions.

Start the Stack

Launch all three containers in detached mode:

cd /opt/guacamole
sudo docker compose up -d

MySQL needs about 30-40 seconds on first startup to initialize the database and import the schema. Wait a moment, then check the status:

sudo docker compose ps

All three containers should show “Up”:

NAME         IMAGE                   STATUS          PORTS
guac-mysql   mysql:8.4               Up 5 minutes    3306/tcp, 33060/tcp
guacamole    guacamole/guacamole     Up 5 minutes    0.0.0.0:8080->8080/tcp
guacd        guacamole/guacd         Up 5 minutes    4822/tcp

Quick sanity check to confirm the web application responds:

curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/guacamole/

A 200 response means Guacamole is ready. If you get 000 or 502, the guacamole container is still starting. Give it another 20 seconds and try again.

Set Up Nginx Reverse Proxy with SSL

Guacamole listens on HTTP port 8080 by default. For production use, put Nginx in front with a Let’s Encrypt certificate. Install Nginx and Certbot:

sudo apt install -y nginx certbot

Obtain the SSL certificate (replace guac.example.com with your actual domain):

sudo certbot certonly --standalone -d guac.example.com --non-interactive --agree-tos -m [email protected]

Create the Nginx virtual host:

sudo vi /etc/nginx/sites-available/guacamole

Add this configuration:

server {
    listen 443 ssl http2;
    server_name guac.example.com;

    ssl_certificate /etc/letsencrypt/live/guac.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/guac.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8080/guacamole/;
        proxy_buffering off;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $http_connection;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

server {
    listen 80;
    server_name guac.example.com;
    return 301 https://$host$request_uri;
}

The proxy_http_version 1.1 and Upgrade/Connection headers are essential. Guacamole uses WebSockets for the remote desktop tunnel, and without these directives the connection drops after a few seconds.

Enable the site and restart Nginx:

sudo ln -s /etc/nginx/sites-available/guacamole /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

Open the firewall for HTTPS traffic:

sudo ufw allow 443/tcp
sudo ufw allow 80/tcp

Verify auto-renewal works:

sudo certbot renew --dry-run

Certbot sets up a systemd timer that renews the certificate automatically before it expires.

Access the Web UI

Open https://guac.example.com in your browser. You should see the Guacamole login page:

Apache Guacamole login page served over HTTPS with Nginx reverse proxy

Log in with the default credentials: username guacadmin, password guacadmin. Change this password immediately after your first login. Go to your username in the top-right corner, select Settings, then Preferences, and update the password.

Enable TOTP Two-Factor Authentication

The compose file already includes TOTP_ENABLED: "true" in the guacamole service environment. This activates time-based one-time password authentication for all users without installing any additional extensions.

On your next login after enabling TOTP, Guacamole presents a QR code enrollment screen:

Guacamole TOTP enrollment screen showing QR code for authenticator app

Scan the QR code with any TOTP authenticator app (Google Authenticator, Authy, FreeOTP, or a password manager that supports TOTP). Enter the six-digit code to complete enrollment. Every login from that point forward requires both the password and a TOTP code.

If you want to deploy without TOTP initially and add it later, remove the TOTP_ENABLED line from compose.yaml and recreate the container:

cd /opt/guacamole
sudo docker compose up -d --force-recreate guacamole

Enable Session Recording

Session recording is also pre-configured in the compose file via RECORDING_SEARCH_PATH: /record. This tells Guacamole where to look for recorded sessions when you want to play them back through the web interface.

To actually record a connection, you need to enable it per connection. In the Guacamole admin panel, edit a connection and scroll to the Screen Recording section. Set the recording path to /record and give the recording a name pattern (for example, ${GUAC_USERNAME}-${GUAC_DATE}).

Recordings are stored in the record Docker volume. To list them from the host:

sudo docker volume inspect record --format '{{ .Mountpoint }}'
sudo ls -lh $(sudo docker volume inspect record --format '{{ .Mountpoint }}')

You can play back any recording directly from the Guacamole web interface under Settings, then History. Click on a session that has a recording to watch the replay in your browser.

Managing the Stack

A few commands you’ll use regularly for day-to-day management.

View logs from all containers:

cd /opt/guacamole
sudo docker compose logs -f

Or check a specific container:

sudo docker compose logs -f guacamole

Restart the entire stack:

sudo docker compose restart

Pull updated images and recreate containers with the new versions:

sudo docker compose pull
sudo docker compose up -d

Docker Compose only recreates containers whose images have changed, so this is safe to run at any time.

Backup the Database

Dump the MySQL database to a file on the host:

sudo docker exec guac-mysql mysqldump -u root -p'R00tStr0ng!' guacamole_db > /opt/guacamole/backup-$(date +%F).sql

This captures all connections, users, permissions, and history. Store these backups off-server. To restore, copy the SQL file into the running MySQL container:

sudo docker exec -i guac-mysql mysql -u root -p'R00tStr0ng!' guacamole_db < /opt/guacamole/backup-2026-03-27.sql

Troubleshooting

Guacamole container keeps restarting

This almost always means MySQL isn't ready yet. The guacamole container starts, tries to connect to the database, fails, and restarts. Check the logs:

sudo docker compose logs guacamole | tail -20

If you see connection refused or authentication errors pointing at MySQL, wait 30-40 seconds for MySQL to finish initializing. On first startup, MySQL imports the schema file which takes time. The container will stabilize on its own once the database is ready because of the restart: unless-stopped policy.

If it keeps failing after a minute, verify the database credentials in compose.yaml. The MYSQL_PASSWORD in the guacamole service must exactly match the one in the mysql service.

WebSocket connection errors or tunnel closed

When the remote desktop loads for a second then drops with "The Guacamole connection has been closed" or "Tunnel closed," the issue is usually Nginx not forwarding WebSocket connections properly. Confirm your Nginx config includes these three lines in the location block:

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;

Also verify that proxy_buffering off; is set. Buffering interferes with the real-time stream between the browser and guacd.

Error: "Could not find or load main class org.apache.guacamole.extension..."

This appeared in older Guacamole Docker images when there was a javax-to-jakarta namespace conflict. The current 1.6.0 Docker images handle this internally, so you should not see it. If you do, pull fresh images:

sudo docker compose pull
sudo docker compose up -d --force-recreate

Never add javax2jakarta workarounds when using the official Docker images. They are already patched.

Related Articles

Containers Configure Active Directory (AD) Authentication for Harbor Registry Containers How To Run Mattermost Server in Docker Containers Docker Install and Configure Traefik Ingress Controller on k0s Automation How To Deploy Matrix Server using Ansible and Docker

Leave a Comment

Press ESC to close