196 lines
8.7 KiB
Markdown
196 lines
8.7 KiB
Markdown
---
|
|
name: proxmox-lxc-deployment
|
|
description: "Deploy containerized services (Docker, Immich, etc.) on Proxmox LXCs. Covers storage, NFS, Docker setup, and the LXC-vs-VM mental model."
|
|
version: 1.0.0
|
|
author: Hermes Agent
|
|
license: MIT
|
|
platforms: [linux]
|
|
metadata:
|
|
hermes:
|
|
tags: [proxmox, lxc, docker, deployment, nfs, storage, immich]
|
|
---
|
|
|
|
# Proxmox LXC Deployment
|
|
|
|
Deploy containerized services on Proxmox LXC containers. The key insight: **an LXC is not a VM.** Block devices, in-guest fstab, in-guest NFS mounts, and kernel module loading all work differently (or not at all). This skill captures the patterns that work.
|
|
|
|
## When to Use
|
|
|
|
- Deploying Docker-based services (Immich, SearXNG, etc.) in a Proxmox LXC
|
|
- Adding storage to an LXC
|
|
- Mounting external NFS/SMB shares for use inside an LXC
|
|
- Planning infrastructure that involves Proxmox containers
|
|
|
|
## LXC vs VM Mental Model
|
|
|
|
| Concern | VM | LXC |
|
|
|---------|----|-----|
|
|
| Add storage | Attach virtual disk → guest sees `/dev/sdX` → mkfs + mount | `pct set CTID -mpN pool:size,mp=/path` on host |
|
|
| fstab | Guest `/etc/fstab` works normally | Guest fstab is NOT processed; use host-side mount points |
|
|
| NFS mount | `mount -t nfs` inside guest works | Fails in unprivileged LXC; mount on host, bind-mount in |
|
|
| Docker | Works out of the box | Needs `nesting=1,keyctl=1` on the CT |
|
|
| Kernel modules | Guest can load modules | Shares host kernel; modules must be loaded on host |
|
|
|
|
## Storage: Adding a Disk to an LXC
|
|
|
|
**Wrong (VM pattern):**
|
|
```bash
|
|
# Inside the LXC — this does NOT work
|
|
mkfs.ext4 /dev/sdb
|
|
mount /dev/sdb /var/data
|
|
```
|
|
|
|
**Right (LXC pattern):**
|
|
```bash
|
|
# On the Proxmox HOST
|
|
pct set 9300 -mp0 local-lvm:200,mp=/var/immich-data
|
|
```
|
|
|
|
This creates a 200GB volume on `local-lvm` storage, formats it, and bind-mounts it at `/var/immich-data` inside the container. No guest-side mkfs, no fstab entry. The container sees it immediately (no reboot needed for new mount points, though a restart may be needed if the CT was running).
|
|
|
|
To pass through an existing host directory:
|
|
```bash
|
|
pct set 9300 -mp1 /host/path,mp=/container/path
|
|
# Read-only:
|
|
pct set 9300 -mp1 /host/path,mp=/container/path,ro=1
|
|
```
|
|
|
|
## NFS: Mounting a NAS Share for LXC Use
|
|
|
|
**Wrong:** `apt install nfs-common` + `mount -t nfs` inside the LXC. Fails with "operation not permitted" in unprivileged containers. This is a kernel-level restriction — NFS and CIFS are not `FS_USERNS_MOUNT` filesystems, so the mount syscall is refused inside a user namespace regardless of capabilities. Docker volume drivers with `type: nfs` opts also fail (same userns restriction). FUSE is also unavailable (no `/dev/fuse` in unprivileged LXC).
|
|
|
|
**Right:** Mount on the Proxmox host, then bind-mount into the LXC.
|
|
|
|
```bash
|
|
# On Proxmox HOST
|
|
mkdir -p /mnt/immich_nfs
|
|
mount -t nfs 10.0.0.50:/volume1/photo /mnt/immich_nfs
|
|
|
|
# Persistent: add to HOST /etc/fstab
|
|
# Use 'hard' not 'soft' for read-write mounts — soft returns EIO on timeout
|
|
# and Immich's job queue will mark assets as failed.
|
|
# 10.0.0.50:/volume1/photo /mnt/immich_nfs nfs vers=4.1,hard,_netdev,nofail 0 0
|
|
|
|
# Pass into LXC (read-write for uploads/thumbs, read-only for external library)
|
|
pct set 9300 -mp0 /mnt/immich_nfs,mp=/mnt/nas
|
|
# Read-only variant:
|
|
pct set 9300 -mp0 /mnt/immich_nfs,mp=/mnt/nas,ro=1
|
|
```
|
|
|
|
### Synology NFS + Unprivileged LXC: UID Mapping
|
|
|
|
Unprivileged LXC maps container root (uid 0) to host uid 100000. Synology's default `root_squash` will return EACCES on every write. On the Synology DSM, set the NFS share's squash to **Map all users to admin** with `anonuid=100000,anongid=100000`. This is the single most common failure in Proxmox+Synology NFS threads.
|
|
|
|
DSM path: Control Panel → Shared Folder → select share → Edit → NFS Permissions → Create rule:
|
|
- Client: `10.0.0.177`
|
|
- Squash: Map all users to admin
|
|
- Security: sys
|
|
- Enable asynchronous: yes
|
|
|
|
### NFS Mount Options by Use Case
|
|
|
|
| Use case | Options | Why |
|
|
|----------|---------|-----|
|
|
| Read-only external library | `ro,vers=4.1,hard,_netdev,nofail` | `hard` prevents EIO on transient blips |
|
|
| Read-write uploads/thumbs | `vers=4.1,hard,_netdev,nofail` | `soft` returns EIO → Immich marks assets failed |
|
|
| Never use `soft` for any Immich data path | — | Job queue corruption on timeout |
|
|
|
|
## Docker in an LXC
|
|
|
|
### Required CT Features
|
|
|
|
Before installing Docker, enable nesting and keyctl on the container:
|
|
|
|
```bash
|
|
# On Proxmox HOST
|
|
pct set 9300 -features nesting=1,keyctl=1
|
|
pct reboot 9300
|
|
```
|
|
|
|
Without `nesting=1`, Docker fails to create containers. Without `keyctl=1`, Docker's overlay2 storage driver may fail.
|
|
|
|
### Docker Storage Location
|
|
|
|
The LXC root disk is often small (20-40GB). Docker images, layers, and logs will fill it. Point Docker's data-root at a dedicated mount point BEFORE installing Docker:
|
|
|
|
```bash
|
|
# Inside LXC, before apt install docker-ce
|
|
mkdir -p /var/immich-data/docker
|
|
cat > /etc/docker/daemon.json << 'EOF'
|
|
{"data-root":"/var/immich-data/docker"}
|
|
EOF
|
|
```
|
|
|
|
After Docker is installed, verify the storage driver:
|
|
```bash
|
|
docker info | grep "Storage Driver" # must say overlay2, not vfs
|
|
```
|
|
|
|
If the LXC root is ZFS-backed and Docker's data-root is also on ZFS, Docker may fall back to `vfs` (which copies every layer in full — 3GB of images becomes 15GB+). Using an ext4 mount point for `data-root` avoids this.
|
|
|
|
### Docker Install (Debian)
|
|
|
|
```bash
|
|
apt update && apt install -y ca-certificates curl
|
|
install -m 0755 -d /etc/apt/keyrings
|
|
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
|
|
chmod a+r /etc/apt/keyrings/docker.asc
|
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list
|
|
apt update && apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
|
usermod -aG docker $USER
|
|
```
|
|
|
|
## Postgres in Docker: Don't Pre-create the Data Directory
|
|
|
|
When using a bind mount for Postgres data, do NOT pre-create and chown the directory. The Postgres container runs as uid 999 and initializes an empty PGDATA itself. Pre-creating with `chown 1000:1000` causes `initdb: could not change permissions` on first boot.
|
|
|
|
**Wrong:**
|
|
```bash
|
|
mkdir -p /var/data/postgres
|
|
chown -R 1000:1000 /var/data/postgres # breaks initdb
|
|
```
|
|
|
|
**Right:**
|
|
```bash
|
|
# Either don't create it at all (Docker will)
|
|
# OR create it but don't chown:
|
|
mkdir -p /var/data/postgres
|
|
# Let the container handle ownership
|
|
```
|
|
|
|
## Pre-flight Checks
|
|
|
|
Before deploying a service in an LXC, verify:
|
|
|
|
```bash
|
|
# CPU features (needed by vector DBs, ML, etc.)
|
|
lscpu | grep -o 'avx2\|sse4_2'
|
|
|
|
# Docker storage driver
|
|
docker info | grep "Storage Driver"
|
|
|
|
# Mount points visible
|
|
df -h | grep -E '/var/|/mnt/'
|
|
|
|
# NFS readability (if applicable)
|
|
ls -l /mnt/photos && head -c1 /mnt/photos/some-file.jpg >/dev/null && echo "NFS OK"
|
|
```
|
|
|
|
## Pitfalls
|
|
|
|
- **Treating LXC like a VM for storage.** Use `pct set -mpN`, not guest-side mkfs/fstab.
|
|
- **NFS inside unprivileged LXC.** Always mount on host, bind-mount in. No workaround exists — NFS/CIFS are not `FS_USERNS_MOUNT` filesystems.
|
|
- **Forgetting `nesting=1,keyctl=1`.** Docker silently fails without them. If Docker is already running, both are already set — skip the check.
|
|
- **Docker data-root on small root disk.** Set `data-root` in daemon.json before first start.
|
|
- **Pre-chowning Postgres data dir.** Let the container initialize it (runs as uid 999).
|
|
- **ZFS + Docker = vfs storage driver.** Ensure Docker's data-root is on ext4/xfs, not ZFS.
|
|
- **`pct` not `qm`.** `qm` is for VMs, `pct` is for containers. Using the wrong one silently does nothing or errors confusingly.
|
|
- **Over-provisioning storage.** Not every service needs a dedicated virtual disk. For Immich: uploads/thumbs can live on NFS (widely used, supported). Only Postgres MUST be local. If root disk is tight, `pct resize CTID rootfs +32G` is one command, online, no reboot — cheaper than a new virtual disk.
|
|
- **PostgreSQL on NFS.** Hard blocker, not a performance warning. NFS hiccup during checkpoint → fsync error → Postgres PANICs or corrupts data silently. Immich docs: "Network shares are not supported for the database." Keep Postgres on local disk always.
|
|
- **Synology NFS UID mapping with unprivileged LXC.** Container root (uid 0) maps to host uid 100000. Synology's default `root_squash` returns EACCES on every write. Set squash to "Map all users to admin" with `anonuid=100000,anongid=100000`.
|
|
- **Agent cannot modify Proxmox host.** The operator's standing rule: all `pct set`, fstab edits, disk creation, and bind mounts must be done by the user manually. Plans must list these as operator steps, not agent-executed commands.
|
|
|
|
## References
|
|
|
|
- `references/immich-plan-example.md` — Full worked example: Immich on LXC with Synology NAS external library (Jul 2026, v3.0.3)
|