Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français
Lire en français

Create an LXC container in Proxmox VE

By updated

Improve this page

Logo Proxmox

This guide walks you through creating your first LXC container in Proxmox VE. You will understand why you would pick a container rather than a VM, master the options of the creation wizard (privileged vs unprivileged, nesting), and learn to manage your containers from the CLI with pct. At the end, you will have a working Debian container, ready to host your services.

What you will learn

  • The difference between an LXC container and a KVM VM: when to choose which
  • Privileged vs unprivileged: understanding the security implications
  • Nesting: allowing Docker or other containers to run inside
  • Create a container from the web interface and from the CLI with pct
  • Templates: where to find them and how to download them
  • The essential pct commands for day-to-day management

Prerequisites

ItemDetail
Proxmox VE installedVersion 8.x or 9.x, installation guide
Web interface masteredThe 3 levels Datacenter → Node → Guest, interface guide
Storage configuredAt least one storage with the "CT Templates" content type
Working networkA configured bridge (vmbr0 by default)

Key concepts before you start

LXC container vs KVM VM: what is the difference?

An LXC container (Linux Container) shares the Linux kernel of the Proxmox host. Unlike a VM, which emulates a complete computer with its own kernel, the container is just an isolated process. The result: near-instant startup, minimal memory footprint, native performance.

CriterionKVM VMLXC container
KernelIts own kernel (Linux, Windows, BSD...)Host kernel (Linux only)
StartupA few tens of secondsA few seconds
Minimum RAMSeveral hundred MBA few tens of MB
IsolationStrong (hypervisor)Medium (namespaces + cgroups)
OverheadLow on CPU, RAM reservedAlmost none
Supported OSAll (Windows, Linux, BSD...)Linux only
Live migrationYesNo (restart migration)

Privileged vs unprivileged: the security choice

This is the most important question when you create a container.

ModeRoot UID inside the containerSecurityUse case
Unprivileged (default)Mapped to UID 100000+StrongMost services, preferred
Privileged= host root (UID 0)WeakSpecific cases, listed below

Unprivileged: the container's root (UID 0) is mapped to an unprivileged user of the host, UID 100000 by default (the table is read in /proc/self/uid_map, see below). Even if an attacker escapes the container, they land with the rights of an ordinary user. This is the default mode when you create a container, so keep it unless you have a specific case.

Privileged: the container's root = the host's root. An escape flaw = root access to your server. The LXC team considers these containers insecure by design. Use them only in a trusted environment.

Nesting: containers inside the container

Nesting exposes part of /proc and /sys to the container, which allows:

  • Docker or Podman inside the container
  • Other nested LXC containers
  • systemd to isolate its services

Nesting + unprivileged is the recommended combination for running Docker in a Proxmox container.

Download a template

Before creating a container, you need a template, a base image containing a minimal Linux system.

  1. Open the template manager

    In the Proxmox interface, go to: Node → local (or your storage) → CT Templates

    ct-templates-view

  2. Download a template from the repositories

    Click the Templates button. A list of official templates appears. Select for example debian-13-standard, then click Download.

    ct-templates-download

    The download takes a few seconds. The template then appears in the list.

From the CLI with pveam:

Terminal window
# Update the list of available templates
pveam update
# List the available system templates
pveam available --section system
# Download a template to the local storage
pveam download local debian-13-standard_13.6-1_amd64.tar.zst
# List the downloaded templates
pveam list local

Create a container from the web interface

  1. Start the creation wizard

    Click the blue Create CT button at the top right of the interface. An 8-tab wizard opens.

  2. General tab: the container's identity

    OptionRecommended valueExplanation
    Node(your node)Server where the CT will be created
    CT ID(suggested automatically)Unique number, keep the suggested one
    Hostnamedebian-testContainer hostname
    Unprivileged containerChecked (default)Secure mode, keep it
    NestingChecked if DockerRequired for Docker/systemd
    Password(set one)Container root password

    general-tab

  3. Template tab: choose the base image

    • Storage: local (or your storage with the templates)
    • Template: debian-13-standard_13.6-1_amd64.tar.zst

    template-tab

  4. Disks tab: the container's storage

    OptionRecommended valueExplanation
    Storagelocal-lvmStorage for the rootfs; on a node installed on top of Debian, local
    Disk size (GiB)88 GB is enough for a basic container

    disks-tab

