Remote desktop access is one of those services that quietly becomes critical after a team grows beyond a single administrator. Engineers need break-glass access to a Windows jump box, finance may need a legacy desktop application, and support teams often need a controlled way to reach internal systems without exposing RDP or SSH directly to the internet. Apache Guacamole fits that job well because users connect through a browser, while the actual RDP, SSH, or VNC sessions stay behind the server.
This production guide shows how to deploy Apache Guacamole on Ubuntu with Docker Compose, PostgreSQL for persistent configuration, and Caddy for automatic TLS. The design is intentionally simple: one host, one public HTTPS endpoint, one database, and a repeatable backup path. It is a good starting point for small teams that need secure browser-based access now, while still leaving room to add SSO, VPN-only access, audit logging, and stricter network segmentation later.
Architecture/flow overview
The public request flow is: user browser → Caddy over HTTPS → Guacamole web application → guacd proxy daemon → internal RDP, SSH, or VNC target. PostgreSQL stores users, connection definitions, permissions, and history. The important security point is that Caddy is the only component exposed publicly; guacd and PostgreSQL stay on an internal Docker network. Target systems should also restrict inbound access to the Guacamole host instead of accepting RDP or SSH from arbitrary office or home IP addresses.
For a first production deployment, use a dedicated DNS name such as remote.example.com, a small Ubuntu server, firewall rules that allow only 80 and 443 from the internet, and private routing from the Guacamole host to the systems it manages. If you already use a VPN or mesh network, place the Guacamole host inside that private network and expose the web portal only to trusted users.
Prerequisites
- Ubuntu 22.04 or 24.04 server with sudo access.
- A DNS record pointing
remote.example.comto the server. - Docker Engine and Docker Compose plugin installed.
- Firewall access for ports 80 and 443.
- Private network reachability from this host to the RDP, SSH, or VNC targets.
- A password manager for database, admin, and connection credentials.
Step-by-step deployment
1. Create the project directory
Keep the stack under /opt so configuration, backups, and operational scripts are easy to find. The initdb directory is used once to initialize Guacamole’s PostgreSQL schema.
sudo mkdir -p /opt/guacamole/{initdb,backups}
sudo chown -R "$USER:$USER" /opt/guacamole
cd /opt/guacamole
Manual-copy fallback: if the copy button is unavailable, select the command text inside the block and copy it manually.
2. Generate strong secrets
Guacamole stores connection definitions in PostgreSQL, so the database password should be unique and long. Do not reuse an administrator password or a password from another service.
openssl rand -base64 32
openssl rand -base64 32
Manual-copy fallback: copy these commands manually if JavaScript is stripped by the editor.
3. Create the environment file
Use .env so Compose can interpolate secrets without hard-coding them into the YAML. Restrict permissions because this file contains the PostgreSQL password.
cat > .env <<'EOF'
POSTGRES_DB=guacamole_db
POSTGRES_USER=guacamole_user
POSTGRES_PASSWORD=replace_with_a_very_long_random_password
GUACAMOLE_HOSTNAME=remote.example.com
EOF
chmod 600 .env
Manual-copy fallback: copy the snippet manually and replace the placeholder before starting the stack.
4. Generate the Guacamole database schema
The official image can print the PostgreSQL initialization SQL. Save it into initdb before the database container starts for the first time.
docker run --rm guacamole/guacamole:1.5.5 \
/opt/guacamole/bin/initdb.sh --postgresql \
> /opt/guacamole/initdb/001-initdb.sql
Manual-copy fallback: select and copy the command if the button does not work.
5. Create Docker Compose and Caddy configuration
This Compose file defines four services: PostgreSQL, guacd, the Guacamole web app, and Caddy. Caddy handles public TLS and proxies to Guacamole over the private Compose network.
cat > compose.yml <<'EOF'
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
env_file: .env
volumes:
- postgres_data:/var/lib/postgresql/data
- ./initdb:/docker-entrypoint-initdb.d:ro
networks: [internal]
guacd:
image: guacamole/guacd:1.5.5
restart: unless-stopped
networks: [internal]
guacamole:
image: guacamole/guacamole:1.5.5
restart: unless-stopped
env_file: .env
environment:
GUACD_HOSTNAME: guacd
POSTGRESQL_HOSTNAME: postgres
POSTGRESQL_DATABASE: ${POSTGRES_DB}
POSTGRESQL_USER: ${POSTGRES_USER}
POSTGRESQL_PASSWORD: ${POSTGRES_PASSWORD}
depends_on: [postgres, guacd]
networks: [internal]
caddy:
image: caddy:2-alpine
restart: unless-stopped
env_file: .env
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on: [guacamole]
networks: [internal]
networks:
internal:
volumes:
postgres_data:
caddy_data:
caddy_config:
EOF
cat > Caddyfile <<'EOF'
{$GUACAMOLE_HOSTNAME} {
encode zstd gzip
reverse_proxy guacamole:8080
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
EOF
Manual-copy fallback: copy each file block manually if clipboard automation is unavailable.
6. Start the stack
Pull images, start containers, and watch logs until Caddy obtains a certificate and Guacamole responds. The first database start runs the initialization SQL automatically.
docker compose pull
docker compose up -d
docker compose ps
docker compose logs -f caddy guacamole
Manual-copy fallback: copy the commands manually from the block above.
Configuration and secrets handling
After the first login, immediately change the default Guacamole administrator password. Create named user groups for administrators, operators, and read-only support users instead of sharing one account. For each connection, store only the credentials needed for that target. If possible, use per-user credentials or short-lived vault-generated passwords rather than a single shared domain administrator account.
Limit what Guacamole can reach. A practical baseline is to allow outbound access from the Guacamole server only to approved RDP, SSH, or VNC targets. On Windows hosts, allow RDP from the Guacamole server’s private IP, enable Network Level Authentication, and keep local administrator membership tight. On Linux hosts, prefer SSH keys scoped to named users, disable password login where possible, and require sudo explicitly.
For higher assurance, place Caddy behind your identity-aware proxy or SSO layer, then still keep Guacamole’s own accounts and permissions. Guacamole supports extensions for additional authentication patterns, but starting with a clean local deployment makes troubleshooting much easier. Once the base service is stable, add SSO in a controlled change window and verify lockout recovery before removing local emergency access.
Verification
Verify the service at three layers: public HTTPS, Guacamole application health, and an actual remote session. A login page alone does not prove that guacd can reach target hosts.
curl -I https://remote.example.com
docker compose ps
docker compose logs --tail=80 guacamole guacd postgres
Manual-copy fallback: copy the verification commands manually if the button is not active.
Then create one SSH connection and one RDP connection in the Guacamole UI. Confirm that session recording, clipboard behavior, and file transfer settings match your policy. If you enable file transfer, test where uploaded files land and ensure operators know the cleanup process.
Backups and routine operations
The most important backup is PostgreSQL because it contains users, connection definitions, and permissions. Caddy data is also useful because it stores certificates, but it can be recreated. Schedule database backups and test restore on a separate host before you consider the deployment production-ready.
cd /opt/guacamole
mkdir -p backups
docker compose exec -T postgres pg_dump \
-U "$POSTGRES_USER" "$POSTGRES_DB" \
> "backups/guacamole-$(date +%F-%H%M).sql"
find backups -type f -name 'guacamole-*.sql' -mtime +14 -delete
Manual-copy fallback: copy the backup command manually and run it from /opt/guacamole.
For upgrades, read the Guacamole release notes, take a fresh database backup, update image tags in compose.yml, and restart during a maintenance window. Avoid floating latest tags; pin versions so rollback is predictable.
Common issues and fixes
Caddy cannot issue a certificate
Confirm the DNS record points to the server and that inbound ports 80 and 443 are open. If another process is already bound to those ports, stop it or move the service before retrying.
The login page loads but sessions fail
Check guacd logs and test network reachability from the Guacamole host to the target. Many failures are firewall-related: the web app is healthy, but the target blocks RDP, SSH, or VNC from the container host.
Database initialization did not run
The schema scripts run only when the PostgreSQL data directory is empty. If you started the database before generating 001-initdb.sql, remove the empty test volume or recreate the stack after confirming there is no production data.
Users have too much access
Review Guacamole connection groups and permissions. Do not make everyone an administrator. Split production servers, staging servers, and vendor access into separate groups, and audit memberships regularly.
FAQ
Can Guacamole replace a VPN?
It can reduce the need to expose desktop protocols, but it is not a universal VPN replacement. Use it for controlled browser-based administrative access, and keep broader private network access behind a VPN or mesh network.
Should I expose RDP directly if Guacamole is available?
No. Keep RDP, SSH, and VNC reachable only from trusted private networks or the Guacamole host. The value of the design is that users authenticate through HTTPS while target protocols stay internal.
How should I handle administrator recovery?
Keep one emergency local administrator account with a strong password stored in a password manager. Test the recovery process after SSO or access-policy changes so you do not lock the team out.
Does this deployment support multiple teams?
Yes, but model teams with groups and connection groups. Assign permissions deliberately and review them during onboarding and offboarding. Avoid shared accounts whenever possible.
What should be monitored?
Monitor HTTPS availability, container restarts, disk usage, PostgreSQL backups, and failed login patterns. Also review Guacamole history for unusual session timing or unexpected target access.
Can I add SSO later?
Yes. Stabilize the base deployment first, then add SSO in a planned change. Keep local emergency access until SSO login, logout, group mapping, and recovery have been tested.
Internal links
- Deploy NetBird with Kubernetes, Helm, and cert-manager for private network access patterns.
- Deploy Vaultwarden with Docker Compose, NGINX, and PostgreSQL for secret management ideas.
- Deploy Keycloak with Docker Compose, Traefik, and PostgreSQL if you plan to centralize identity.
Talk to us
If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.