Get Started with Docker - Part 6b: Clientless Remote Desktop with Apache Guacamole

Learn how to deploy Apache Guacamole in Docker behind Cloudflare Tunnel for secure clientless browser RDP and SSH access to homelab servers

Get Started with Docker - Part 6b: Clientless Remote Desktop with Apache Guacamole

In Part 6a, we set up Cloudflare Tunnel (cloudflared) in Docker and learned how to route public hostnames through Cloudflare's edge network with Zero-Trust Email OTP MFA and Geo-IP filtering—without opening a single inbound port on our home router.

Now that our secure remote tunnel foundation is in place, it's time to build on top of it!

In this guide, we're going to deploy Apache Guacamole, an HTML5 clientless gateway that renders full Windows Remote Desktop (RDP), VNC, and Linux SSH sessions directly inside any standard web browser. By putting Guacamole behind your Part 6a Cloudflare Tunnel, you get high-speed, secure remote desktop access to your home lab from any device, anywhere in the world!

We're going to:

  • Look at what Apache Guacamole is and how its microservices work
  • Set up your persistent Docker Config folders (Files/AppData/Config/guacamole)
  • Create our Docker Compose file using Nano Text Editor
  • Initialize the required PostgreSQL database schema
  • Run and set up Apache Guacamole using Docker Compose
  • Connect Guacamole to your Part 6a Cloudflare Tunnel (remote.yourdomain.com)
  • Configure your first Remote Desktop (RDP) and SSH connections in Guacamole
  • Test accessing your desktops securely inside your web browser

What's Apache Guacamole?

Apache Guacamole is a clientless web-based remote desktop gateway. It runs in a container on your network and renders full RDP (Windows), VNC, and SSH (Linux) sessions right inside any web browser.

That means you can access your home lab servers from a laptop, Chromebook, iPad, or work computer without installing any remote desktop software or VPN clients!


How Apache Guacamole Works (The 3 Microservices)

Under the hood, Apache Guacamole relies on three containerized microservices working in harmony:

  1. ⚙️ guacd (Guacamole Protocol Proxy Daemon):
    The native C daemon (guacamole/guacd) is the protocol translator. It connects directly to your target homelab servers over RDP (port 3389), SSH (port 22), or VNC (port 5900) and converts screen updates into a web-friendly stream.

  2. 🗄️ postgres (Relational Database):
    The PostgreSQL container (postgres:15-alpine) stores user credentials, encrypted password hashes, saved connection profiles (target IPs, ports, SSH keys), active session states, and audit logs.

  3. 🖥️ guacamole (Java HTML5 Web Application):
    The Java web client (guacamole/guacamole) serves the web UI dashboard in your browser. It handles user authentication against PostgreSQL and passes browser mouse/keyboard events over the internal Docker network to guacd.


💡 Prerequisite Check:
Before starting this guide, ensure you have completed Part 6a: Secure Remote Access with Cloudflare Tunnel to get your cloudflared tunnel connector up and running.


Where can I install this?

In this guide, we'll be using our Linux Docker host. You can run the exact same steps on a Raspberry Pi, an Ubuntu server, or inside WSL2 on Windows 11.


Set up your persistent Docker Config folders

In this guide and throughout our Get Started with Docker series, we'll be using the following folder structure to store all of our persistent configuration files:

/home/$yourusername/Files/AppData/Config

It's best practice to keep a structured folder setup to make organizing, updating, and backing up your Docker data volumes easy.

Create our Guacamole Config folders

First, we're going to create folders to mount into our Docker containers so your Guacamole settings, database, and session recordings persist even if containers are updated or recreated.

  • SSH into your Docker server installation.
  • Check you are in your home folder by running pwd.
  • Copy and paste the below command and press Enter to create the required folders:
mkdir -p Files/AppData/Config/guacamole/init Files/AppData/Config/guacamole/data Files/AppData/Config/guacamole/drive Files/AppData/Config/guacamole/record
  • Change into the newly created folder:
cd Files/AppData/Config/guacamole
  • You should now be in /home/$yourusername/Files/AppData/Config/guacamole.

Creating our Docker Compose file

We're going to use a Docker Compose file for this deployment. Now that we're in the right folder, create the compose file with the command below:

nano docker-compose.yml

💡 Tip: If you don't have nano installed on your system, run sudo apt-get install nano.

Copy and paste the below code block into your terminal window:

version: '3.8'

networks:
  guac_network:
    driver: bridge

services:
  # 1. guacd Protocol Proxy Daemon
  guacd:
    container_name: guacd
    image: guacamole/guacd:latest
    networks:
      - guac_network
    restart: always
    volumes:
      - ./drive:/drive:rw
      - ./record:/record:rw

  # 2. PostgreSQL Database
  postgres:
    container_name: guacamole_postgres
    image: postgres:15-alpine
    environment:
      PGDATA: /var/lib/postgresql/data/guacamole
      POSTGRES_DB: guacamole_db
      POSTGRES_USER: guacamole_user
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    networks:
      - guac_network
    restart: always
    volumes:
      - ./init:/docker-entrypoint-initdb.d:ro
      - ./data:/var/lib/postgresql/data:rw

  # 3. Guacamole Java Web Application
  guacamole:
    container_name: guacamole_app
    image: guacamole/guacamole:latest
    depends_on:
      - guacd
      - postgres
    environment:
      GUACD_HOSTNAME: guacd
      POSTGRES_DATABASE: guacamole_db
      POSTGRES_HOSTNAME: postgres
      POSTGRES_USER: guacamole_user
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    networks:
      - guac_network
    ports:
      - "8080:8080" # Local port for web GUI / reverse proxy
    restart: always
    volumes:
      - ./record:/record:rw

