Backing up an LXC with Veeam

Backing up an LXC with Veeam

If you use Proxmox VE, you already know how amazing and lightweight Linux Containers (LXCs) are. So, when Veeam announced native support for Proxmox VE, I was incredibly excited. But my excitement turned to pure frustration when I realized a major catch: Veeam’s native Proxmox plug-in completely ignores LXC containers. It only supports QEMU/KVM virtual machines.
Veeam justifies this by saying enterprise workflows rely on VMs and that they require block-level Changed Block Tracking (CBT) to prevent massive network overhead. But for my lightweight setup, I just wanted a simple way to back up my containers without the corporate bloat.
Instead of walking away, I decided to test out a theory and found a bulletproof workaround using Proxmox’s native tools alongside Veeam. Here is how I set it up, step-by-step:

Step 1: I leverage Proxmox’s Native vzdump

First, I use Proxmox’s built-in CLI or Web GUI to generate standard backups. By navigating to Datacenter > Backup in PVE, I schedule a job that takes a live snapshot of my LXC and compresses it into a single .tar.zst file. Because these files pile up quickly, I set the PVE retention policy to Keep Last: 1. This ensures Proxmox doesn’t do shit here automatically it just fails a backup job if one already exists, kind of dumb it should just create one and purge the old one. cause technically me being able to delete it goes against the rentention policy, where as creating a new one and purging the old one does not.

Step 2: I hook Veeam into the Host Operating System

Since Proxmox stores these dumps locally on the host’s Debian filesystem (usually under /var/lib/vz/dump/), I need a way for Veeam to reach them. I went into the Veeam Backup & Replication console and added my Proxmox host as a Managed Linux Server.
A quick warning if you try this: The Veeam wizard tries to force-install a massive list of unnecessary corporate storage packages (like Dell Data Domain and NetApp drivers) onto your lean Proxmox host. I aggressively unchecked all optional components, keeping only the absolute essentials: the Installer Service and the Veeam Data Mover.

Step 3: I Use a Veeam File Copy Job for Offsite Retention

With Veeam now able to safely browse my Proxmox filesystem, I built a File Copy Job. I pointed the source to my Proxmox dump folder, used a wildcard filter (like *.tar.zst), and set my standard Veeam repository as the destination. Now, Veeam automatically pulls the full compressed backup file off my host every night. Even though Proxmox deletes its local copy the next day, Veeam keeps my historical recovery points safe in my long-term repository. From here we can complete the 3-2-1-0 Rule for backups.

Step 4: Seamless Disaster Recovery to a Second Host

The best part about this architecture is how incredibly simple the restore process is. Proxmox backup files are completely self-contained; they don’t rely on a central database.
If my primary Proxmox host dies, I can add a secondary Proxmox host to Veeam as a managed server. I then use Veeam to copy the .tar.zst backup files directly into the second host’s local dump folder. The exact second the file transfer completes, the backup instantly populates in the second Proxmox GUI under the storage menu. From there, I just hit “Restore,” assign a unique Container ID if needed, and my LXC is back online.
It might not be the “official” automated method Veeam envisioned, but it bypasses their limitations perfectly and works flawlessly for my environment.
It also doesn’t provide any dedup as the base file is literally replaced in place.
Hope this helps someone.

Deploying Pi-hole v6 Offline on Proxmox VE with a Palo Alto Firewall

Deploying Pi-hole v6 Offline on Proxmox VE with a Palo Alto Firewall

An architectural summary and step-by-step rebuilding guide for the containerized offline deployment.

🏗️ Technical Architecture Overview

    • Hypervisor: Proxmox VE (PVE)
    • Parent Compute Layer: Privileged Linux Container (LXC) running a minimal Debian 12 base template.
    • Network Blueprint: Bound to vmbr1, isolated natively via VLAN 7 (172.16.7.2/24), routing through gateway 172.16.7.1.
    • Runtime Engine: Decoupled, standalone static binary installation of Docker Engine v27.3.1.
    • Application Tenant: Pi-hole v6 running in native Host Networking Mode (--net=host).
    • Upstream Security: Border perimeter managed by a Palo Alto Networks (PAN) Layer 7 firewall with strict application checking and URL filtering.


🛑 Phase 1: Perimeter Firewall Provisioning (Palo Alto)

Because the Pi-hole requires internet access to initialize its database but must remain highly secured, a tailored Layer 7 inspection ruleset was applied.
    1. Custom URL Category Object: Created an object named Pihole-Gravity-Sources populated with explicit domains to pull source databases:
        • *.github.com
        • *.githubusercontent.com
        • *.pi-hole.net
        • install.pi-hole.net

    2. Security Policy Mapping:
        • Source: VLAN 7 Zone (172.16.7.2)
        • Destination: WAN/External Zone (any)
        • Applications Permit List: ssl, web-browsing, dns, github-base (Crucial: required to pass deep inspection chunking packets during data stream ingestion).
        • Service Profile: Standard ports 80/http and 443/https.
        • URL Category Constraint: Enforced to match Pihole-Gravity-Sources.


💾 Phase 2: Staging Offline Files (Management PC)

