Skip to main content

Production UIS on Proxmox VE

The reference on-premises production deployment: a single Proxmox host running one Kubernetes cluster plus the components beside the cluster.

Read Production overview first — this page is only the mechanics.

Everything here was built and verified on a real deployment. Where something bit us, it is called out as a ⚠️ gotcha rather than left for you to rediscover.


Topology

GuestTypePurpose
(host)Proxmox VEhypervisor, ZFS pool, guest backups
k8sVMthe cluster (k3s)
pgLXCPostgreSQL — outside the cluster
minioLXCobject storage — outside the cluster
baoLXCsecret store — outside the cluster
registryLXCpull-through image cache — outside the cluster
opsLXCmanagement plane (provision-host + kubeconfigs)
nasLXCoptional: SMB/NFS file shares

Containers rather than VMs for the supporting services: lower overhead, and they boot in seconds. The cluster itself is a VM — k3s in an LXC is possible but fights you over kernel modules and cgroups.

Storage tiering

With two disks, split them by purpose, not size:

TierDiskHoldsSnapshots?
systemsfastest/healthiestguest rootfs, VM system disks, container imagesno — rebuildable
datacapacitydatabase, object store, secrets, file shares, backupsyes

Put the data tier on ZFS — snapshots, checksums and compression are what make the backup story work. Tune per dataset: recordsize=16K for PostgreSQL (matches its page size), 1M for object/blob storage and backup archives.

⚠️ Kubernetes system disks belong on the systems tier. Image churn on the data disk buys nothing — as a zvol it does not get dataset-level snapshots anyway.


Build sequence

1. Host preparation

Update Proxmox, create the ZFS pool, and cap the ARC if guests will use most of the RAM (options zfs zfs_arc_max=… in /etc/modprobe.d/zfs.conf) — otherwise ARC and your guests compete.

⚠️ Check the bootloader matches the boot mode. A host booting via EFI with only the BIOS GRUB package installed will boot today but silently stop receiving bootloader updates.

2. Cluster VM

An Ubuntu LTS cloud image plus cloud-init is the fastest path. Then k3s:

curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server --write-kubeconfig-mode 644" sh -

k3s ships Traefik v3, matching the UIS manifests. It is also what Rancher Desktop runs, so development and production behave the same.

⚠️ Cloud images take their DHCP lease before cloud-init applies the hostname, so DNS registers the host as ubuntu. Set the hostname before networking, or force a lease renewal afterwards.

3. Components beside the cluster

Each in its own container, each with its data on a tuned ZFS dataset:

  • PostgreSQL — install from PGDG for a current version with pgvector/PostGIS. On ZFS, set full_page_writes = off; copy-on-write makes torn pages impossible, so PostgreSQL's own full-page writes are redundant. Keep pg_wal in the same dataset as the data so a snapshot is atomic and crash-consistent.
  • Object storage — MinIO as a systemd service, data on a recordsize=1M dataset.
  • Secret store — see secrets.
  • Registry cache — see step 7.

Expose them on the overlay network so machines outside the cluster (developer laptops, workers at other sites) can reach them directly.

⚠️ Making UIS accept an external database

UIS currently assumes PostgreSQL and MinIO run in the cluster in two places: its dependency check looks for a Running pod with a specific label, and some playbooks kubectl exec into that pod to run psql. An external database satisfies neither.

Until UIS supports an external mode, bridge it with a small in-cluster Deployment carrying the expected label:

  • container 1 — a postgres image running sleep infinity. It must be first, because that is where kubectl exec lands, and it needs bash + psql. Set PGHOST on it so bare psql uses TCP rather than a Unix socket.
  • container 2socat forwarding the pod's :5432 to the real host.

Plus a normal ClusterIP Service selecting it. UIS is then satisfied, playbooks work unchanged, and the database stays outside Kubernetes. The same pattern works for MinIO (an mc container first, socat for :9000/:9001).

Do not uis deploy postgresql afterwards — it would replace the bridge.

4. Management plane

Install Docker in the ops container and run the provision-host there, holding kubeconfigs for every target.

⚠️ Never run ./uis with sudo — it mounts root's ~/.kube (so the wrong kubeconfig) and leaves .uis.extend / .uis.secrets owned by root.

5. Storage classes — request a disk, get a disk

proxmox-csi-plugin lets a PVC create a real Proxmox disk and hot-attach it to the cluster VM, so services get storage on demand across both tiers:

ClassBacked byFor
fastsystems-tier storagelatency-sensitive workloads, monitoring TSDBs
standard (default)data-tier ZFSgeneral persistent data
sharedNFS export from the hostReadWriteMany
local-pathnode diskscratch only — not snapshotted

Make the backed-up class the default. UIS service manifests deliberately omit storageClassName so they inherit the cluster default — one lever then governs storage for the whole platform.

⚠️ The node needs providerID: proxmox://<region>/<vmid>. k3s sets k3s://<node> and providerID is immutable — set kubelet-arg: provider-id=…, restart k3s, delete the node object, restart again so it re-registers, then re-apply the topology labels.

⚠️ Prometheus cannot run on NFS (it needs mmap and POSIX locks) — give it a block-backed class.

6. Secrets

See the secrets guide. The short version: a store outside the cluster, External Secrets Operator inside it, and Kubernetes auth so nothing static is stored in the cluster to bootstrap secret access.

7. Registry cache

Without it the cluster cannot restart when upstream registries are unreachable. See the registry cache guide.

8. Ingress

Two complementary paths:

  • Private — an overlay-network ingress for admin UIs. No DNS, no public exposure, no certificates to manage.
  • Public — a tunnel to your CDN/edge for the few services that should be reachable, with authentication in front.

⚠️ Some overlay "expose" helpers publish to the public internet and bypass the reverse proxy, so forward-auth middleware does not apply. Read what the command actually does before using it for an admin UI.

9. Backups — four layers

LayerProtects againstFrequency
ZFS snapshotsdeletion, bad writesevery 15 min
Database PITR (WAL archiving)data loss between backupscontinuous, RPO ≈ 5 min
Guest backups (vzdump)losing a whole VM/containernightly, snapshot mode
Off-box copy (restic)losing the hostnightly

⚠️ Order matters, and hosts must agree on a timezone. A container on UTC and a host on local time will run "02:30" and "03:30" jobs three hours apart — with the dump landing after the backup that was supposed to capture it, so every offsite copy is a day stale while both jobs report success.

⚠️ Avoid scheduling in the DST transition hour (02:00–03:00 in Europe). At spring-forward that hour does not exist and the job is silently skipped.

10. Verify the restores

Not optional — see production overview #4.

  • Restore a database dump into a scratch database.
  • Restore a file from the off-box repo and compare it byte-for-byte with the live one.
  • Restore a guest to a spare VMID and boot it. ⚠️ A privileged container must be restored with --unprivileged 0, or tar fails with a uid-mapping error.
  • Do a point-in-time restore into an alternate directory and start a temporary instance on a spare port — this validates PITR without touching production. ⚠️ On Debian, postgresql.conf lives in /etc, not the data directory, so a restored data directory needs a minimal config supplied by hand.

11. Observability

Install the stack, then do the part that makes it useful: a log-collection agent, alert rules, and scrape targets for the components outside the cluster — including backup freshness. See production overview #5.


What you end up with

  • One Kubernetes cluster running the UIS service catalogue.
  • Database, object storage, secrets and image cache outside it — each independently restartable, each backed up.
  • Storage on demand across two performance tiers.
  • Private access to admin UIs, public access only where chosen.
  • Four backup layers, restore-tested, with point-in-time recovery for the database.
  • A management plane that does not depend on anyone's laptop.