Skip to content

Instantly share code, notes, and snippets.

@noslin005
Last active May 1, 2026 00:18
Show Gist options
  • Select an option

  • Save noslin005/408fd6ab210bf7973cd880f9eaab04f7 to your computer and use it in GitHub Desktop.

Select an option

Save noslin005/408fd6ab210bf7973cd880f9eaab04f7 to your computer and use it in GitHub Desktop.

Deploy Guacamole using Podman Kube

Directory Structuy

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

Podman Network

# 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 guacamole SQL

# 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

Certs

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/

Files

  • 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;

Postgres Backup

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment