Run MinIO on Proxmox LXC as a Self-Hosted S3 Backend

Deploy MinIO in a Proxmox LXC container and connect it as a native S3 datastore in PBS 4.2. Your backups stay local with full deduplication and no cloud fees.

Proxmox Pulse Proxmox Pulse
9 min read
Isometric miniature data center of stacked storage buckets with glowing data streams flowing into a central vault.

MinIO deployed in a Proxmox LXC container gives you a fully self-hosted S3-compatible endpoint that Proxmox Backup Server 4.2 can write its chunk store to — no cloud account, no egress fees, and your backup data never leaves your network. By the end of this guide you will have MinIO running inside an unprivileged Debian 12 LXC container on Proxmox VE 9.1, serving HTTPS, and registered as a native S3 datastore in PBS 4.2.

Key Takeaways

  • MinIO in LXC: A single-node MinIO instance runs comfortably in 512 MB RAM inside an unprivileged Debian 12 LXC container — no VM overhead required.
  • PBS 4.2 native S3: Proxmox Backup Server 4.2 added native S3 chunk-store datastores; MinIO is the cleanest self-hosted target.
  • Real throughput: Expect 300–500 MB/s on a gigabit LAN; PBS deduplication, not MinIO, is the bottleneck.
  • ZFS data disk: Attach MinIO's data directory as a ZFS dataset via pct set --mp for native snapshots of the backup target itself.
  • Redundancy caveat: MinIO on a second local host adds proximity redundancy — it is not a substitute for a geographically separate backup site.

Why MinIO Belongs in Your Proxmox Backup Strategy

PBS 4.2 added S3 as a first-class chunk-store datastore. Before that, self-hosted options were a remote PBS node, rsync jobs, or paying for a cloud bucket. S3 integration is architecturally cleaner — PBS deduplicates and compresses chunks before writing them, so MinIO's disk usage reflects actual backup data reduction, not raw VM sizes.

MinIO is AGPLv3 for self-hosted use. It is free as long as you are not reselling a managed service built on top of it. For a homelab or small business that is a non-issue.

The LXC approach is lightweight. MinIO is a single statically-linked Go binary — no Docker, no container runtime, just a process. Running it inside an LXC container rather than a full VM means you are paying 20–30 MB of RAM overhead for the container namespace, not 256–512 MB for a guest OS kernel. If you have already set up automated backups with Proxmox Backup Server, wiring in a MinIO backend is the natural next step for adding a second local datastore without buying dedicated hardware.

What You Need Before You Start

  • Proxmox VE 9.1 with at least one storage pool (ZFS preferred for the data disk)
  • Proxmox Backup Server 4.2 — same host or a separate node
  • A spare disk or ZFS dataset; allocate at least 2× your expected backup data size
  • A static IP for the MinIO container
  • A TLS certificate — self-signed works; instructions below

Container sizing at a glance:

Resource Minimum Recommended
vCPU cores 2 4
RAM 512 MB 2 GB
Root disk 8 GB 8 GB
Data disk 2× backup data 3× backup data

How to Create the LXC Container

Download a Debian 12 template if you do not have one:

pveam update
pveam download local debian-12-standard_12.7-1_amd64.tar.zst

Create the container. Replace vmbr0, 192.168.1.50, and local-zfs with your bridge name, desired IP, and storage pool:

pct create 200 local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst \
  --hostname minio \
  --cores 4 \
  --memory 2048 \
  --swap 512 \
  --rootfs local-zfs:8 \
  --net0 name=eth0,bridge=vmbr0,ip=192.168.1.50/24,gw=192.168.1.1 \
  --nameserver 1.1.1.1 \
  --unprivileged 1 \
  --start 1

Add a dedicated data disk. Keeping it separate from the root disk lets you snapshot, resize, or migrate your backup storage independently. On ZFS, pct set creates a new dataset automatically:

pct set 200 --mp0 local-zfs:500,mp=/mnt/data,size=500G
pct reboot 200

Replace 500G with your target capacity.

Installing MinIO Inside the Container

Enter the container and download the MinIO binary:

pct exec 200 -- bash
apt update && apt install -y wget curl

wget https://dl.min.io/server/minio/release/linux-amd64/minio \
  -O /usr/local/bin/minio
chmod +x /usr/local/bin/minio

# Confirm it runs
minio --version

Create a dedicated system user — never run MinIO as root:

useradd -r -s /sbin/nologin -d /mnt/data minio-user
mkdir -p /mnt/data
chown -R minio-user:minio-user /mnt/data

Create the environment file the systemd unit will source:

mkdir -p /etc/minio
cat > /etc/minio/minio.env << 'EOF'
MINIO_ROOT_USER=admin
MINIO_ROOT_PASSWORD=changeme-use-a-32char-random-string
MINIO_VOLUMES="/mnt/data"
MINIO_OPTS="--address :9000 --console-address :9001"
EOF
chmod 600 /etc/minio/minio.env

Set MINIO_ROOT_PASSWORD to a random 32-character string before continuing. openssl rand -hex 16 generates a good one.

Generating a TLS Certificate for the S3 Endpoint

PBS 4.2's S3 client requires HTTPS and will reject self-signed certificates unless you explicitly trust the CA. Here is a self-signed cert covering both the IP and a hostname:

mkdir -p /mnt/data/.minio/certs
openssl req -x509 -nodes -days 3650 -newkey rsa:4096 \
  -keyout /mnt/data/.minio/certs/private.key \
  -out /mnt/data/.minio/certs/public.crt \
  -subj "/CN=minio" \
  -addext "subjectAltName=IP:192.168.1.50,DNS:minio.local"
chown -R minio-user:minio-user /mnt/data/.minio

MinIO auto-detects private.key and public.crt in $MINIO_VOLUMES/.minio/certs/ and enables TLS automatically — no flag required. If you use Let's Encrypt, copy your cert and key to the same paths and add a renewal hook to reload MinIO.

Configuring MinIO as a systemd Service

cat > /etc/systemd/system/minio.service << 'EOF'
[Unit]
Description=MinIO S3-Compatible Object Storage
Documentation=https://docs.min.io
Wants=network-online.target
After=network-online.target
AssertFileIsExecutable=/usr/local/bin/minio

[Service]
User=minio-user
Group=minio-user
WorkingDirectory=/mnt/data
EnvironmentFile=/etc/minio/minio.env
ExecStart=/usr/local/bin/minio server $MINIO_VOLUMES $MINIO_OPTS
Restart=always
RestartSec=5
LimitNOFILE=65536
TasksMax=infinity
TimeoutStopSec=infinity
SendSIGKILL=no

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now minio
systemctl status minio

The MinIO console is now reachable at https://192.168.1.50:9001. Log in with the credentials from minio.env.

Creating the PBS Bucket and a Least-Privilege Access Key

From the MinIO console at https://192.168.1.50:9001, navigate to Buckets → Create Bucket. Name it proxmox-backups and enable Versioning — it adds protection against accidental deletes without breaking PBS compatibility.

Or use mc, the MinIO CLI client, from inside the container:

wget https://dl.min.io/client/mc/release/linux-amd64/mc \
  -O /usr/local/bin/mc
chmod +x /usr/local/bin/mc

# --insecure is only needed for self-signed certs
mc alias set local https://localhost:9000 admin 'your-root-password' --insecure

mc mb local/proxmox-backups
mc version enable local/proxmox-backups

Creating a Scoped Access Key for PBS

Using root credentials in PBS is sloppy. In the MinIO console go to Access Keys → Create Access Key and attach this inline policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:ListBucket",
        "s3:GetBucketLocation"
      ],
      "Resource": [
        "arn:aws:s3:::proxmox-backups",
        "arn:aws:s3:::proxmox-backups/*"
      ]
    }
  ]
}

This gives PBS read, write, delete, and list on the one bucket — nothing else. Copy the generated access key ID and secret key; you will need them in the next step.

How to Add MinIO as a PBS 4.2 Datastore

If you used a self-signed certificate, copy the MinIO CA cert to PBS's trust directory first. Run this on the PBS host:

mkdir -p /etc/proxmox-backup/trusted-certificates
scp root@192.168.1.50:/mnt/data/.minio/certs/public.crt \
  /etc/proxmox-backup/trusted-certificates/minio.crt

