Containers

Create Debian and Ubuntu LXC Containers on Proxmox VE

An LXC container gives you a full Debian or Ubuntu userland on Proxmox VE without the weight of a virtual machine. It shares the host kernel, starts in about a second, and an idle container holds onto 30 to 100 MB of RAM instead of the half a gigabyte a VM reserves before it does any work. For Linux services on a homelab or a busy node, a Proxmox LXC container is almost always the right unit to reach for first.

Original content from computingforgeeks.com - post 135341

This guide creates containers two ways: the Create CT wizard in the web interface, and the pct command line for repeatable, scriptable builds. It then goes past the install into the parts most guides skip: unprivileged security, resizing CPU and disk live, static addressing, bind mounts, running Docker inside a container, snapshots, backups, and turning a working container into a reusable template. Every command here was run on Proxmox VE 9.2 in June 2026, building Debian 13 and Ubuntu 24.04 containers on a real node.

When to use an LXC container instead of a VM

Use an LXC container when the workload is Linux and you want density and speed. Use a virtual machine when you need a different kernel, a non-Linux guest, or hard isolation between tenants. The split comes down to the kernel: a container borrows the host’s, a VM boots its own.

AspectLXC containerVirtual machine
KernelShares the host kernelRuns its own kernel
Boot timeAbout a second20 to 60 seconds
Idle memory30 to 100 MB512 MB or more reserved
IsolationNamespace and cgroup levelFull hardware virtualization
Guest OSLinux onlyLinux, Windows, BSD, anything
Live migrationRestart on the target nodeLive migration supported
Best forLinux services, density, homelab appsWindows, custom kernels, strict isolation

There is one practical consequence of the shared kernel worth stating up front. A container cannot run a kernel module the host does not have, and it cannot run Windows. If you are moving workloads off VMware and some of them are Windows, those stay as VMs. Our guide on migrating from VMware ESXi covers that path. Everything Linux is a candidate for a container.

Prerequisites

  • A working Proxmox VE node. If you are starting from bare metal, follow the Proxmox VE install guide first, or the install on top of Debian walkthrough.
  • Root shell access to the node, by SSH or the web console.
  • A network bridge. A default install gives you vmbr0 connected to your LAN.
  • Storage with space for templates and container root filesystems. The defaults local (for templates) and local-lvm (for disks) are enough.

LXC is built into Proxmox. There is nothing to install on the node. Confirm the version you are on:

pveversion

The node used for this guide reports the current release and kernel:

pve-manager/9.2.2/b9984c6d90a4bd80 (running kernel: 7.0.2-6-pve)

Download a Debian or Ubuntu container template

A container is built from a template, which is a compressed root filesystem. Proxmox ships a catalog of them through the appliance manager, pveam. Refresh the catalog first:

pveam update

List what is available for Debian and Ubuntu:

pveam available --section system | grep -E 'debian|ubuntu'

On a current node the catalog carries both supported Debian releases and every Ubuntu LTS plus the latest interim:

system          debian-12-standard_12.12-1_amd64.tar.zst
system          debian-13-standard_13.1-2_amd64.tar.zst
system          ubuntu-22.04-standard_22.04-1_amd64.tar.zst
system          ubuntu-24.04-standard_24.04-2_amd64.tar.zst
system          ubuntu-25.04-standard_25.04-1.1_amd64.tar.zst
system          ubuntu-26.04-standard_26.04-1_amd64.tar.zst

The catalog is not limited to Debian and Ubuntu. Rocky, AlmaLinux, Alpine, Fedora, and more are all there, and the steps below are identical whichever you pick. If you want a RHEL-family base, the Rocky and AlmaLinux template guide covers the specifics.

Pull the two we will build with. Templates download to the local storage:

pveam download local debian-13-standard_13.1-2_amd64.tar.zst
pveam download local ubuntu-24.04-standard_24.04-2_amd64.tar.zst

Each download finishes with a checksum verification. Confirm both landed:

pveam list local

Both templates are now cached on the node, ready to build from:

NAME                                                         SIZE
local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst         123.70MB
local:vztmpl/ubuntu-24.04-standard_24.04-2_amd64.tar.zst     135.03MB

Create a Proxmox LXC container in the web interface