The first four steps set the container's identity, its image and its disk. The next ones set its resources and its network, then confirm the creation.

  1. CPU tab: processor

    OptionRecommended valueExplanation
    Cores11 core is enough for most services

    The container can use all the host's cores if you do not specify a limit. The "Cores" value sets the maximum.

    cpu-tab

  2. Memory tab: RAM

    OptionRecommended valueExplanation
    Memory (MiB)512512 MB, enough for a minimal container
    Swap (MiB)512Swap allocated to the container

    memory-tab

  3. Network tab: networking

    OptionRecommended valueExplanation
    Nameeth0Interface name inside the container
    Bridgevmbr0Default Proxmox bridge
    IPv4DHCP or static IPChoose according to your network
    FirewallCheckedEnables the Proxmox firewall

    For a static IP: enter the address in CIDR format (e.g. 192.168.1.100/24) and the gateway (e.g. 192.168.1.1).

    network-tab

  4. DNS tab: name resolution

    OptionRecommended valueExplanation
    DNS domain(leave empty)Inherits from the host
    DNS servers(leave empty)Inherits from the host

    By default, the container uses the DNS settings of the Proxmox host. You can specify custom DNS servers (e.g. 1.1.1.1 for Cloudflare).

  5. Confirm tab: check and create

    The wizard shows a summary. Check that Unprivileged is ticked, then click Finish.

    Optional: tick Start after created to start it immediately.

    confirm-tab

Create the same container from the command line

The web interface calls pct create. Here is the CLI equivalent:

Terminal window
# Download the template (if not already done)
pveam download local debian-13-standard_13.6-1_amd64.tar.zst
# Create the container
pct create 101 local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst \
--hostname debian-test \
--memory 512 \
--swap 512 \
--cores 1 \
--rootfs local-lvm:8 \
--net0 name=eth0,bridge=vmbr0,ip=dhcp,firewall=1 \
--unprivileged 1 \
--features nesting=1 \
--password "YourSecurePassword"
# Start the container
pct start 101

On a node installed on top of Debian, the local-lvm storage does not exist: replace --rootfs local-lvm:8 with --rootfs local:8. If Proxmox refuses, open the local storage to container volumes with pvesm set local --content iso,vztmpl,backup,images,rootdir,snippets.

How do you check that a resource limit applies?

The --memory and --cores values are not just a label: Proxmox translates them into cgroup limits, kernel counters that bound the container's processes. You can observe them from the host, in /sys/fs/cgroup/lxc/<ctid>/. For container 101 created above with --memory 512, once started:

Terminal window
cat /sys/fs/cgroup/lxc/101/memory.max
cat /sys/fs/cgroup/lxc/101/cpuset.cpus.effective

The first command must print 536870912, which is 512 MB expressed in bytes. The second lists the cores the container may use. These paths match Proxmox VE on Debian 13 with cgroups v2. Then compare with pct config 101: the configuration says what you asked for, the cgroups say what the kernel applies.

Swap is set separately and is added to the memory: with --memory 512 and --swap 512, the container can consume up to 1024 MB in total. For 512 MB to be a real ceiling, create the container with --swap 0, or fix it with pct set 101 --swap 0, then check that pct config 101 shows swap: 0.

To confirm the identity isolation, read the UID table from inside the container:

Terminal window
pct exec 101 -- cat /proc/self/uid_map

An unprivileged container prints 0 100000 65536: the container's root is host UID 100000, over 65536 consecutive identifiers.

Access the container

Three ways to open a shell in your container:

In the web interface: CT → Console. A getty session opens, log in with root and the password you set.

console

How do you run a service under a dedicated user?

A service that runs as root in the container has every right there. It is better to create a system user for it, with no password and no login shell, and to run the service under that identity. From the host, with pct exec:

Terminal window
pct exec 101 -- useradd --system --no-create-home --shell /usr/sbin/nologin web
pct exec 101 -- runuser -u web -- <service-command>

A process that is not root cannot, by default, listen on a port below 1024: this is why a test service sits on a port such as 8080. To check the result, find the container's address with pct exec 101 -- ip addr show eth0, query the port from the host, then check the process owner with pct exec 101 -- ps -o user=,pid=,comm= -u web.

The container's disk survives a pct reboot 101: a file written before the restart can be read back afterwards. A process started by hand, however, does not restart on its own; for it to come back with the container, you have to hand it over to a systemd service.

Configure nesting for Docker

If you want to run Docker in an LXC container, here is the complete configuration:

  1. Enable nesting and keyctl

    Terminal window
    # If the container already exists
    pct set 101 --features nesting=1,keyctl=1
    • nesting: exposes part of /proc and /sys
    • keyctl: required for Docker (kernel key management)
  2. Restart the container

    Terminal window
    pct reboot 101
  3. Install Docker in the container

    Terminal window
    pct enter 101
    # Inside the container: Docker from the official signed repository
    apt-get update && apt-get install -y ca-certificates curl
    install -m 0755 -d /etc/apt/keyrings
    . /etc/os-release
    curl -fsSL "https://download.docker.com/linux/${ID}/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/${ID} ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list
    apt-get update
    apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
  4. Check that Docker works

    Terminal window
    docker run hello-world