systemctl restart proxmox-backup

Skipping the restart is the single most common reason the first connection test fails — PBS reads the trust store at startup, not on demand.

In the PBS web UI go to Datastore → Add → S3 Datastore and fill in:

  • Datastore ID: s3-minio
  • S3 Endpoint: https://192.168.1.50:9000
  • Bucket: proxmox-backups
  • Region: us-east-1 — required by PBS; MinIO ignores the value but PBS will return a 400 auth error without it
  • Access Key / Secret Key: the scoped credentials you just created

Or via the PBS CLI:

proxmox-backup-manager datastore create s3-minio \
  --s3-endpoint https://192.168.1.50:9000 \
  --s3-bucket proxmox-backups \
  --s3-region us-east-1 \
  --s3-access-key YOUR_ACCESS_KEY \
  --s3-secret-key 'YOUR_SECRET_KEY'

Verifying the End-to-End Flow

On a Proxmox VE node, add the PBS instance as PVE storage and run a test backup:

pvesm add pbs pbs-s3 \
  --server 192.168.1.51 \
  --datastore s3-minio \
  --username backup@pbs \
  --password 'your-pbs-password' \
  --fingerprint AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD

# Trigger a snapshot backup of VM 101
vzdump 101 --storage pbs-s3 --mode snapshot

After completion, confirm objects landed in MinIO:

mc ls --recursive local/proxmox-backups/ | head -20

You will see directories per VM ID containing .fidx, .didx, and chunk files. A 50 GB VM typically produces 12,000–18,000 objects. That is normal PBS chunk behavior, not a misconfiguration.

Gotchas From Production Use

The region field is not optional. PBS's S3 client embeds the region in the authentication signature. MinIO ignores it, but PBS returns a cryptic 400 authentication error if you leave the field blank. Always set it to us-east-1.

Inode exhaustion on ext4. PBS chunks are small — typically 4 MB each. A 1 TB backup set can produce 250,000+ objects. If MinIO's data directory is on ext4 with default inode allocation, you will exhaust inodes before disk space. ZFS handles this automatically. If you must use ext4, format with mkfs.ext4 -i 4096 /dev/sdX to allocate one inode per 4 KB block.

UID mapping and bind mounts. If you manually bind-mount a host path into an unprivileged container instead of using pct set --mpX, files appear owned by nobody inside the container and MinIO silently fails to write. This is the same footgun you hit with running Docker inside LXC containers on Proxmox — always use Proxmox tooling for storage attachments, not raw bind mounts in /etc/pve/lxc/.

GC locks the datastore. Garbage collection on a 2 TB MinIO-backed datastore holds the chunk store locked for 15–25 minutes. Schedule it outside business hours via the PBS prune/GC job settings.

Performance Reality Check

On a gigabit LAN with NVMe storage on both ends:

  • Initial full backup of a 100 GB VM: 8–14 minutes
  • Daily incremental at 5–10% change rate: 45–90 seconds
  • Backup verification pass: roughly 1 minute per 100 GB
  • GC on a 2 TB datastore: 15–25 minutes

The bottleneck is PBS's deduplication pipeline, not MinIO. MinIO itself can saturate a 10 Gbps link with a handful of concurrent upload streams — far beyond what a single PBS node will push. If you want more throughput from PBS, increase worker threads under Administration → Configuration → Worker Threads in the PBS web UI.

For anyone building this out as part of a broader homelab infrastructure, the guide on building a private cloud at home with Proxmox VE covers how pieces like this fit into a coherent multi-service architecture.

Conclusion

You now have a self-hosted S3 endpoint running in an unprivileged Debian 12 LXC container, with PBS 4.2 writing deduplicated chunk stores directly to it — the entire setup takes under 30 minutes and runs on hardware you already have. The logical next step is spinning up a second MinIO instance on a different physical host and configuring a PBS remote sync job to replicate the s3-minio datastore there, giving you off-node redundancy without any cloud spend.

Share
Proxmox Pulse

Written by

Proxmox Pulse

Sysadmin-driven guides for getting the most out of Proxmox VE in production and homelab environments.

Related Articles

View all →