VPS Tutorials
Deploy Apache Guacamole on a VPS (2026): Docker, Remote Desktop Gateway, HTTPS and Security Configuration
This tutorial shows how to deploy Apache Guacamole 1.6.0 on an Ubuntu 24.04 VPS with Docker Compose, configuring MariaDB, guacd, Caddy HTTPS, TOTP, login lockout, RDP/SSH/VNC connections, least privilege, backup, upgrades and troubleshooting.

When you need to temporarily log in to an office Windows machine, a Linux host at home, or several client servers, exposing 3389, 5900 and 22 directly to the public internet is both hard to manage and expands the attack surface. Apache Guacamole offers another structure: users only need a browser to access backend RDP, VNC, SSH and other connections through a single HTTPS gateway, without installing a dedicated client on their personal computers.
But Guacamole is not a magic bastion host that is "secure once installed." It controls remote entry points, account permissions and potentially stored target credentials; if the web admin account is stolen, an attacker may use it to reach more machines. This article uses Ubuntu 24.04, the official Apache Guacamole 1.6.0 Docker images, MariaDB, Caddy HTTPS, TOTP and the built-in login lockout, and covers database initialization, remote connections, security hardening, backup, upgrades and troubleshooting in full.
Apache Guacamole is a clientless remote desktop gateway. The browser communicates with the Guacamole web application, which hands the connection to the guacd proxy process, and guacd connects to the actual RDP, VNC, SSH, Telnet or Kubernetes target.
The deployment structure in this article is as follows:
浏览器
-> HTTPS 443
-> Caddy
-> Guacamole Web / Tomcat 8080
-> guacd 4822(仅 Docker 内网)
-> RDP 3389 / SSH 22 / VNC 5900 等目标主机
Guacamole Web
-> MariaDB 3306(仅 Docker 内网)
The most important security boundary here is: the public internet only sees Caddy's 80/443, the Guacamole 8080 binds only to the VPS loopback address, and neither guacd:4822 nor the database 3306 is mapped to the host's public network.
Apache officially warns that guacd is a passive proxy and performs no authentication whatsoever. If 4822 is exposed to an untrusted network, attackers may use it as a springboard into other systems. Therefore this article does not configure ports for guacd.
| Approach | Suitable scenarios | Client requirements | Main risks or limitations |
|---|---|---|---|
| Apache Guacamole | Multi-user, multi-protocol, unified browser entry point | Modern browser | The gateway becomes a high-value entry point; MFA and permission isolation are mandatory |
| WireGuard | Your own devices entering a trusted private network | Install a VPN client | User and connection management is less intuitive than Guacamole |
| Direct public RDP/VNC | Temporary testing | Native client | Exposes high-risk ports; not recommended for long-term use |
| SSH jump host | Linux operations, command line | SSH client | Does not directly address graphical desktops and ordinary-user access |
| Commercial bastion host/PAM | Enterprise auditing, approvals, credential vaulting | Depends on the product | Higher cost and deployment complexity |
If you only have two or three devices of your own, entering the private network through a WireGuard VPN first is often simpler. Guacamole is better suited to scenarios that need browser access, centralized connection assignment, multiple user accounts, connection history, or mixed RDP/VNC/SSH management.
Guacamole also cannot replace target host security: Windows still needs patches, NLA and strong passwords; Linux should still restrict SSH users and sudo privileges; VNC still needs reliable authentication. The gateway only changes the access path; it does not fix vulnerabilities in backend services.
Guacamole itself does not store a full desktop system, but it needs to transcode screen updates and handle audio, clipboard, file transfer and multiple concurrent connections. Latency and bandwidth usually affect the operating experience more directly than disk capacity.
| Usage scale | Suggested starting configuration | Network and storage priorities |
|---|---|---|
| 1–3 lightweight SSH/RDP sessions | 2 vCPU, 4GB RAM, 40GB SSD | Low latency, stable routing |
| 5–10 concurrent office sessions | 4 vCPU, 8GB RAM, 80GB SSD | 100Mbps or more, sustained CPU performance |
| HD desktops, audio or many concurrent users | 8 vCPU, 16GB RAM and up | High bandwidth, low jitter, capacity planning based on real measurements |
| Session recording enabled | Add a separate data disk to the above | Recording file growth, backup and retention policy |
Screen resolution, refresh rate, video content, audio and network quality all change actual traffic. Browsing a static admin interface may need only modest bandwidth, while playing video or scrolling frequently greatly increases data. Do not buy based on a fixed "X Mbps per user"; first stress-test with real workloads, then monitor peaks and monthly traffic.
The data center should be close to both the users and the target hosts. The access path is "user → Guacamole VPS → target host," and high latency in any segment adds up. If users are in Asia and the target host is in Europe, placing the gateway in the Americas usually will not improve the experience.
This article uses:
| Port | Purpose | Public policy |
|---|---|---|
22/TCP | The VPS's own SSH | Best restricted to the administrator's source IP |
80/443 TCP | Caddy and HTTPS | Open to users who need access |
8080/TCP | Guacamole Web | Bound to 127.0.0.1 only |
4822/TCP | guacd | Docker network only, not mapped |
3306/TCP | MariaDB | Docker network only, not mapped |
The target host's 3389, 22 or 5900 also does not need to be open to the whole internet. If the target is another public server, the firewall should allow only the Guacamole VPS's fixed IP; if the target is on a home or office intranet, first establish WireGuard, a site-to-site VPN or another controlled tunnel.
Update Ubuntu:
sudo apt update
sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg openssl ufw
Install Docker Engine and the Compose plugin from the official Docker repository, then verify:
docker version
docker compose version
sudo systemctl enable --now docker
sudo systemctl status docker --no-pager
For production log rotation, resource limits, rollback and secret management, you can check against the VPS Docker Compose production configuration checklist.
Create the project directory:
sudo install -d -m 750 /opt/guacamole
sudo chown "$(id -u):$(id -g)" /opt/guacamole
cd /opt/guacamole
As of August 2026, the latest stable release of Apache Guacamole is 1.6.0. This article explicitly pins the web and guacd image versions to avoid latest changing across versions unnoticed. Before deploying, you should still check the Apache Guacamole Release Archives to confirm whether a newer release exists.
Generate the MariaDB initialization SQL using the script built into the official image:
docker run --rm guacamole/guacamole:1.6.0 \
/opt/guacamole/bin/initdb.sh --mysql > initdb.sql
test -s initdb.sql
chmod 600 initdb.sql
head -n 5 initdb.sql
Guacamole does not create the JDBC authentication tables automatically; the database must be initialized first. This SQL file runs the first time the MariaDB data directory is empty; if you later reuse an existing database volume, the Docker entrypoint script will not run it again.
Create /opt/guacamole/.env with a random value:
cd /opt/guacamole
umask 077
GUAC_DB_PASSWORD=$(openssl rand -hex 32)
GUAC_ROOT_PASSWORD=$(openssl rand -hex 32)
{
printf 'MARIADB_DATABASE=guacamole_db\n'
printf 'MARIADB_USER=guacamole_user\n'
printf 'MARIADB_PASSWORD=%s\n' "$GUAC_DB_PASSWORD"
printf 'MARIADB_ROOT_PASSWORD=%s\n' "$GUAC_ROOT_PASSWORD"
} > .env
unset GUAC_DB_PASSWORD GUAC_ROOT_PASSWORD
chmod 600 .env
Confirm the variable name exists, but do not print the password value to a shared terminal or screenshot:
cut -d= -f1 .env
.env, database backups and Guacamole connection configurations may all contain sensitive information and must not be committed to a public Git repository.
Create /opt/guacamole/compose.yaml:
services:
mariadb:
image: mariadb:11.4
container_name: guacamole-mariadb
environment:
MARIADB_ROOT_PASSWORD: "${MARIADB_ROOT_PASSWORD}"
MARIADB_DATABASE: "${MARIADB_DATABASE}"
MARIADB_USER: "${MARIADB_USER}"
MARIADB_PASSWORD: "${MARIADB_PASSWORD}"
volumes:
- mariadb-data:/var/lib/mysql
- ./initdb.sql:/docker-entrypoint-initdb.d/001-guacamole.sql:ro
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 12
start_period: 30s
restart: unless-stopped
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-size: 10m
max-file: "3"
guacd:
image: guacamole/guacd:1.6.0
container_name: guacamole-guacd
environment:
LOG_LEVEL: info
restart: unless-stopped
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-size: 10m
max-file: "3"
guacamole:
image: guacamole/guacamole:1.6.0
container_name: guacamole-web
depends_on:
mariadb:
condition: service_healthy
guacd:
condition: service_started
environment:
GUACD_HOSTNAME: guacd
MYSQL_ENABLED: "true"
MYSQL_HOSTNAME: mariadb
MYSQL_DATABASE: "${MARIADB_DATABASE}"
MYSQL_USERNAME: "${MARIADB_USER}"
MYSQL_PASSWORD: "${MARIADB_PASSWORD}"
WEBAPP_CONTEXT: ROOT
REMOTE_IP_VALVE_ENABLED: "true"
TOTP_ENABLED: "true"
BAN_ENABLED: "true"
ports:
- "127.0.0.1:8080:8080"
restart: unless-stopped
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-size: 10m
max-file: "3"
volumes:
mariadb-data:
This configuration deliberately applies the following restrictions:
- Guacamole Web is mapped only to
127.0.0.1:8080; guacdhas no host port mapping and is reachable only by the web application on the Compose network;- MariaDB has no public port and persists only to a named volume;
- The database initialization SQL is mounted read-only;
- Log rotation is configured for the web, guacd and database services;
- Guacamole enables TOTP and keeps the login-failure lockout provided by default in the 1.6.0 Docker image;
RemoteIpValvelets the application behind the reverse proxy recognize the real client IP; otherwise all failed logins may appear to come from Caddy.
WEBAPP_CONTEXT=ROOT places the login page directly at / instead of the default /guacamole/. If you remove this variable, both Caddy and the access URL must be changed to the corresponding subpath; you cannot change only one side.
First let Compose expand and validate the configuration:
cd /opt/guacamole
docker compose config > /tmp/guacamole-compose-resolved.yaml
docker compose pull
docker compose up -d
docker compose ps
Check the startup logs:
docker compose logs --tail=200 mariadb
docker compose logs --tail=200 guacd
docker compose logs --tail=200 guacamole
Check the database tables:
docker compose exec mariadb sh -c \
'mariadb -u"$MARIADB_USER" -p"$MARIADB_PASSWORD" "$MARIADB_DATABASE" -e "SHOW TABLES;"'
You should see tables such as guacamole_user and guacamole_connection. If the tables are empty, first check whether MariaDB had already created a data volume before initdb.sql was generated. The initialization script runs only on the first startup with an empty data directory; do not try your luck by restarting repeatedly.
Test the web app on the VPS itself:
curl -I http://127.0.0.1:8080
sudo ss -lntp | grep ':8080'
At this point, http://VPS-IP:8080 being unreachable from the public internet is the correct result.
Temporarily access it from your personal computer via an SSH tunnel:
ssh -L 8080:127.0.0.1:8080 [email protected]
203.0.113.10 is a documentation example address and must be replaced with the VPS's real public IP. Keep the SSH session open, then open in your browser:
http://127.0.0.1:8080
The official database initialization SQL creates a default administrator:
用户名:guacadmin
密码:guacadmin
These default credentials are public information and are only for the first login. Immediately after entering the system:
- Change the
guacadminpassword to a strong random password generated by a password manager; - Complete TOTP enrollment and securely save the recovery process;
- Create a new daily-use administrator account;
- Create a non-administrator test user;
- After verifying the new account can log in, stop using the default administrator for daily work;
- Do not grant ordinary users permission to create users, modify the system or manage all connections.
If you have not yet changed the default password, do not expose the HTTPS domain to the whole internet, and do not start creating backend connections.
Point the DNS A/AAAA record for remote.example.com to the VPS. The example domain must be replaced with your own actual domain.
Add the following to the Caddyfile:
remote.example.com {
encode zstd gzip
reverse_proxy 127.0.0.1:8080
}
Caddy automatically handles the WebSocket proxying Guacamole needs; you generally do not need to write Upgrade headers manually. Validate and reload:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
sudo systemctl status caddy --no-pager
sudo journalctl -u caddy -n 100 --no-pager
Open the web port:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp comment 'Caddy HTTP'
sudo ufw allow 443/tcp comment 'Caddy HTTPS'
sudo ufw enable
sudo ufw status numbered
Do not open 8080, 4822 or 3306. For the full Caddy installation, DNS and automatic certificate process, see the complete guide to using Caddy as a reverse proxy on a VPS.
Guacamole officially recommends terminating TLS at the reverse proxy. REMOTE_IP_VALVE_ENABLED=true makes Tomcat use the client address and protocol information passed in by the proxy, which matters for login lockout, auditing and source-IP-based restrictions.
After logging in to the admin interface, create Connections in the settings. Start with a low-privilege test account and non-production targets.
Common parameters:
Protocol: SSH
Hostname: 10.20.0.10
Port: 22
Username: ops-test
Prefer a dedicated low-privilege user and SSH keys. Do not copy the VPS's root private key into every connection, and do not share high-privilege connections with ordinary Guacamole users.
Common parameters:
Protocol: RDP
Hostname: 10.20.0.20
Port: 3389
Username: remote-user
Domain: 按实际环境填写
Security mode: NLA
Windows targets should enable NLA, system updates, account lockout and firewall restrictions. If the target uses a self-signed RDP certificate, verify the certificate against the target's identity first; do not treat "ignore certificate errors" as a long-term default.
Protocol: VNC
Hostname: 10.20.0.30
Port: 5900
VNC implementations and encryption capabilities vary widely. Have Guacamole and the VNC target communicate over a VPN or trusted intranet; do not expose 5900 directly to the public internet.
If the target host cannot be reached, first test the network from the Guacamole VPS instead of repeatedly changing the web configuration:
nc -vz 10.20.0.10 22
nc -vz 10.20.0.20 3389
nc -vz 10.20.0.30 5900
The firewall on the public target should allow only the Guacamole VPS source IP. For port listening, UFW and cloud security groups, check layer by layer using the troubleshooting checklist for services that start but cannot be reached from the internet.
Do not make everyone an administrator, and do not let all users share the same target system account.
Recommended layering:
- System administrators: only responsible for Guacamole configuration and auditing;
- Connection administrators: manage specified connections, not all system users;
- Ordinary users: can only use the connections assigned to them;
- Read-only or demo users: disable unneeded clipboard, file transfer and input capabilities;
- Temporary users: set an explicit expiry time and revoke access after the task ends.
For sensitive connections, consider:
- Do not store target passwords permanently in Guacamole; require entry at login instead;
- Each user uses an independent target account for easier auditing and revocation;
- Disable unneeded clipboard, drive mapping, printing, audio and file transfer;
- Put production, test and customer environments in different connection groups;
- Restrict the times and source subnets from which users can log in;
- Regularly review Active Sessions and History.
Administrator privileges should be kept to a minimum. System administrators are usually not affected by some login restrictions, so time-based or source-address-based restrictions cannot be treated as the only defense for administrator accounts.
The official Guacamole 1.6.0 Docker image supports standard TOTP via TOTP_ENABLED=true. This article enables both database authentication and TOTP, so users must enter a dynamic verification code after their password.
Before going live, test at least:
- Both administrators and ordinary users complete TOTP enrollment;
- The server time is accurate so TOTP does not keep failing due to clock drift;
- There is a controlled recovery process for a lost phone;
- Recovery requires administrator verification rather than disabling all MFA;
- The client IP passed in by the reverse proxy is correct.
The Guacamole 1.6.0 Docker image includes a login-failure lockout extension by default. The official default threshold is 5 failures from the same source, with a 300-second lockout. It reduces the rate of online password guessing but cannot replace strong passwords, TOTP, source restrictions and log monitoring.
View logs related to failed logins and lockouts:
docker compose logs --since=1h guacamole | grep -i -E 'auth|fail|ban|invalid'
If all requests appear as 127.0.0.1 or the same Docker address, first fix client IP forwarding through the reverse proxy, then configure source-address-based policies.
Guacamole's users, connections, permissions and history live mainly in the database. Backing up only compose.yaml cannot restore this data.
Create a backup:
cd /opt/guacamole
backup_stamp=$(date +%Y%m%d-%H%M%S)
sudo install -d -m 700 /var/backups/guacamole
docker compose exec -T mariadb sh -c \
'mariadb-dump -uroot -p"$MARIADB_ROOT_PASSWORD" --single-transaction "$MARIADB_DATABASE"' \
| gzip > "/var/backups/guacamole/database-${backup_stamp}.sql.gz"
sudo tar -C /opt/guacamole -czf \
"/var/backups/guacamole/config-${backup_stamp}.tar.gz" \
compose.yaml .env initdb.sql
sudo chmod 600 /var/backups/guacamole/*
Verify the backup:
gzip -t "/var/backups/guacamole/database-${backup_stamp}.sql.gz"
sudo tar -tzf "/var/backups/guacamole/config-${backup_stamp}.tar.gz"
ls -lh /var/backups/guacamole
Backup files contain password hashes, connection information and database credentials, so they must be transferred encrypted and access-restricted. Do not leave backups only on the same VPS disk; for a full restore drill, see the VPS backup and restore drill tutorial.
Check the Guacamole Release Notes before updating. The database schema may require upgrade SQL to be run in version order; you cannot simply change the image tag and start it.
Record the current state:
cd /opt/guacamole
docker compose images
docker image inspect guacamole/guacamole:1.6.0 \
--format '{{index .RepoDigests 0}}'
docker image inspect guacamole/guacd:1.6.0 \
--format '{{index .RepoDigests 0}}'
docker compose ps
Update procedure:
- Choose a maintenance window and notify users who are active;
- Stop new connections and confirm active sessions have ended;
- Back up MariaDB,
.env, Compose and the initialization files; - Read all Schema Upgrade notes from the current version to the target version;
- Pull the new images and verify the versions;
- Run the database upgrade SQL in the official order;
- Update the web and guacd to the same version;
- Recreate the containers and test login, TOTP, SSH, RDP, VNC and audit history;
- Keep the old images and backups, and clean up only after confirming stability.
Guacamole 1.x components have some protocol compatibility, but keeping the web, guacd, extensions and database schema on the same version over time is easier to maintain. Third-party extensions must have their API compatibility confirmed separately.
cd /opt/guacamole
docker compose ps
docker compose logs --tail=200 guacamole
docker compose logs --tail=200 mariadb
docker compose config
Common causes are a missing .env variable, a database password mismatch, the database not yet healthy, missing initialization tables, or an image version that does not match the extensions.
Check whether the database was actually initialized:
docker compose exec mariadb sh -c \
'mariadb -u"$MARIADB_USER" -p"$MARIADB_PASSWORD" "$MARIADB_DATABASE" -e "SHOW TABLES; SELECT username FROM guacamole_entity WHERE type = '\''USER'\'';"'
If the database is empty, do not repeatedly change initdb.sql on an existing data volume. First determine whether the volume contains data that must be kept; only consider re-initializing when you are certain it is a fresh failed deployment.
curl -I http://127.0.0.1:8080
sudo systemctl status caddy --no-pager
sudo journalctl -u caddy -n 100 --no-pager
docker compose logs --tail=200 guacamole
sudo ss -lntp | grep -E ':443|:8080'
If 8080 fails locally, the problem is in the Guacamole container; if it succeeds locally but the domain returns 502, it is more likely Caddy, DNS or the proxy target. If the remote desktop disconnects after a few minutes, also check the upstream CDN, proxy timeouts and the client network.
First test the target port from the VPS:
nc -vz 10.20.0.10 22
nc -vz 10.20.0.20 3389
nc -vz 10.20.0.30 5900
docker compose logs --tail=200 guacd
If TCP does not connect, check the target host's listening address, system firewall, security groups, VPN routes and source IP allowlist. If TCP connects but the protocol fails, check the username, domain, NLA, certificate, VNC authentication type or SSH key format.
timedatectl status
date -u
docker compose logs --tail=200 guacamole
Make sure the VPS and phone times are automatically synchronized. Do not mask time or account-binding problems by disabling TOTP; use the controlled administrator recovery process to re-enroll instead.
Observe both network segments and server resources separately:
docker stats
ping -c 20 10.20.0.20
sudo apt install -y sysstat
iostat -xz 1 10
free -h
Lower the remote resolution and color depth, disable unneeded audio, printing and drive mapping, then test latency again. If the route from the VPS to the target host is poor, simply adding CPU to the VPS will not fix network jitter.
- Use a currently supported stable Guacamole release with matching web and guacd versions;
- Use database authentication and do not treat
user-mapping.xmlas a public production solution; - The default
guacadmin / guacadminhas been changed immediately; - TOTP is enabled for both administrators and ordinary users;
REMOTE_IP_VALVE_ENABLED=true, and auditing can see the real client IP;8080binds only to127.0.0.1;4822and3306have no host port mappings;- Caddy HTTPS works and HTTP automatically redirects to HTTPS;
- Target RDP, SSH and VNC allow access only from the Guacamole VPS or VPN subnet;
- Ordinary users have no system administration or all-connections permissions;
- Unneeded clipboard, file transfer, printing and drive mapping are disabled;
- The database,
.envand connection configurations have encrypted backups; - Database restore, TOTP recovery and image rollback have been tested;
- Login failures, active sessions, CPU, memory, bandwidth and disk are monitored;
- There is a fixed process for removing temporary users, old connections and departed staff permissions.
No. Ordinary users access it through a modern browser that supports HTML5 and WebSocket. The target host still needs the corresponding RDP, SSH or VNC service enabled.
Not entirely. Guacamole provides a browser entry point for specific remote connections; a VPN provides network-layer private network access. For home intranets or cross-cloud targets, a common approach is to first have the Guacamole VPS join the target subnet via WireGuard, then have guacd connect to internal addresses.
guacd itself does not authenticate users; it is a passive remote desktop proxy. Exposing it to an untrusted network may let attackers use it to connect to other targets. In a Docker deployment, only the Guacamole web container should be able to reach it.
A few lightweight SSH sessions can be tested, but MariaDB, Tomcat, guacd and the system all need memory. For long-term RDP/VNC access, start from 4GB and scale up based on measured concurrency, resolution and audio.
Technically yes, but it expands the impact if the gateway is compromised. For sensitive connections, prefer having users enter credentials each time, or use enterprise identity, dedicated low-privilege accounts and supported key management solutions.
Guacamole Web uses HTTP/HTTPS and WebSocket, so technically it can go through a proxy that supports WebSocket, but long sessions, timeouts, logging, access policies and terms of service must be tested in practice. Whether or not you use a CDN, do not expose guacd:4822 or the database through it.
The official JDBC initialization SQL creates guacadmin / guacadmin. It is only for the first login and must be changed immediately before exposing the service to the public internet, with TOTP enabled.
The standard for a completed Guacamole deployment is not "the login page opens," but that only HTTPS is exposed publicly, default credentials have been changed, TOTP and lockout are in effect, the real client IP is auditable, target ports are not exposed to the whole internet, ordinary user permissions are minimized, and database backup and restore have actually been rehearsed. First run the full chain with a low-privilege test host, then gradually connect production systems.