Guacamole is an open-source clientless remote desktop gateway — VNC, RDP, and SSH all accessible through a browser, with no client software required. This guide deploys a production-ready stack using Docker Compose, PostgreSQL for persistent user management, Nginx as a TLS-terminating reverse proxy, and Let's Encrypt for free, auto-renewing certificates.
Stack: guacd · guacamole · postgres · nginx · TLS: Let's Encrypt · OS: Linux
01 — Prerequisites
| Requirement | Notes |
| Docker + Compose | Docker Engine v24+ and the Compose v2 plugin installed on the host |
| Domain name | An A record pointing to your server's public IP — required for Let's Encrypt |
| Ports 80 & 443 | Must be reachable from the internet; firewall rules adjusted accordingly |
| Linux server | Ubuntu 22.04+ or Debian 11+ recommended |
02 — Project Structure
Create the following layout on your server. All paths in later steps are relative to the guacamole/ root.
Directory tree
guacamole/
├── docker-compose.yml
├── nginx/
│ └── guacamole.conf
└── init/
└── initdb.sql ← generated in the next step
Create directories
mkdir -p guacamole/init guacamole/nginx
cd guacamole
03 — Generate the Database Schema
Guacamole ships a schema initialisation script inside its own image. Run the container once, pipe its output to a file, then discard the container. PostgreSQL will execute this file automatically on first startup.
Export schema
docker run --rm guacamole/guacamole \
/opt/guacamole/bin/initdb.sh --postgresql \
> init/initdb.sql
ℹ This must be done before docker compose up. If the postgres_data volume already exists and is populated, the init script will be silently skipped by PostgreSQL.
04 — Nginx Configuration
Create nginx/guacamole.conf. The first server block forces all HTTP traffic to HTTPS. The second terminates TLS and proxies to the Guacamole Tomcat container on port 8080.
nginx/guacamole.conf
server {
listen 80;
server_name your.domain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name your.domain.com;
ssl_certificate /etc/letsencrypt/live/your.domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your.domain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://guacamole:8080/guacamole/;
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_cookie_path /guacamole/ /;
access_log off;
}
}
⚠ Replace both instances of your.domain.com with your actual domain. Mismatched names will cause Nginx to reject connections or serve the wrong certificate.
05 — Docker Compose File
This file defines all four services and their dependencies. The depends_on chain ensures startup order: PostgreSQL → guacd → guacamole → nginx.
docker-compose.yml
services:
guacd:
image: guacamole/guacd
container_name: guacd
restart: unless-stopped
volumes:
- drive:/drive
- record:/record
postgres:
image: postgres:15
container_name: guac-postgres
restart: unless-stopped
environment:
POSTGRES_DB: guacamole_db
POSTGRES_USER: guacamole_user
POSTGRES_PASSWORD: StrongPasswordHere
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init/initdb.sql:/docker-entrypoint-initdb.d/initdb.sql
guacamole:
image: guacamole/guacamole
container_name: guacamole
restart: unless-stopped
depends_on:
- guacd
- postgres
environment:
GUACD_HOSTNAME: guacd
POSTGRESQL_HOSTNAME: postgres
POSTGRESQL_DATABASE: guacamole_db
POSTGRESQL_USER: guacamole_user
POSTGRESQL_PASSWORD: StrongPasswordHere
nginx:
image: nginx:alpine
container_name: guac-nginx
restart: unless-stopped
depends_on:
- guacamole
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/guacamole.conf:/etc/nginx/conf.d/default.conf:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
- /var/lib/letsencrypt:/var/lib/letsencrypt:ro
volumes:
postgres_data:
drive:
record:
⚠ Replace StrongPasswordHere with a strong password in both the postgres and guacamole blocks — they must match exactly.
06 — Obtain a TLS Certificate
Use Certbot in standalone mode to issue the certificate before the stack starts. Port 80 must be free — nothing should be running on it yet.
Install Certbot & issue certificate
sudo apt install -y certbot
sudo certbot certonly --standalone -d your.domain.com
Certificates are written to /etc/letsencrypt/live/your.domain.com/ and are valid for 90 days. Certbot installs a systemd timer that handles renewal automatically.
Test renewal & add Nginx reload hook
sudo certbot renew --dry-run
sudo tee /etc/letsencrypt/renewal-hooks/post/reload-nginx.sh <<'EOF'
#!/bin/bash
docker exec guac-nginx nginx -s reload
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/post/reload-nginx.sh
ℹ Without the reload hook, Nginx will keep serving the old certificate after renewal until the container is manually restarted.
07 — Start the Stack
Bring everything up
docker compose up -d
Verify all four containers are running
docker compose ps
NAME IMAGE STATUS
guacd guacamole/guacd Up
guac-postgres postgres:15 Up
guacamole guacamole/guacamole Up
guac-nginx nginx:alpine Up
Open your browser and navigate to your domain. You should land on the Guacamole login page over a valid HTTPS connection.
✓ Default credentials: guacadmin / guacadmin. Go to Settings → Users and change these immediately before doing anything else.
⚠ Leaving the default admin password in place is equivalent to leaving a root shell open with no password — anyone who knows the URL has full administrative access to all connections and credentials stored in Guacamole.
08 — Optional: Self-Signed Certificate
No public domain? Generate a self-signed certificate for internal or lab use. Browsers will show a security warning — expected and acceptable for private networks.
Generate certificate
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/ssl/private/guacamole.key \
-out /etc/ssl/certs/guacamole.crt
Update nginx/guacamole.conf
ssl_certificate /etc/ssl/certs/guacamole.crt;
ssl_certificate_key /etc/ssl/private/guacamole.key;
Also update the Nginx volume mounts in docker-compose.yml — replace the /etc/letsencrypt mounts with /etc/ssl:/etc/ssl:ro.
09 — Key Commands
| Task | Command |
| Start stack | docker compose up -d |
| Stop stack | docker compose down |
| View logs (live) | docker compose logs -f |
| Restart Nginx | docker exec guac-nginx nginx -s reload |
| Check status | docker compose ps |
| Renew certificate | sudo certbot renew |
| Reset database | docker compose down -v && docker compose up -d |
Guacamole is only as secure as the credentials protecting it. Rotate the admin password, enable MFA if your deployment supports it, and never expose the admin interface directly to the internet without at least an IP allowlist in Nginx.