To populate an offline PVE node, core files were staged using an online workstation browser:
    1. PVE Debian 12 Template:
      Index of /images/system/
    2. Docker Standalone Core:
      Index of linux/static/stable/x86_64/
    3. Pi-hole v6 Docker Image: Pulled down via an archive generator engine as a clean deployment container tarball (pihole-pihole-latest-linux-amd64.image.tar).


🛠️ Phase 3: Parent LXC Deployment & Pre-Staging

All instructions were passed natively via the Proxmox Host Shell CLI to build the environment and seed assets before booting the container.
    1. Create the Parent Host:
      pct create 105 /mnt/pve/mass-storage/template/cache/debian-12-standard_12.7-1_amd64.tar.zst \
        -cores 1 \
        -memory 1024 \
        -swap 512 \
        -ostype debian \
        -storage mass-storage \
        -rootfs mass-storage:8 \
        -unprivileged 0 \
        -features nesting=1 \
        -onboot 1 \
        -net0 name=eth0,bridge=vmbr1,tag=7,ip=172.16.7.2/24,gw=172.16.7.1 \
        -nameserver 1.1.1.1
      

    2. Mount the Raw Virtual Disk for Staging:
      pct mount 105
      

    3. Decompress and Inject Standalone Docker Binaries into System Paths:
      tar -xf /mnt/pve/mass-storage/template/cache/docker-27.3.1.tgz -C /var/lib/lxc/105/rootfs/usr/bin/ --strip-components=1
      

    4. Decompress and Inject Pi-hole Image Template:
      tar -xf /mnt/pve/mass-storage/template/cache/pihole-pihole-latest-linux-amd64.image.tar.gz -C /var/lib/lxc/105/rootfs/root/
      

    5. Close and Fire Up Compute Layer:
      pct unmount 105
      pct start 105
      


🐳 Phase 4: Initializing Engine Layers (Inside Container Shell)

Navigating straight inside the container environment using pct enter 105.
    1. Iptables isn’t natively available in the Debian Image so…
    2. Mitigate Missing Network Engine Elements (iptables Bypass JSON Config):
      Because the thin container lacked the legacy iptables stack, we commanded Docker to use a hypervisor-safe block driver storage matrix and turn off firewall manipulation:
      mkdir -p /etc/docker
      cat << 'EOF' > /etc/docker/daemon.json
      {
        "exec-opts": ["native.cgroupdriver=cgroupfs"],
        "storage-driver": "vfs",
        "iptables": false
       }
      EOF
      

    3. Start Core Engine Systems:
      dockerd > /var/log/dockerd.log 2>&1 &
      

    4. Ingest Web Registry Layouts:
      docker load -i /root/pihole-pihole-latest-linux-amd64.image.tar
      


🎯 Phase 5: Executing the Pi-hole v6 Application Tenant

Deploying with variables matching the newly restructured Pi-hole v6 engine specifications.
    1. The Live Run Sequence:
      docker run -d \
        --name=pihole \
        --net=host \
        -e TZ="America/Chicago" \
        -e FTLCONF_webserver_api_password="WebLoginPasswordHere" \
        -e FTLCONF_dns_listeningMode="ALL" \
        -e FTLCONF_dns_upstreams="1.1.1.1;9.9.9.9" \
        -v pihole_config:/etc/pihole \
        -v dnsmasq_config:/etc/dnsmasq.d \
        --restart=unless-stopped \
        pihole/pihole:latest
      

        • --net=host: Bypassed broken virtual bridge layers, mapping directly to VLAN 7 network cards.
        • FTLCONF_webserver_api_password: Resolved the deprecated WEBPASSWORD v5 variable problem.
        • FTLCONF_dns_listeningMode="ALL": Opened cross-subnet resolution rules, allowing external VLAN query targets.
        • FTLCONF_dns_upstreams: Seaded upstream parameters to prevent silent drops.


🔒 Phase 6: Core System Persistence (Automation on Boot)

Because raw binary files do not auto-load rules or daemons natively when an LXC starts, a custom boot initialization script was injected to force persistence across power outages.
cat << 'EOF' > /etc/init.d/docker
#!/bin/sh
### BEGIN INIT INFO
# Provides:          docker
# Required-Start:    $network $remote_fs
# Required-Stop:     $network $remote_fs
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Start Docker Daemon
### END INIT INFO

case "$1" in
  start)
    echo "Starting Docker Daemon..."
    # Carve runtime networks back into memory paths
    nft add table inet filter 2>/dev/null
    nft add chain inet filter input { type filter hook input priority 0 \; policy accept \; } 2>/dev/null
    nft add rule inet filter input udp dport 53 accept 2>/dev/null
    nft add rule inet filter input tcp dport 53 accept 2>/dev/null
    # Run core daemon engine
    /usr/bin/dockerd > /var/log/dockerd.log 2>&1 &
    ;;
  stop)
    echo "Stopping Docker Daemon..."
    killall dockerd
    ;;
  restart)
    $0 stop
    sleep 2
    $0 start
    ;;
  *)
    echo "Usage: $0 {start|stop|restart}"
    exit 1
    ;;