Next, create a .env file in the same folder (nano .env) to set your database password:

POSTGRES_PASSWORD=SetYourStrongPasswordHere123!
  • Press Ctrl + X, type Y, and press Enter to save the file.

Initializing the PostgreSQL Database Schema

You might wonder why we mapped ./init:/docker-entrypoint-initdb.d in our Docker Compose file.

The official PostgreSQL Docker container has a built-in auto-initialization feature: whenever a fresh database container starts up for the first time, it automatically scans /docker-entrypoint-initdb.d and executes any .sql script placed inside it.

Apache Guacamole provides a built-in helper tool (initdb.sh) that generates all required database tables, indexes, and default admin accounts into a single SQL file.

Run this single command while inside Files/AppData/Config/guacamole before starting your stack:

# Generate the database schema SQL file directly into your ./init folder
docker run --rm guacamole/guacamole /opt/guacamole/bin/initdb.sh --postgres > ./init/initdb.sql

Now start up your containers:

docker compose up -d

Docker will start your services. On its very first boot, PostgreSQL detects initdb.sql inside ./init and automatically builds all database tables and default admin credentials for you—no manual SQL commands or database tools required!

Important Security Step:
The default login is username guacadmin and password guacadmin. Log in immediately after setup to change this password!


Connecting Guacamole to your Cloudflare Tunnel (Part 6a)

If you need to deploy the cloudflared connector on your Docker host, here is a brief look at the Docker Compose setup stored in Files/AppData/Config/cloudflared:

version: '3.8'

services:
  cloudflared:
    container_name: cloudflared
    image: cloudflare/cloudflared:latest
    restart: unless-stopped
    command: tunnel --no-autoupdate run
    environment:
      - TUNNEL_TOKEN=${TUNNEL_TOKEN} # Tunnel Token from Cloudflare Zero Trust Dashboard

💡 Full Cloudflare Setup Guide:
For a detailed, step-by-step walkthrough on generating your TUNNEL_TOKEN, creating Zero-Trust Email OTP MFA rules, bypassing CGNAT, and configuring Geo-IP blocks, check out our complete Part 6a: Secure Remote Access with Cloudflare Tunnel guide!

Now, map your Guacamole service in the Cloudflare Zero Trust Dashboard:

  1. Log in to your Cloudflare Zero Trust Dashboard.
  2. Navigate to Networks > Tunnels and edit your Homelab-Tunnel.
  3. Add a new Public Hostname:
    • Public Hostname: remote.yourdomain.com
    • Service Type: HTTP
    • URL: http://localhost:8080 (or http://192.168.0.23:8080)
  4. Save the hostname.

Because you already configured your Cloudflare Access policy in Part 6a, any request to remote.yourdomain.com is automatically protected by Email OTP MFA and Geo-IP filtering before reaching your Guacamole portal!

🔒 The Cloudflare Tunnel Security & Automatic SSL Bonus

By connecting Apache Guacamole through Cloudflare Tunnel (cloudflared), you instantly gain several major security benefits without adding extra container overhead:

  • 💡 Automatic HTTPS / SSL Certificates: Cloudflare automatically issues and manages trusted Universal SSL/TLS certificates for https://remote.yourdomain.com. You don't need to run Certbot containers, open port 80 for HTTP-01 challenges, or manually renew SSL certificates every 90 days.
  • 💡 Bypasses CGNAT & Dynamic IPs: Because cloudflared makes an outbound connection from inside your network to Cloudflare, your Guacamole portal remains accessible remotely even if your ISP uses Carrier-Grade NAT (CGNAT) or dynamic WAN IPs—with no expensive Static IP required!
  • 💡 Zero-Trust Edge MFA: Cloudflare Access blocks unauthorized users at Cloudflare's edge with Email OTP MFA and Geo-IP filtering before anyone can even see your Guacamole login page.
  • 💡 Zero Inbound Open Ports: Router ports 3389 (RDP), 22 (SSH), and 8080 (Guacamole) stay 100% closed to the internet.

Adding your first Remote Connection in Guacamole

Log into your Guacamole web interface (https://remote.yourdomain.com) using guacadmin.

Go to Settings > Connections > Add Connection:

For Windows Server (RDP):

  • Name: Windows Server DC
  • Protocol: RDP
  • Network Hostname: 192.168.0.10 (Target local IP)
  • Port: 3389
  • Authentication: Set Security Mode to NLA and tick Ignore Server Certificate.

For Linux Server (SSH):

  • Name: Ubuntu Host SSH
  • Protocol: SSH
  • Network Hostname: 192.168.0.25
  • Port: 22
  • Authentication: Enter your SSH Username and paste your SSH Private Key.

Click Save, head back to the Home dashboard, and click your new connection. You will now have a full, high-speed remote desktop or SSH session running right inside your browser tab!


Congratulations! You've combined Cloudflare Zero Trust (Part 6a) with Apache Guacamole (Part 6b) to build a secure, clientless remote desktop gateway powered by Docker!


Don't forget to explore the rest of our website as we build out more content. Stay tuned for more tutorials, tips, and tricks to help you make tech work for you.

If you want to stay up-to-date with regular updates, make sure to subscribe to our free mailing list.