The graphical path is the fastest way to get a feel for the options. Click Create CT in the top right of the web interface. The wizard opens on the General tab, where you set the container ID, hostname, and root credentials. Two checkboxes matter here. Leave Unprivileged container ticked, which is the safe default and is covered below. Tick Nesting if you plan to run Docker or systemd-in-systemd workloads inside.

Proxmox VE 9 Create LXC Container wizard General tab with unprivileged and nesting enabled

The Template tab is where the earlier pveam download pays off. Pick the local storage, then choose the Debian or Ubuntu template from the dropdown. If the list is empty, the template did not download to that storage.

Selecting a Debian 13 LXC template in the Proxmox VE Create CT wizard

Step through Disks (root filesystem size and storage), CPU (cores), and Memory (RAM and swap in MB). The Network tab is the one to slow down on. Set the bridge to vmbr0, leave IPv4 on DHCP for a quick start, or select Static and enter an address in CIDR form with a gateway. The firewall checkbox enables the per-container Proxmox firewall.

Proxmox VE LXC container Network tab with vmbr0 bridge and DHCP IPv4

The Confirm tab prints every setting as a key and value table. Read it. This is exactly the configuration the pct create command in the next section produces, which makes the wizard a good way to learn the CLI flags by name.

Proxmox VE Create CT Confirm tab showing the full LXC container configuration

Tick Start after created and click Finish. The container is up in a couple of seconds.

Create the container from the command line with pct

The wizard is fine for one container. For repeatable builds, the pct create command is the tool. Every wizard field maps to a flag. Here is the Debian build, an unprivileged container with nesting enabled, a DHCP address, and an 8 GB root disk on local-lvm. Replace 100 with any unused ID on your node; run pct list to see what is taken.

pct create 100 local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst \
  --hostname debian-ct \
  --cores 2 --memory 2048 --swap 512 \
  --rootfs local-lvm:8 \
  --net0 name=eth0,bridge=vmbr0,ip=dhcp,firewall=1 \
  --unprivileged 1 \
  --features nesting=1 \
  --password \
  --onboot 1 \
  --start 1

The --password flag with no value prompts for the root password instead of leaving it in your shell history. To inject an SSH key instead, swap it for --ssh-public-keys /root/.ssh/id_ed25519.pub. The Ubuntu build is the same command with a different template:

pct create 101 local:vztmpl/ubuntu-24.04-standard_24.04-2_amd64.tar.zst \
  --hostname ubuntu-ct \
  --cores 2 --memory 2048 --swap 512 \
  --rootfs local-lvm:8 \
  --net0 name=eth0,bridge=vmbr0,ip=dhcp,firewall=1 \
  --unprivileged 1 --features nesting=1 \
  --password --onboot 1 --start 1

Read back the full configuration of a container at any time with pct config:

pct config 100

The output mirrors every flag from the create command, including the unprivileged and nesting settings:

arch: amd64
cores: 2
features: nesting=1
hostname: debian-ct
memory: 2048
net0: name=eth0,bridge=vmbr0,firewall=1,hwaddr=BC:24:11:A3:7B:F1,ip=dhcp,type=veth
onboot: 1
ostype: debian
rootfs: local-lvm:vm-100-disk-0,size=8G
swap: 512
unprivileged: 1

Privileged vs unprivileged containers

An unprivileged container maps its root user (UID 0) to an unprivileged host UID, starting at 100000. Root inside the container is nobody special on the host, so a container breakout lands an attacker as an unprivileged user rather than host root. This is the default and it is the right default. Only choose a privileged container when something genuinely needs it, such as certain kernel-level mounts, and only when you trust everything that runs inside. The UID mapping has one visible side effect on bind mounts, covered further down.

Start and access the container

Containers created with --start 1 are already running. The lifecycle verbs are what you would expect:

pct start 100
pct reboot 100
pct shutdown 100
pct stop 100

To get a root shell inside the container with no password, use pct enter from the node:

pct enter 100

To run a single command without an interactive shell, use pct exec with a -- separator. This checks the OS release, the kernel, and the address the container picked up:

pct exec 100 -- bash -c 'cat /etc/os-release | grep PRETTY; uname -r; ip -4 addr show eth0 | grep inet'

The container reports its Debian release, the running kernel, and the address it leased:

PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
7.0.2-6-pve
    inet 192.168.1.136/24 brd 192.168.1.255 scope global dynamic eth0

Notice the kernel reported inside the Debian container is the host’s Proxmox kernel, not a Debian one. That is the shared-kernel model in one line of output. For a serial console (handy when networking is broken), use pct console 100 and exit with Ctrl+a q. Once SSH is configured inside, you reach the container over the network like any other host.

Every container also has a Summary page in the web interface with live CPU, memory, and disk graphs, its status, and its address.

Running Debian LXC container Summary in Proxmox VE showing CPU memory and IP

Resize CPU, memory, and disk

Containers resize without a rebuild. CPU and memory changes apply live, no reboot needed:

pct set 100 --cores 4 --memory 4096

Growing the root disk uses pct resize with a + increment. This adds 4 GB to the existing disk and expands the filesystem in one step:

pct resize 100 rootfs +4G

Check it from inside the container:

pct exec 100 -- df -h /

The root filesystem now reads 12 GB, with the extra space already usable:

Filesystem                        Size  Used Avail Use% Mounted on
/dev/mapper/pve-vm--100--disk--0   12G  571M   11G   6% /

Disks only grow. Passing a smaller size returns unable to shrink disk size and changes nothing, which is the safe behaviour. To make a container smaller, back it up, restore into a new container with a smaller root, and delete the old one.

Give the container a static IP

DHCP is fine for throwaway containers. Anything that other machines connect to wants a fixed address. Rewrite net0 with a static IP in CIDR form and a gateway:

pct set 100 --net0 name=eth0,bridge=vmbr0,ip=192.168.1.50/24,gw=192.168.1.1,firewall=1

A running container needs a reboot to apply a changed address. After pct reboot 100, confirm it from inside:

pct exec 100 -- ip -4 addr show eth0 | grep inet

Set DNS at the same time with --nameserver and --searchdomain on pct set if your network does not hand those out over DHCP.

Share a host directory with a bind mount

Bind mounts pass a directory from the host straight into the container, which is how you share media libraries, datasets, or a backup target without copying anything. Add one with a mount point flag:

mkdir -p /opt/ct-shared
pct set 100 --mp0 /opt/ct-shared,mp=/mnt/shared

The container can now read /mnt/shared. Writing is where the unprivileged UID mapping bites. A fresh host directory is owned by host root (UID 0), but the container’s root is host UID 100000, so it cannot write:

pct exec 100 -- bash -c 'echo test > /mnt/shared/ct-note.txt'
bash: line 1: /mnt/shared/ct-note.txt: Permission denied

The fix is to give the host directory to the mapped UID range. Container root maps to host UID 100000, so chown the directory to match:

chown 100000:100000 /opt/ct-shared

Now the container writes, and the host sees the file owned by 100000, which is container root viewed from outside:

-rw-r--r-- 1 100000 100000 18 Jun  2 15:59 /opt/ct-shared/ct-note.txt

For a non-root user inside the container, add that user’s container UID to 100000 and chown to the result. This UID arithmetic is the price of the security an unprivileged container buys you.

Run Docker inside the container

Running Docker inside an LXC container is a popular pattern for keeping container workloads off a full VM. It needs two features set on the LXC container: nesting and keyctl. Update the features and reboot for them to take effect:

pct set 100 --features nesting=1,keyctl=1
pct reboot 100

Install Docker inside the container the same way you would on any Debian box. The distribution package is enough for most uses, or follow the Docker install guide for Debian for the upstream repository:

pct exec 100 -- bash -c 'apt-get update && apt-get install -y docker.io'

Confirm the daemon is up and run the test image, all from inside the container:

pct exec 100 -- docker run --rm hello-world

Docker pulls the image and runs it cleanly from inside the container:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

This works, and many homelabs run this way for years. Be honest about the trade though. Proxmox staff recommend a VM for Docker because the nesting relaxes some isolation, and a few storage drivers behave differently on top of a container filesystem. For a personal node it is fine. For multi-tenant or security-sensitive hosts, give Docker its own VM.

Snapshot and back up the container

Snapshots capture a point in time you can roll back to. They need a storage that supports them, such as LVM-thin or ZFS, which the default local-lvm is. Take one before a risky change:

pct snapshot 101 clean-base --description 'fresh install'
pct listsnapshot 101

Roll back with pct rollback 101 clean-base. One gotcha catches people: a container that has a bind mount cannot be snapshotted, and Proxmox returns snapshot feature is not available. Bind-mounted host directories live outside the container’s storage, so there is nothing consistent to snapshot. Keep stateful data you want in snapshots on the container’s own root or a managed volume, not a bind mount.

Snapshots are not backups. For a real backup, vzdump writes a portable archive you can restore on any node:

vzdump 100 --storage local --mode snapshot --compress zstd

The whole container compresses to a single portable archive in a few seconds:

INFO: creating vzdump archive '/var/lib/vz/dump/vzdump-lxc-100-2026_06_02-16_01_48.tar.zst'
INFO: Total bytes written: 601292800 (574MiB, 133MiB/s)
INFO: archive file size: 167MB
INFO: Finished Backup of VM 100 (00:00:05)

Restore that archive into a new container with pct restore 105 /var/lib/vz/dump/vzdump-lxc-100-...tar.zst. For scheduled, deduplicated backups across many containers, point the node at a Proxmox Backup Server instead of dumping to local storage.

Turn a container into a template and clone it

Once a container is configured the way you like, the way you build the next ten is to clone it. Convert a stopped container into a template:

pct stop 101
pct template 101

Templating fails with unable to create template, because CT contains snapshots if any snapshots remain. Remove them first with pct delsnapshot 101 clean-base, then template. From the template, clone as many containers as you need, each with its own ID and hostname:

pct clone 101 102 --hostname web-01

Start the clone and it comes up as an independent container with a fresh DHCP lease and the hostname you set. This is the golden-image workflow: build once, snapshot the base, template it, and stamp out copies in seconds. You can also clone a regular stopped container directly without templating it first, which is handy for one-off duplicates.

Faster path: the Proxmox VE community scripts

For ready-to-run application containers, the community-maintained helper scripts wrap container creation and app install into one command. The original tteck scripts were handed off and now live at the community-scripts ProxmoxVE project. They are convenient, but they run as root on your node, so read the script before you pipe it into a shell. The native pveam and pct path in this guide is what those scripts automate for you, and it is the one that does not break when a project moves.

Troubleshooting

“unable to open file … template … No such file or directory”

The template name in your pct create command does not match a downloaded template. Run pveam list local and copy the exact local:vztmpl/... string. A version in the filename changes between catalog updates, so do not assume an old command still matches.

“snapshot feature is not available”

The container has a bind mount, or its root filesystem is on storage that cannot snapshot (plain directory storage). Remove the bind mount, or move the container to LVM-thin or ZFS, then snapshots work.

Docker fails to start inside the container

The container is missing the keyctl feature, or it is privileged when it should be unprivileged with nesting. Set --features nesting=1,keyctl=1 and reboot the container. Check journalctl -u docker inside the container for the exact failure.

Permission denied writing to a bind mount

An unprivileged container’s root is host UID 100000. Chown the host directory to 100000:100000, or to the mapped UID of whichever user writes inside the container.

pct command quick reference

Most container management on a node comes down to a handful of pct subcommands. Keep this within reach:

TaskCommand
List containerspct list
Create from templatepct create ID TEMPLATE --options
Start / stop / rebootpct start|stop|reboot ID
Root shell insidepct enter ID
Run one commandpct exec ID -- command
Show configurationpct config ID
Change CPU / RAMpct set ID --cores N --memory MB
Grow the diskpct resize ID rootfs +NG
Snapshot / roll backpct snapshot ID name / pct rollback ID name
Convert to templatepct template ID
Clonepct clone SRC NEWID --hostname name
Back upvzdump ID --storage local --compress zstd

With the template downloaded once and these commands in muscle memory, a new Debian or Ubuntu container is a five-second job, and a tested base container clones into a fleet just as fast. That speed is the whole reason to reach for a container before a VM on Proxmox.

Related Articles

Containers How To Run Ghost CMS in Docker Containers Debian Mount Google Drive on Ubuntu/Debian and Upload Files Security Install Metasploit Framework on Ubuntu / Debian Virtualization How To Run Alpine Linux with Vagrant

Leave a Comment

Press ESC to close