The Utilities Stack

The Utilities Stack
Proxmox 9-1

I am sharing this as I believe this will help a great many people understand a few things

Here is my base stack - it also includes Termix. It includes a number of utility services that a lot of people will find handy to say the least.

It is not rocket science it is very simple stuff.

A few notes:

  1. I have left the homepage labels in to help with your understanding
  2. I have left the subdomains in to help with your understanding. You will need to create your cname records within your DNS managment interface.
  3. Watchtower will not update your images for each service in this configuration. It is designed this way. It will notify you and you will then need to refer to the release notification for each service to ensure it does not include breaking changes. Yes, I have made this mistake more than once so I have stopped auto updates.
  4. Watchtower. Yes this is not the default image. This one is actively maintained, and should be docker ready. Refer to the crap that went down when docker updated its repository and broke a good many servers - its a post.
  5. The app-net network. Yes its external and created via script on configuration of the baden.braedach.com LXC application server. You can change this to your hearts desire just modify the network. Termix is in its own sub network. Please refer to the appropriate documentation for your preferred container system.

The Code

As per below.

# Utilities stack for Proxmox LXC
# References are included in the individual service sections below
# CNAME records need to be pointed at the proxy not the actual container
# Remember all healthchecks are performed internally within the container, so use localhost and the internal port
# This is a podman compose file, but should work with docker-compose as well. The only difference is the socket path for watchtower.
# This is a podman compose file, the U flag is used to ensure that the volume is created with the correct permissions.
#
# Notes:
#  - Considerable work has been done on the healthchecks - dont change
#  - termix host port is 6666 (container 8080) - 8080 was already in use by omni-tools
#  - termix is attached to app-net (proxy reachable) AND termix-net (to reach guacd)
#  - guacd is on termix-net only and is NOT published to the host - only termix needs it
#  - before first start, create the bind mount dir: mkdir -p /srv/termix/data
#  - termix healthcheck is a TCP connect to localhost:8080 - verify after first boot (see warning below)
#
# Changelog:
#  - 2.6.1 (2026-06-20): added healthcheck for termix (TCP connect to localhost:8080)
#          Added watchtower monitor-only mode (report updates, do not apply)
#  - 2.6.0 (2026-06-10): added termix + guacd (self-hosted SSH/RDP/VNC/Telnet management, Termius alternative)
#  - 2.5.0 (2026-05-10): prior baseline
#
# Last updated: 2026-07-10
# Version: 2.6.2

name: utilities-stack

