Deploy Guacamole using Podman Kube
mkdir -p /mnt/containers/guacamole/{storage/drive,storage/record,data/postgres,init,nginx/conf.d,nginx/certs}
├── data
│ └── postgres
├── docker-compose.yml
├── gencert.sh
├── guacamole.yml
├── init
│ └── initdb.sql
├── nginx
│ ├── certs
│ │ ├── guacamole.crt
│ │ └── guacamole.key
│ └── conf.d
│ ├── default.conf
│ └── rate-limit.conf
└── storage
├── drive
└── record
# Internal pod network
podman network create guacamole_net
# Macvlan for libvirt
podman network create \
--driver macvlan \
--opt parent=virbr0 \
--subnet 192.168.122.0/24 \
--gateway 192.168.122.1 \
--ip-range 192.168.122.240/28 \
libvirt_net
# Generate init SQL if not already done
podman run --rm guacamole/guacamole:1.6.0 \
/opt/guacamole/bin/initdb.sh --postgresql \
> /mnt/containers/guacamole/init/initdb.sql
dnf install -y certbot
certbot certonly --standalone -d your-domain.com
cp /etc/letsencrypt/live/your-domain.com/fullchain.pem \
/mnt/containers/guacamole/nginx/certs/
cp /etc/letsencrypt/live/your-domain.com/privkey.pem \
/mnt/containers/guacamole/nginx/certs/
File: /mnt/containers/guacamole/gencert.sh
#! /usr/bin/env bash
set -x
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
-keyout nginx/certs/guacamole.key -out nginx/certs/guacamole.crt \
-subj ' /CN=guacamole.example.lab' \
-addext ' subjectAltName=DNS:guacamole.example.lab,DNS:*.example.lab'
File: /mnt/containers/guacamole/guacamole.yml
---
apiVersion : v1
kind : Secret
metadata :
name : guacamole-secret
type : Opaque
stringData :
POSTGRES_DB : guacamole_db
POSTGRES_USER : guacamole_user
POSTGRES_PASSWORD : " ChangeThisPassword123"
POSTGRESQL_DATABASE : guacamole_db
POSTGRESQL_USERNAME : guacamole_user
POSTGRESQL_PASSWORD : " ChangeThisPassword123"
---
apiVersion : v1
kind : Pod
metadata :
name : guacamole
labels :
app : guacamole
annotations :
io.podman.annotations.network : " guacamole_net,libvirt_net"
io.podman.annotations.label : " disable"
spec :
restartPolicy : Always
containers :
- name : postgres
image : postgres:15.7-alpine
env :
- name : PGDATA
value : /var/lib/postgresql/data/guacamole
- name : POSTGRES_DB
valueFrom :
secretKeyRef :
name : guacamole-secret
key : POSTGRES_DB
- name : POSTGRES_USER
valueFrom :
secretKeyRef :
name : guacamole-secret
key : POSTGRES_USER
- name : POSTGRES_PASSWORD
valueFrom :
secretKeyRef :
name : guacamole-secret
key : POSTGRES_PASSWORD
volumeMounts :
- name : postgres-data
mountPath : /var/lib/postgresql/data
- name : postgres-init
mountPath : /docker-entrypoint-initdb.d
readinessProbe :
exec :
command :
- pg_isready
- -U
- guacamole_user
- -d
- guacamole_db
initialDelaySeconds : 10
periodSeconds : 5
failureThreshold : 10
- name : guacd
image : guacamole/guacd:1.6.0
volumeMounts :
- name : drive-storage
mountPath : /drive
- name : record-storage
mountPath : /record
readinessProbe :
tcpSocket :
port : 4822
initialDelaySeconds : 5
periodSeconds : 5
failureThreshold : 5
- name : guacamole
image : guacamole/guacamole:1.6.0
env :
- name : GUACD_HOSTNAME
value : localhost
- name : GUACD_PORT
value : " 4822"
- name : POSTGRESQL_HOSTNAME
value : localhost
- name : POSTGRESQL_PORT
value : " 5432"
- name : POSTGRESQL_DATABASE
valueFrom :
secretKeyRef :
name : guacamole-secret
key : POSTGRESQL_DATABASE
- name : POSTGRESQL_USERNAME
valueFrom :
secretKeyRef :
name : guacamole-secret
key : POSTGRESQL_USERNAME
- name : POSTGRESQL_PASSWORD
valueFrom :
secretKeyRef :
name : guacamole-secret
key : POSTGRESQL_PASSWORD
- name : RECORDING_SEARCH_PATH
value : /record
ports :
- containerPort : 8080
protocol : TCP
volumeMounts :
- name : record-storage
mountPath : /record
readinessProbe :
httpGet :
path : /guacamole/
port : 8080
initialDelaySeconds : 30
periodSeconds : 10
failureThreshold : 10
resources :
requests :
memory : " 256Mi"
cpu : " 250m"
limits :
memory : " 512Mi"
cpu : " 500m"
- name : nginx
image : nginx:1.27-alpine
ports :
- containerPort : 80
hostPort : 80
protocol : TCP
- containerPort : 443
hostPort : 443
protocol : TCP
volumeMounts :
- name : nginx-conf
mountPath : /etc/nginx/conf.d
readOnly : true
- name : nginx-certs
mountPath : /etc/nginx/certs
readOnly : true
volumes :
- name : postgres-data
hostPath :
path : /mnt/containers/guacamole/data/postgres
type : DirectoryOrCreate
- name : postgres-init
hostPath :
path : /mnt/containers/guacamole/init
type : Directory
- name : drive-storage
hostPath :
path : /mnt/containers/guacamole/storage/drive
type : DirectoryOrCreate
- name : record-storage
hostPath :
path : /mnt/containers/guacamole/storage/record
type : DirectoryOrCreate
- name : nginx-conf
hostPath :
path : /mnt/containers/guacamole/nginx/conf.d
type : DirectoryOrCreate
- name : nginx-certs
hostPath :
path : /mnt/containers/guacamole/nginx/certs
type : DirectoryOrCreate
File: /mnt/containers/guacamole/nginx/conf.d/default.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80 ;
server_name guacamole.example.lab;
return 301 https://$host$request_uri ;
}
server {
listen 443 ssl ;
server_name guacamole.example.lab;
ssl_certificate /etc/nginx/certs/guacamole.crt;
ssl_certificate_key /etc/nginx/certs/guacamole.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_session_cache shared:SSL:10m ;
ssl_session_timeout 10m ;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block" ;
# Static assets — rewrite then proxy, no rate limit
location ~ * \.(svg|css|js|png|jpg|gif|ico|woff|woff2|ttf)$ {
rewrite ^/( .*) $ /guacamole/$1 break ;
proxy_pass http ://localhost:8080 ;
proxy_buffering on;
proxy_http_version 1.1;
proxy_set_header Host $host ;
proxy_set_header X-Real-IP $remote_addr ;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for ;
proxy_set_header X-Forwarded-Proto $scheme ;
expires 1d ;
access_log off;
}
# Everything else — rate limited, WebSocket supported
location / {
proxy_pass http ://localhost:8080 /guacamole/;
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade ;
proxy_set_header Connection $connection_upgrade ;
proxy_set_header Host $host ;
proxy_set_header X-Real-IP $remote_addr ;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for ;
proxy_set_header X-Forwarded-Proto $scheme ;
proxy_read_timeout 3600s ;
proxy_send_timeout 3600s ;
proxy_connect_timeout 10s ;
limit_req zone =guac_limit burst=30 nodelay;
limit_req_status 429 ;
access_log off;
}
}
File: /mnt/containers/guacamole/nginx/conf.d/rate-limit.conf
limit_req_zone $binary_remote_addr zone =guac_limit:10m rate=60r /m;
vim /usr/local/bin/guacamole-backup.sh
chmod +x /usr/local/bin/guacamole-backup.sh
# Schedule daily at 2am
echo " 0 2 * * * root /usr/local/bin/guacamole-backup.sh" > /etc/cron.d/guacamole-backup
#! /bin/bash
BACKUP_DIR=/mnt/backups/guacamole
DATE=$( date +%F)
mkdir -p $BACKUP_DIR
# Dump postgres
podman exec guacamole-postgres \
pg_dump -U guacamole_user guacamole_db \
| gzip > $BACKUP_DIR /guacamole_db_$DATE .sql.gz
# Keep only last 7 days
find $BACKUP_DIR -name " *.sql.gz" -mtime +7 -delete
Docker Compose alternative
networks :
guacamole_net :
driver : bridge
libvirt_net :
driver : macvlan
driver_opts :
parent : virbr0
ipam :
config :
- subnet : 192.168.122.0/24
gateway : 192.168.122.1
ip_range : 192.168.122.240/28
services :
guacd :
image : guacamole/guacd:1.6.0
container_name : guacd
restart : always
networks :
- guacamole_net
- libvirt_net
volumes :
- ./storage/drive:/drive:rw
- ./storage/record:/record:rw
healthcheck :
test : ["CMD-SHELL", "nc -z localhost 4822 || exit 1"]
interval : 10s
timeout : 5s
retries : 5
start_period : 10s
postgres :
image : postgres:15-alpine
container_name : guacamole_db
restart : always
env_file :
- /mnt/containers/guacamole/.env
environment :
PG_DATA : /var/lib/postgresql/data/guacamole
POSTGRES_DB : guacamole_db
POSTGRES_USER : guacamole_user
POSTGRES_PASSWORD : " ChangeThisPassword123"
volumes :
- ./init:/docker-entrypoint-initdb.d:z
- ./data/postgres:/var/lib/postgresql/data:Z
networks :
- guacamole_net
healthcheck :
test : ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval : 10s
timeout : 5s
retries : 5
start_period : 20s
guacamole :
image : guacamole/guacamole:1.6.0
container_name : guacamole
restart : always
depends_on :
guacd :
condition : service_healthy
postgres :
condition : service_healthy
environment :
GUACD_HOSTNAME : guacd
GUACD_PORT : 4822
POSTGRESQL_HOSTNAME : postgres
POSTGRESQL_DATABASE : guacamole_db
POSTGRESQL_USERNAME : guacamole_user
POSTGRESQL_PASSWORD : " ChangeThisPassword123"
RECORDING_SEARCH_PATH : /record
networks :
- guacamole_net
volumes :
- ./storage/record:/record:rw
ports :
- 8080/tcp
healthcheck :
test : ["CMD-SHELL", "curl -f http://localhost:8080/guacamole/ || exit 1"]
interval : 15s
timeout : 5s
retries : 5
start_period : 30s
nginx :
image : nginx:alpine
container_name : guacamole_nginx
restart : always
ports :
- " 80:80"
- " 443:443"
volumes :
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./nginx/certs:/etc/nginx/certs:ro
depends_on :
guacamole :
condition : service_healthy
networks :
- guacamole_net