Mount a host directory (bind mount)

A bind mount gives the container access to a folder of the Proxmox host. Useful to share data or to reach NFS storage.

Terminal window
# Create the folder on the host
mkdir -p /mnt/bindmounts/shared
# Add the bind mount to the container (container stopped)
pct set 101 --mp0 /mnt/bindmounts/shared,mp=/shared

The host folder /mnt/bindmounts/shared becomes /shared in the container.

In an unprivileged container, the container's root is host UID 100000. A folder owned by root on the host therefore appears as owned by nobody in the container, and refuses writes. The remedy is not to switch to privileged: it is to give the folder to the mapped UID, that is 100000 plus the UID you want inside the container. For root (UID 0) to write:

Terminal window
chown 100000:100000 /mnt/bindmounts/shared

For a container user with UID 1000, the owner to set on the host is 101000. The isolation stays intact: the container gets only that folder.

pct cheat sheet

The essential pct commands to manage your containers:

ActionCommandNotes
Create a CTpct create <ctid> <template> ...See the complete example above
List CTspct listShows ID, status, name
Startpct start <ctid>
Clean shutdownpct shutdown <ctid>
Force stoppct stop <ctid>
Restartpct reboot <ctid>
Enter the CTpct enter <ctid>Immediate root shell
Run a commandpct exec <ctid> -- <cmd>E.g.: pct exec 101 -- hostname
Show the configpct config <ctid>
Change a parameterpct set <ctid> --<option> <val>E.g.: pct set 101 --memory 1024
Resize the diskpct resize <ctid> rootfs +5GGrow by 5 GB
Clone a CTpct clone <ctid> <newctid>Full clone
Create a snapshotpct snapshot <ctid> <name>
Delete a CTpct destroy <ctid>Warning: also deletes the data

Troubleshooting

SymptomLikely causeSolution
CT does not start: "permission denied"Storage incompatible with unprivilegedUse a dir or zfspool storage (not some LVM setups)
No networkMisconfigured bridgeCheck that vmbr0 exists and is up on the host
Docker fails: "keyctl" errorkeyctl feature missingpct set <ctid> --features nesting=1,keyctl=1 then reboot (the option replaces the whole feature list, so keep nesting in it)
UID mapping error or write refused on a bind mountThe host folder does not belong to the mapped UID (100000 + the container UID)chown 100000:100000 <folder> for the container's root, or chown 101000:101000 for UID 1000, without switching to privileged
CT starts but services failNesting missing for systemdpct set <ctid> --features nesting=1 (add keyctl=1 if Docker runs in the container)
Template not foundNot downloaded or wrong storagepveam list local to check
"Resource busy" on deletionCT still runningpct stop <ctid> before pct destroy
"TASK ERROR: CT is locked" errorPrevious operation interruptedpct unlock <ctid>

Put it into practice

The lab has you create container 170 from the Debian 13 template checked by pveam, unprivileged, with 1 core, 512 MB and no swap, then serve a page on port 8080 under the web system user. The validation reads unprivileged: 1 and the effective UID table, the page from the node and the process's ownership by web, the limits in the configuration and in the cgroups, then a file written before a container restart and read back after it.

Key takeaways

  1. LXC container = Linux only, but much lighter than a VM. Ideal for Linux services, Pi-hole, Home Assistant, Nginx...

  2. Unprivileged by default: keep this mode except in very specific cases. A permissions error on a bind mount is fixed with a chown on the mapped UID, not by switching to privileged.

  3. Nesting + keyctl for Docker: configure these features if you want to run Docker in a container.

  4. An immediate root shell: pct enter opens the container without authentication from the host.

  5. Online resizing: you can grow the disk of a running container with pct resize.

  6. Bind mounts with care: never mount system folders. Create a dedicated tree under /mnt/bindmounts/.

  7. TurnKey templates: think of them to quickly deploy a preconfigured application (WordPress, GitLab...).

  8. A limit can be observed: memory.max and cpuset.cpus.effective in /sys/fs/cgroup/lxc/<ctid>/ tell you what the kernel applies.

Is this project useful to you?

I take the time to write these guides, test the commands and check the labs so that you can learn for free. If this work helps you, buy me a coffee. It is a simple way to contribute to what comes next for the project.

Buy me a coffee (new tab) Support monthly (new tab) Learn more

Follow the project on LinkedIn