Docker in LXC containers
On this page
- A container or a virtual machine
- Privileged or unprivileged
- Creating a container for Docker
- Enabling the features on an existing container
- Installing Docker
- Running a single container
- Deploying a Compose application
- Starting the app at boot
- Updating an app
- Logs, listing and removal
- Worked example
- Docker’s storage driver
- Troubleshooting
- Related pages
The lws app commands install Docker in a Debian or Ubuntu LXC container and
run single containers or Compose applications in it. LWS does all of it with
pct exec on the Proxmox host: nothing is installed on the host, and the
container does not need SSH.
A container or a virtual machine
Proxmox recommends running Docker in a QEMU virtual machine. A VM is isolated
from the host’s kernel and can be live-migrated; a container shares the
host’s kernel and moves to another node only with a restart. Docker does run
in containers, which need less memory and start faster, provided they have
the LXC feature nesting, plus keyctl when unprivileged. Updates to the
host’s kernel, LXC or AppArmor, or to Docker, have broken this setup before:
try host upgrades on a container that is not critical first. LWS manages
containers only; in a VM, install Docker with your usual tools.
Privileged or unprivileged
lws lxc run creates a privileged container unless you pass
--unprivileged, because that is the default of pct create.
- In an unprivileged container, root inside is mapped to an unprivileged
user on the host. Proxmox recommends this type. Docker needs
nesting=1andkeyctl=1. - In a privileged container, root inside is root on the host, held back
by AppArmor and seccomp. Proxmox advises it for trusted environments only.
Docker needs
nesting=1, which Proxmox notes exposes host procfs and sysfs content to the container.
Decide when you create the container: pct set cannot switch an existing
container between the two types.
Creating a container for Docker
lws lxc run --size lws-web --unprivileged --features nesting=1,keyctl=1 \
--hostname web --dhcp \
--image-id local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst
LWS prints the ID it picked (Instance 100 created successfully.) and starts
the container, named web-100: LWS appends the ID to the hostname. This page
uses container 100. lws-web gives 2 GB of memory, a 2-CPU limit and a
20 GiB root disk (Instance sizes); Docker keeps its
images and volumes on that disk, so size it for them.
Enabling the features on an existing container
app setup checks the features first. When one is missing, it warns
(Container 100 lacks the LXC feature(s) nesting, ...) and installs Docker
anyway. --enable-nesting adds the missing features to those already set
(pct set --features) and restarts the container with pct reboot, which
interrupts what runs in it, before installing:
lws app setup 100 --enable-nesting
Installing Docker
lws app setup 100
The container must be running. app setup installs from the distribution’s
own apt repositories, not Docker’s: docker.io, then docker-compose-v2
(Compose v2, on Ubuntu) or, where that package does not exist,
docker-compose (Debian; in Debian 12 it is the older Compose v1, 1.29).
The other app commands use docker compose when it works and
docker-compose otherwise, so either is fine. When Docker and Compose are
already there, from any source, app setup says so and installs nothing.
Running a single container
app run passes the arguments after -- to docker run. Use -d, or LWS
waits until the container exits; a restart policy brings it back after a
reboot. The app deploy actions do not see containers started this way.
lws app run 100 -- -d --name hello --restart unless-stopped -p 8080:80 nginx:1.27
Deploying a Compose application
app deploy takes an action, the container ID and --compose-file, a path
on your machine or an http:// or https:// URL. LWS downloads a URL itself,
so the container does not need to reach it.
The first service in the file names the application. LWS copies the file
to /opt/lws/apps/<app>/docker-compose.yml in the container and uses <app>
as the Compose project name (-p <app>), so later actions find the same
containers. The name may contain letters, digits, - and _; keep it
lowercase, as Compose v2 rejects capitals in project names.
| Action | What runs in the container |
|---|---|
install |
copies the file, then up -d |
status |
ps |
stop, start, restart |
stop, start, restart |
uninstall |
down, then removes the auto-start unit |
Only install copies the file; the other actions read the app name from your
local file and use the copy in the container. uninstall leaves that copy in
place, and down keeps named volumes.
LWS copies the Compose file and nothing else. Relative paths in it resolve
against /opt/lws/apps/<app>/ in the container, and a local .env file is
not copied. Use images rather than build:, and keep settings in the file or
in named volumes, or create the files in the container with lws lxc exec.
Starting the app at boot
--auto-start, with install only, writes and enables a systemd unit,
/etc/systemd/system/lws-<app>.service, in the container. It runs up -d
after Docker has started and down when it stops, so the app’s containers
are created afresh at each boot: keep data in volumes. The LXC container
starts with the host because lxc run sets onboot by default. Without
--auto-start, services come back only if they have a restart: policy.
Updating an app
lws app update 100 docker-compose.yml
app update copies the new file over the old one, then runs pull and
up -d, which recreates the services whose image or configuration changed.
The file is a positional argument here. Keep the first service name, or LWS
treats the file as a new app and leaves the old one running. A snapshot
taken first gives you a way back; see
Snapshots and backups.
Logs, listing and removal
lws app list 100
lws app logs 100 web_web_1 --tail 100
lws app deploy uninstall 100 --compose-file docker-compose.yml
lws app remove 100
app list prints running Docker containers as ID: name (image). Compose
names them after project and service: web_web_1 with Compose v1,
web-web-1 with v2. app logs prints the logs once; to follow them, run
pct exec 100 -- docker logs -f web_web_1 on the host.
app remove runs apt-get remove for whichever of docker.io,
docker-compose, docker-compose-v2 and docker-compose-plugin are
installed. It keeps images, volumes, /opt/lws/apps and the auto-start
units, so uninstall the apps first. --purge runs
docker system prune -a -f --volumes beforehand, which removes what no
running container uses (recent Docker versions prune only anonymous volumes).
Worked example
An unprivileged Debian 12 container running nginx with Compose, started at
boot. Save this as docker-compose.yml on your machine; the service web
names the app:
services:
web:
image: nginx:1.27
ports:
- "80:80"
volumes:
- html:/usr/share/nginx/html
restart: unless-stopped
volumes:
html:
Create the container and wait until hostname -I shows an address. Then
install Docker, deploy the app, check it, and request the page from your
machine at that address. The reboot at the end shows that the app comes back:
lws lxc run --size lws-web --unprivileged --features nesting=1,keyctl=1 \
--hostname web --dhcp \
--image-id local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst
lws lxc exec 100 "hostname -I"
lws app setup 100
lws app deploy install 100 --compose-file docker-compose.yml --auto-start
lws app deploy status 100 --compose-file docker-compose.yml
lws lxc exec 100 "systemctl is-enabled lws-web.service"
curl -I http://192.168.1.50/
lws lxc reboot 100
lws app list 100
To move to nginx:1.28 later, change the image in your file, take a
snapshot with lws lxc snapshot-add 100 before-nginx-update, and run
app update as shown above.
Docker’s storage driver
Docker uses the overlay2 storage driver where it can. The
Storage Driver: line of lws lxc exec 100 "docker info" shows the one in
use. vfs works but copies every image layer in full, which is slow and
takes much more disk space. It happens mostly with a root disk on ZFS,
depending on the OpenZFS and Docker versions; a root disk on LVM-thin
(local-lvm) does not have the problem.
Troubleshooting
LXC container 100 is not running. Start it with: lws lxc start 100:
app setup, run, deploy and update need a running container.
Failed to install Docker in container 100: ...: the rest of the message
comes from apt. Unable to locate package or Temporary failure resolving
usually means the container has no network or DNS yet. If apt-get is not
found, the container is not Debian or Ubuntu.
Failed to extract application name from Docker Compose file.: the file
is not valid YAML, has no services:, or its first service name has other
characters than letters, digits, - and _ (details in lws.log).
Docker containers fail with permission denied: an OCI runtime error
from app run or app deploy install mentioning permission denied usually
means a missing feature. Check the features: line of lws lxc show 100 and
run lws app setup 100 --enable-nesting.
Related pages
- CLI reference: every
appoption - Snapshots and backups
- Firewall security groups
- Troubleshooting