services:

  bentopdf:
    # https://hub.docker.com/r/bentopdf/bentopdf-simple
    image: bentopdf/bentopdf:latest
    container_name: bentopdf
    hostname: bentopdf
    restart: always
    ports:
      - "8990:8080"
    labels:
      - com.docker.compose.project=utilities-stack
      - com.docker.compose.service=bentopdf
      - homepage.group=IT Tools
      - homepage.name=BentoPDF Service
      - homepage.href=https://bentopdf.baden.braedach.com
      - com.centurylinklabs.watchtower.enable=true
    environment:
      - TZ=Australia/Perth
    volumes:
      # The U flag is used to ensure that the volume is created with the correct permissions for BentoPDF to write to it.
      - bentopdf-config:/config:U
      - bentopdf-data:/data:U
      - bentopdf-logs:/var/log/bentopdf:U
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
    healthcheck:
      test: wget -q -O - http://localhost:8080/api/v1/healthcheck
      interval: 60s
      retries: 5
      start_period: 60s
      timeout: 10s
    cap_add:
      - SYS_TIME
    security_opt:
      - no-new-privileges:true
    networks:
      - app-net


  it-tools:
    # https://github.com/CorentinTh/it-tools
    image: ghcr.io/corentinth/it-tools:latest
    container_name: it-tools
    hostname: it-tools
    restart: always
    ports:
      - "8880:80"
    labels:
      - com.docker.compose.project=utilities-stack
      - com.docker.compose.service=it-tools
      - homepage.group=IT Tools
      - homepage.name=IT Tools
      - homepage.href=https://it-tools.baden.braedach.com
      - com.centurylinklabs.watchtower.enable=true
    environment:
      - TZ=Australia/Perth
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
    healthcheck:
      test: curl -f -s http://localhost:80/health
      interval: 60s
      retries: 5
      start_period: 60s
      timeout: 10s
    cap_add:
      - SYS_TIME
    security_opt:
      - no-new-privileges:true
    networks:
      - app-net


  net-tools:
    # https://github.com/Lissy93/networking-toolbox
    image: lissy93/networking-toolbox:latest
    container_name: net-tools
    hostname: net-tools
    restart: always
    ports:
      - "8888:8888"
    labels:
      - com.docker.compose.project=utilities-stack
      - com.docker.compose.service=net-tools
      - homepage.group=IT Tools
      - homepage.name=Network Tools
      - homepage.href=https://nettools.baden.braedach.com
      - com.centurylinklabs.watchtower.enable=true
    environment:
      - TZ=Australia/Perth
      - NODE_ENV=production
      - PORT=8888
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
    healthcheck:
      test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:8888/', r => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))\""]
      interval: 60s
      timeout: 10s
      retries: 5
      start_period: 60s
    cap_add:
      - SYS_TIME
    security_opt:
      - no-new-privileges:true
    networks:
      - app-net


  omni-tools:
    # https://github.com/iib0011/omni-tools#self-hostrun
    image: iib0011/omni-tools:latest
    container_name: omni-tools
    hostname: omni-tools
    restart: always
    ports:
      - 8080:80
    labels:
      - com.docker.compose.project=utilities-stack
      - com.docker.compose.service=omni-tools
      - homepage.group=IT Tools
      - homepage.name=Omni-Tools
      - homepage.href=https://omnitools.baden.braedach.com
      - com.centurylinklabs.watchtower.enable=true
    environment:
      - TZ=Australia/Perth
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
    healthcheck:
      test: curl -f -s http://localhost:80/health
      interval: 60s
      retries: 5
      start_period: 60s
      timeout: 10s
    security_opt:
      - no-new-privileges:true
    networks:
      - app-net


  freshrss:
    # https://github.com/FreshRSS/FreshRSS
    image: freshrss/freshrss:latest
    container_name: freshrss
    hostname: freshrss
    restart: always
    ports:
      - 8100:80
    labels:
      - com.docker.compose.project=utilities-stack
      - com.docker.compose.service=freshrss
      - homepage.group=Family Services
      - homepage.name=FreshRSS
      - homepage.href=https://freshrss.baden.braedach.com
      - com.centurylinklabs.watchtower.enable=true
    environment:
      - TZ=Australia/Perth
      - CRON_MIN=*/15
      - BASE_URL=http://freshrss.baden.braedach.com
      - ADMIN_EMAIL={YOUR ADMIN EMAIL ADDRESS}
      - ADMIN_PASSWORD={YOUR ADMIN PASSWORD}
      # Admin API needs be be added after first login.
      - ADMIN_API_PASSWORD={PULL THIS FROM THE LOGS}
    volumes:
      - /srv/freshrss/data:/var/www/FreshRSS/data:U
      - /srv/freshrss/extensions:/var/www/FreshRSS/extensions:U
      - /srv/freshrss/logs:/var/www/FreshRSS/data/freshrss/logs:U
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
    healthcheck:
      test: ["CMD", "cli/health.php"]
      interval: 60s
      timeout: 10s
      retries: 5
      start_period: 30s
    security_opt:
      - no-new-privileges:true
    networks:
      - app-net


  termix:
    # https://github.com/Termix-SSH/Termix
    # Self-hosted SSH/RDP/VNC/Telnet management (Termius alternative). Remote desktop via guacd.
    image: ghcr.io/lukegus/termix:latest
    container_name: termix
    hostname: termix
    restart: always
    ports:
      - 6666:8080
    labels:
      - com.docker.compose.project=utilities-stack
      - com.docker.compose.service=termix
      - homepage.group=IT Tools
      - homepage.name=Termix
      - homepage.href=https://termix.baden.braedach.com
      - com.centurylinklabs.watchtower.enable=true
    environment:
      - TZ=Australia/Perth
      - PORT=8080
    volumes:
      - /srv/termix/data:/app/data:U
      - /etc/localtime:/etc/localtime:ro
    depends_on:
      - guacd
    healthcheck:
      # TCP connect check - no documented HTTP health endpoint, and Termix may redirect HTTP->HTTPS
      test: ["CMD-SHELL", "node -e \"require('net').connect(8080,'127.0.0.1').on('connect',()=>process.exit(0)).on('error',()=>process.exit(1))\""]
      interval: 60s
      timeout: 10s
      retries: 5
      start_period: 60s
    security_opt:
      - no-new-privileges:true
    networks:
      - app-net
      - termix-net


  guacd:
    # https://hub.docker.com/r/guacamole/guacd - Apache Guacamole proxy daemon for termix RDP/VNC
    # Backend only - reached by termix over termix-net. Deliberately NOT published to the host.
    image: guacamole/guacd:1.6.0
    container_name: guacd
    hostname: guacd
    restart: always
    expose:
      - "4822"
    labels:
      - com.docker.compose.project=utilities-stack
      - com.docker.compose.service=guacd
      - com.centurylinklabs.watchtower.enable=false
    environment:
      - TZ=Australia/Perth
    volumes:
      - /etc/localtime:/etc/localtime:ro
    security_opt:
      - no-new-privileges:true
    networks:
      - termix-net

  watchtower:
    # https://marrrrrrrrry.github.io/watchtower-docs/quick-start.html
    image: docker.io/marrrrrrrrry/watchtower:latest
    container_name: watchtower
    hostname: watchtower
    restart: always
    labels:
      - com.docker.compose.project=utilities-stack
      - com.docker.compose.service=watchtower
      - com.centurylinklabs.watchtower.enable=true
    environment:
      - DOCKER_CLIENT_TIMEOUT=300
      - COMPOSE_HTTP_TIMEOUT=300
      - WATCHTOWER_POLL_INTERVAL=21600
      - WATCHTOWER_LABEL_ENABLE=true
      - WATCHTOWER_CLEANUP=true
      - WATCHTOWER_LOG_LEVEL=info
      - TZ=Australia/Perth
      # --- Monitor only: report available updates, do not apply them ---
      - WATCHTOWER_MONITOR_ONLY=true
      # --- SMTP Notifications ---
      - WATCHTOWER_NOTIFICATIONS=email
      - WATCHTOWER_NOTIFICATION_EMAIL_FROM={WATCHTOWER_NOTIFICATION_EMAIL_FROM}
      - WATCHTOWER_NOTIFICATION_EMAIL_TO={WATCHTOWER_NOTIFICATION_EMAIL_TO}
      - WATCHTOWER_NOTIFICATION_EMAIL_SERVER={WATCHTOWER_NOTIFICATION_EMAIL_SERVER}
      - WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT={WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT}
      - WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER={WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER}
      - WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD={WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD}
      - WATCHTOWER_NOTIFICATION_EMAIL_DELAY={WATCHTOWER_NOTIFICATION_EMAIL_DELAY}
      - WATCHTOWER_NOTIFICATION_REPORT={WATCHTOWER_NOTIFICATION_REPORT}
      - WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG={WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG}
      - WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY=true
    volumes:
      # Change this socket as required whether deploying docker or podman
      - /run/podman/podman.sock:/var/run/docker.sock
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
    security_opt:
      - no-new-privileges:true
    networks:
      - app-net


volumes:
  bentopdf-config:
  bentopdf-data:
  bentopdf-logs:

networks:
  app-net:
    external: true
  termix-net:
    driver: bridge

I hope this helps. It is a very good multiservice stack, but you will probably need to adjust ports and make considerable alterations to your reverse proxy. If you dont have a reverse proxy just ensure when you set up Portainer CE that you SETUP a public IP for the server environment, which will require a DNS record yet again.

#enoughsaid