Complete setup for 20 concurrent users with web-based remote desktop access via Apache Guacamole, RDP, and Cloudflare tunnel on Ubuntu 24.04 LTS.
- Bare Ubuntu 24.04 LTS server
- SSH access as root
- Domain configured with Cloudflare DNS
- Minimum 8GB RAM, 4+ CPU cores recommended for 20 concurrent users
- Frontend: Apache Guacamole web interface (browser-based)
- Backend: xrdp providing RDP sessions with XFCE desktop
- Database: PostgreSQL for user/connection management
- Containers: Podman containers for Guacamole services
- Tunnel: Cloudflare tunnel for secure HTTPS access
- Users: 20 individual accounts with dedicated desktop sessions
apt update
apt upgrade -y
apt install curl wget vim net-tools lsof
Install XFCE desktop environment:
apt install xfce4 xfce4-goodies xorg dbus-x11 x11-xserver-utils
Install xrdp:
apt install xrdp
systemctl enable xrdp
systemctl start xrdp
Configure xrdp to use XFCE:
echo "startxfce4" > /etc/skel/.xsession
chmod +x /etc/skel/.xsession
Verify xrdp is running:
systemctl status xrdp
netstat -tlnp | grep 3389
Create 20 system users for RDP access:
for i in {1..20}; do
useradd -m -s /bin/bash user$i
echo "user$i:UserPass$i" | chpasswd
cp /etc/skel/.xsession /home/user$i/
chown user$i:user$i /home/user$i/.xsession
echo "Created user$i with password UserPass$i"
done
apt install podman
Configure container registries:
cat >> /etc/containers/registries.conf << 'EOF'
[registries.search]
registries = ['docker.io', 'quay.io']
EOF
Create Guacamole directory:
mkdir -p /opt/guacamole
cd /opt/guacamole
Create podman network:
podman network create guacamole-net
Start PostgreSQL database:
podman run -d --name guacamole-postgres \
--network guacamole-net \
-e POSTGRES_DB=guacamole_db \
-e POSTGRES_USER=guacamole_user \
-e POSTGRES_PASSWORD=guacamole_pass \
docker.io/postgres:13
Wait for database to initialize:
sleep 15
Initialize Guacamole database schema:
# Generate the database schema
podman run --rm docker.io/guacamole/guacamole /opt/guacamole/bin/initdb.sh --postgresql > /tmp/schema.sql
# Copy schema to database container
podman cp /tmp/schema.sql guacamole-postgres:/tmp/schema.sql
# Apply schema to database
podman exec -it guacamole-postgres psql -U guacamole_user -d guacamole_db -f /tmp/schema.sql
# Verify schema was applied (should show multiple tables)
podman exec -it guacamole-postgres psql -U guacamole_user -d guacamole_db -c "\dt"
Start Guacamole daemon:
podman run -d --name guacd \
--network guacamole-net \
docker.io/guacamole/guacd:latest
Start Guacamole web application:
podman run -d --name guacamole \
--network guacamole-net \
-p 8080:8080 \
-e GUACD_HOSTNAME=guacd \
-e POSTGRESQL_DATABASE=guacamole_db \
-e POSTGRESQL_USER=guacamole_user \
-e POSTGRESQL_PASSWORD=guacamole_pass \
-e POSTGRESQL_HOSTNAME=guacamole-postgres \
docker.io/guacamole/guacamole:latest
Verify all containers are running:
podman ps
Test Guacamole web interface:
curl -I localhost:8080/guacamole/
Stop current containers:
podman stop guacamole guacd guacamole-postgres
podman rm guacamole guacd guacamole-postgres
Restart with host networking:
# PostgreSQL
podman run -d --name guacamole-postgres \
--network host \
-e POSTGRES_DB=guacamole_db \
-e POSTGRES_USER=guacamole_user \
-e POSTGRES_PASSWORD=guacamole_pass \
docker.io/postgres:13
# Wait for database
sleep 15
# Reinitialize database schema (CRITICAL STEP)
podman run --rm docker.io/guacamole/guacamole /opt/guacamole/bin/initdb.sh --postgresql > /tmp/schema.sql
podman cp /tmp/schema.sql guacamole-postgres:/tmp/schema.sql
podman exec -it guacamole-postgres psql -U guacamole_user -d guacamole_db -f /tmp/schema.sql
# Verify tables exist (should show guacamole_user, guacamole_connection, etc.)
podman exec -it guacamole-postgres psql -U guacamole_user -d guacamole_db -c "\dt"
# Guacamole daemon
podman run -d --name guacd \
--network host \
docker.io/guacamole/guacd:latest
# Guacamole web app
podman run -d --name guacamole \
--network host \
-e GUACD_HOSTNAME=localhost \
-e POSTGRESQL_DATABASE=guacamole_db \
-e POSTGRESQL_USER=guacamole_user \
-e POSTGRESQL_PASSWORD=guacamole_pass \
-e POSTGRESQL_HOSTNAME=localhost \
docker.io/guacamole/guacamole:latest
Access Guacamole web interface:
- Open browser to
http://localhost:8080/guacamole/
- Login with
guacadmin
/guacadmin
- Go to Settings → Connections → New Connection
- Create test connection:
- Name: User 1 Desktop
- Protocol: RDP
- Hostname: localhost
- Port: 3389
- Username: user1
- Password: UserPass1
- Save and test the connection
Install Python dependencies:
apt install python3-pip
pip3 install requests
Create the bulk setup script:
cat > /opt/guacamole/bulk_setup.py << 'EOF'
#!/usr/bin/env python3
"""
Bulk setup script for Guacamole users and RDP connections
Creates 20 users with their own login and dedicated RDP connection
"""
import requests
import json
import sys
# Configuration
GUACAMOLE_URL = "http://localhost:8080/guacamole"
ADMIN_USER = "guacadmin"
ADMIN_PASS = "guacadmin"
def get_auth_token():
"""Get authentication token from Guacamole"""
auth_url = f"{GUACAMOLE_URL}/api/tokens"
auth_data = {
"username": ADMIN_USER,
"password": ADMIN_PASS
}
response = requests.post(auth_url, data=auth_data)
if response.status_code != 200:
print(f"Failed to authenticate: {response.status_code}")
sys.exit(1)
return response.json()["authToken"]
def create_user(token, username, password):
"""Create a new Guacamole user"""
headers = {"Content-Type": "application/json"}
user_url = f"{GUACAMOLE_URL}/api/session/data/mysql/users?token={token}"
user_data = {
"username": username,
"password": password,
"attributes": {
"disabled": "",
"expired": "",
"access-window-start": "",
"access-window-end": "",
"valid-from": "",
"valid-until": "",
"timezone": "",
"full-name": f"User {username[-2:]}",
"email-address": "",
"organization": "",
"organizational-role": ""
}
}
response = requests.post(user_url, headers=headers, data=json.dumps(user_data))
if response.status_code == 200:
print(f"✓ Created user: {username}")
return True
else:
print(f"✗ Failed to create user {username}: {response.status_code}")
return False
def create_connection(token, name, rdp_username, rdp_password):
"""Create an RDP connection"""
headers = {"Content-Type": "application/json"}
conn_url = f"{GUACAMOLE_URL}/api/session/data/mysql/connections?token={token}"
connection_data = {
"name": name,
"protocol": "rdp",
"parameters": {
"hostname": "localhost",
"port": "3389",
"username": rdp_username,
"password": rdp_password,
"security": "any",
"ignore-cert": "true",
"color-depth": "32",
"width": "1024",
"height": "768",
"dpi": "96"
},
"attributes": {
"max-connections": "1",
"max-connections-per-user": "1",
"weight": "",
"failover-only": "",
"guacd-port": "",
"guacd-encryption": "",
"guacd-hostname": ""
}
}
response = requests.post(conn_url, headers=headers, data=json.dumps(connection_data))
if response.status_code == 200:
print(f"✓ Created connection: {name}")
return response.json()["identifier"]
else:
print(f"✗ Failed to create connection {name}: {response.status_code}")
return None
def grant_connection_permission(token, username, connection_id):
"""Grant user permission to access their connection"""
headers = {"Content-Type": "application/json"}
perm_url = f"{GUACAMOLE_URL}/api/session/data/mysql/users/{username}/permissions?token={token}"
permissions = [
{
"op": "add",
"path": f"/connectionPermissions/{connection_id}",
"value": "READ"
}
]
response = requests.patch(perm_url, headers=headers, data=json.dumps(permissions))
if response.status_code == 204:
print(f"✓ Granted connection permission to {username}")
return True
else:
print(f"✗ Failed to grant permission to {username}: {response.status_code}")
return False
def main():
print("Starting Guacamole bulk setup...")
print("=" * 50)
# Get authentication token
print("Authenticating with Guacamole...")
token = get_auth_token()
print("✓ Authentication successful")
print()
# Create 20 users and connections
for i in range(1, 21):
user_num = f"{i:02d}" # 01, 02, 03, etc.
guac_username = f"user{user_num}"
guac_password = f"GuacPass{i}"
rdp_username = f"user{i}"
rdp_password = f"UserPass{i}"
connection_name = f"Desktop {i}"
print(f"Setting up User {i}...")
# Create Guacamole user account
if create_user(token, guac_username, guac_password):
# Create RDP connection
connection_id = create_connection(token, connection_name, rdp_username, rdp_password)
if connection_id:
# Grant user permission to their connection
grant_connection_permission(token, guac_username, connection_id)
print()
print("=" * 50)
print("Setup complete!")
print()
print("User Access Information:")
print("=" * 50)
print("URL: https://subdomain.yourdomain.com/guacamole/")
print()
print("User Credentials:")
for i in range(1, 21):
user_num = f"{i:02d}"
print(f"User {user_num}: username=user{user_num}, password=GuacPass{i}")
print()
print("Each user will see their own 'Desktop X' connection after login.")
if __name__ == "__main__":
# Check if requests is available
try:
import requests
except ImportError:
print("Error: 'requests' module not found.")
print("Install it with: pip3 install requests")
sys.exit(1)
main()
EOF
chmod +x /opt/guacamole/bulk_setup.py
Run the bulk setup:
cd /opt/guacamole
python3 bulk_setup.py
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
dpkg -i cloudflared-linux-amd64.deb
Authenticate with Cloudflare:
cloudflared tunnel login
Create tunnel:
cloudflared tunnel create guacamole-tunnel
Configure tunnel (replace YOUR_TUNNEL_ID with actual tunnel ID):
cat > ~/.cloudflared/config.yml << 'EOF'
tunnel: YOUR_TUNNEL_ID
credentials-file: /root/.cloudflared/YOUR_TUNNEL_ID.json
ingress:
- hostname: subdomain.yourdomain.com
service: http://localhost:8080
- service: http_status:404
EOF
Add DNS record:
cloudflared tunnel route dns guacamole-tunnel subdomain.yourdomain.com
Start tunnel:
cloudflared tunnel run guacamole-tunnel
Create Guacamole service:
cat > /etc/systemd/system/guacamole.service << 'EOF'
[Unit]
Description=Guacamole Containers
After=network.target
[Service]
Type=forking
RemainAfterExit=yes
ExecStart=/bin/bash -c '\
podman run -d --name guacamole-postgres \
--network host \
-e POSTGRES_DB=guacamole_db \
-e POSTGRES_USER=guacamole_user \
-e POSTGRES_PASSWORD=guacamole_pass \
docker.io/postgres:13 && \
sleep 10 && \
podman run -d --name guacd \
--network host \
docker.io/guacamole/guacd:latest && \
podman run -d --name guacamole \
--network host \
-e GUACD_HOSTNAME=localhost \
-e POSTGRESQL_DATABASE=guacamole_db \
-e POSTGRESQL_USER=guacamole_user \
-e POSTGRESQL_PASSWORD=guacamole_pass \
-e POSTGRESQL_HOSTNAME=localhost \
docker.io/guacamole/guacamole:latest'
ExecStop=/bin/bash -c '\
podman stop guacamole guacd guacamole-postgres && \
podman rm guacamole guacd guacamole-postgres'
Restart=always
[Install]
WantedBy=multi-user.target
EOF
Create Cloudflare tunnel service:
cat > /etc/systemd/system/cloudflared.service << 'EOF'
[Unit]
Description=Cloudflare Tunnel
After=network.target
[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/cloudflared tunnel run guacamole-tunnel
Restart=always
[Install]
WantedBy=multi-user.target
EOF
Enable and start services:
systemctl enable guacamole cloudflared xrdp
systemctl start guacamole cloudflared
- Navigate to:
https://subdomain.yourdomain.com/guacamole/
- Login credentials:
- User 01:
user01
/GuacPass1
- User 02:
user02
/GuacPass2
- ...
- User 20:
user20
/GuacPass20
- User 01:
- Click their desktop connection (e.g., "Desktop 1")
- Access their XFCE desktop directly in the browser
- Each user gets a dedicated XFCE desktop session
- Sessions persist between browser connections
- Full desktop environment with applications
- Copy/paste between local machine and remote desktop
- File transfer capabilities
systemctl status xrdp guacamole cloudflared
podman ps
journalctl -u guacamole -f
journalctl -u cloudflared -f
podman logs guacamole
systemctl restart guacamole
systemctl restart cloudflared
# Add system user
useradd -m -s /bin/bash userX
echo "userX:UserPassX" | chpasswd
cp /etc/skel/.xsession /home/userX/
chown userX:userX /home/userX/.xsession
# Add to Guacamole via web interface or modify bulk_setup.py
- Network Security: All traffic encrypted via Cloudflare tunnel
- User Isolation: Each user has separate system account and RDP session
- Authentication: Multi-layer authentication (Guacamole + RDP)
- Access Control: Users can only access their assigned desktop
- Session Management: Automatic session cleanup on disconnect
"ERROR: relation 'guacamole_user' does not exist"
- This means the database schema wasn't properly initialized
- Stop Guacamole:
podman stop guacamole
- Reinitialize database:
# Drop and recreate database podman exec -it guacamole-postgres psql -U guacamole_user -d postgres -c "DROP DATABASE IF EXISTS guacamole_db;" podman exec -it guacamole-postgres psql -U guacamole_user -d postgres -c "CREATE DATABASE guacamole_db;" # Apply fresh schema podman run --rm docker.io/guacamole/guacamole /opt/guacamole/bin/initdb.sh --postgresql > /tmp/schema.sql podman cp /tmp/schema.sql guacamole-postgres:/tmp/schema.sql podman exec -it guacamole-postgres psql -U guacamole_user -d guacamole_db -f /tmp/schema.sql # Restart Guacamole podman start guacamole
"An error has occurred and this action cannot be completed"
- Usually indicates database connection or schema issues
- Check Guacamole logs:
podman logs guacamole
- Verify database schema exists:
podman exec -it guacamole-postgres psql -U guacamole_user -d guacamole_db -c "\dt"
- If no tables exist, reinitialize schema as above
- Check containers:
podman ps
- Check logs:
podman logs guacamole
- Verify port 8080:
netstat -tlnp | grep 8080
- Check xrdp status:
systemctl status xrdp
- Verify user credentials
- Check system user exists:
id userX
- Verify tunnel config:
cat ~/.cloudflared/config.yml
- Check DNS record:
nslookup subdomain.yourdomain.com
- Review logs:
journalctl -u cloudflared
- Restart PostgreSQL container:
podman restart guacamole-postgres
- Check database schema exists:
podman exec -it guacamole-postgres psql -U guacamole_user -d guacamole_db -c "\dt"
- Reinitialize schema if tables missing:
# Generate fresh schema podman run --rm docker.io/guacamole/guacamole /opt/guacamole/bin/initdb.sh --postgresql > /tmp/fresh_schema.sql # Apply to database podman cp /tmp/fresh_schema.sql guacamole-postgres:/tmp/fresh_schema.sql podman exec -it guacamole-postgres psql -U guacamole_user -d guacamole_db -f /tmp/fresh_schema.sql # Restart Guacamole after schema fix podman restart guacamole
- Check database connectivity:
podman exec -it guacamole-postgres psql -U guacamole_user -d guacamole_db -c "SELECT version();"
- RAM: 16GB+ recommended (1GB per concurrent user)
- CPU: 8+ cores recommended
- Storage: SSD recommended for user home directories
- Network: 1Gbps+ connection for optimal performance
# Increase file descriptors
echo '* soft nofile 65536' >> /etc/security/limits.conf
echo '* hard nofile 65536' >> /etc/security/limits.conf
# Optimize kernel parameters
echo 'net.core.somaxconn = 65536' >> /etc/sysctl.conf
echo 'net.ipv4.tcp_max_syn_backlog = 65536' >> /etc/sysctl.conf
sysctl -p
- Monitor disk usage in user home directories
- Check container logs for errors
- Update containers periodically
- Backup Guacamole database
- Monitor system resources
podman exec guacamole-postgres pg_dump -U guacamole_user guacamole_db > guacamole_backup.sql
podman exec -i guacamole-postgres psql -U guacamole_user -d guacamole_db < guacamole_backup.sql
This setup provides a complete, scalable solution for 20 concurrent users to access individual XFCE desktop sessions via web browser. The architecture uses modern containerization with Podman, secure tunneling via Cloudflare, and efficient RDP for desktop sessions.
Key Features:
- Browser-based access (no client software required)
- Individual user accounts and isolated sessions
- Secure HTTPS access via Cloudflare tunnel
- Persistent desktop sessions
- Scalable architecture
- Full XFCE desktop environment
- Copy/paste and file transfer support