esac
exit 0
EOF

chmod +x /etc/init.d/docker
update-rc.d docker defaults
Holy Ballssacks bro, why does LXc containers have to be such a bitch.. all it should be is define the imagine you want to use, and the properties you want to define just like the docker deployment, i shouldn’t have to setup a LXC to run docker to run a proper container… fucking LXC.
Direct Comparison of Approaches
Feature 🛠️ The Video’s Approach (Native Script) 🟦 Our Approach (Docker inside LXC)
Architecture LXC ➡️ Pi-hole Script LXC ➡️ Docker ➡️ Pi-hole Container
How it works The script installs software packages (lighttpd, php, pihole-FTL) directly into the Debian template file system, just like a traditional VM or physical server. The Debian template is treated as a clean, blank slate. Docker manages the dependencies, keeping the Pi-hole completely sealed off in its own application layer.
Internet Needs Requires constant, wide-open internet during installation to pull dozens of Linux package dependencies via apt. Can be done 100% offline. We only needed a tiny firewall hole for the single pre-packaged image file.
Upgrades You must run pihole -up inside the console, which modifies the system packages and can break if dependencies drift. To upgrade, you simply delete the container and spin up a new image version. Your underlying configuration directories stay untouched.
Flexibility If you want to run another tool (like a dashboard or home automation), its packages mix with Pi-hole’s web server settings, often causing conflicts. You can spin up 5 more isolated Docker containers inside this same LXC host without them ever interfering with Pi-hole.
Why did the video creator do it that way?
The video creator used the native script because it is the easiest path for a standard home lab with wide-open internet access. You just paste one command (curl -sSL https://pi-hole.net | bash), click “Next” on a few menus, and it handles everything automatically. They treat the LXC exactly like a lightweight VM because they don’t have strict network constraints.
Why were we forced to use our approach?
Your specific environment made the video’s method nearly impossible for two massive reasons:
    1. Your PVE Host is Offline: The native Pi-hole installer script expects to talk to official Debian package repositories to download web servers and compilation tools. Because your host couldn’t reach the open web, that script would have crashed immediately on missing dependencies.
    2. Your Strict Palo Alto Firewall: Even if we got the script running, a standard Linux installation queries dozens of different repository mirrors across random IP addresses and domain spaces. Your Palo Alto would have blocked 90% of them. By using Docker, we only had to tell the firewall to trust one single destination (github.com) to get the container fully populated.
Our approach required a lot more upfront engineering, but you ended up with a far more resilient setup. Your core Debian system remains pristine, and your Pi-hole is perfectly contained.
But wait you must be asking, how do we manage it. Well, the web interface will be available for most administrative work.
Why it doesn’t just go to the dashboard login… unno… anyway.  “Nooo, like the container…” well you should have access to the PVE host, enter the LXC with “pct enter 105”, replacing 105 with the ID of your LXC container.  “nooo, the docker container”
ohh…
root@docker-host:~# docker exec -it pihole /bin/bash
failed to create runc console socket: stat /run/user/0: no such file or directory: unknown
root@docker-host:~# mkdir -p /run/user/0
root@docker-host:~# docker exec -it pihole /bin/bash
docker-host:/#
look we’re in… but why doesn’t it have its own hostname?
“The reason you are seeing docker-host inside the container is because your container is running on the host network mode (–net=host).In Docker, when a container uses the host network stack, it bypasses Docker’s standard network isolation. It shares the host’s network namespace, IP addresses, network interfaces, and hostname directly.”
What about updates? … Well, adjusted security rule to allow app: apt-get, and destination security.debian.org and deb.debian.org, run apt update and apt upgrade, good to go. Just make a backup first. Pi-hole updates? whent he latest container release is available, simple bring down the instance, pull the latest image, and bring the container back up.
I wish containers were a bit different on PVE, but I also understand their complexities over advanced deployments. but it would be nice if they had simple FOSS containers for simple stuff like Pi-Hole, let me click add containerized app in the PVE host menu, pick the app (in this case Pi-Hole) specify the options (IP[vlan], DNS, Hostname) click deploy and it just runs the container on the PVE host itself and be done with it, updates then would just be stop the container, pull latest image, bring the container up.. none of this having to manage an LXC with its own apps and libraries and have to run apt update on it… ehhh whatever, I’ll just move on at this point lol…
Man, but I can’t back it up using Veeam, you can only backup VMs. So, unless I want to manage to backup solutions Veeam + PVE Backup Server, I’d have to convert to a VM instead of an LXC anyway. The main advantage was RAM savings..
vs
well **** I might just end up throwing this in the trash and replacing it with a VM running docker instead… **** me, well.. guess you live and you learn. but it’s still a super tight setup if someone wanted to do PiHole on LXC, while being able to do a regular docker style pull instead of a raw dawg install,
Hope this helps someone.
*Update* I had an issue where pihole wasn’t starting on boot, so used crontab to resolve it.
  • Run crontab -e .
  • Arrow down to the very bottom line.
  • Paste the fallback rule:
    text
    @reboot dockerd > /var/log/dockerd.log 2>&1 &