Migrate Docker-in-LXC to OCI LXC on Proxmox VE 9.1
Migrate your Docker-in-LXC workloads to native OCI LXC on Proxmox VE 9.1. Reduce overhead and improve isolation with unprivileged, image-based containers.

On this page
If you've been running Docker inside an LXC container, Proxmox VE 9.1 gives you a concrete reason to stop: its native OCI LXC support lets those same images run without a nested Docker daemon, which usually means better isolation and less overhead. This guide walks through moving your Docker-in-LXC workloads over to OCI containers, step by step, so you keep your services running while shedding the privileged-mode hacks that made the old setup brittle.
Key Takeaways
- Native runtime: Proxmox VE 9.1 runs OCI images through systemd-nspawn and podman, so no Docker daemon has to live inside your container.
- Better isolation: OCI containers run unprivileged by default, removing the privileged-mode tradeoff that Docker-in-LXC forced on you.
- Import, don't rebuild: You pull OCI-compliant images into Proxmox storage and run them directly, so most existing images carry over unchanged.
- Migrate data: Volumes and config files have to travel with you explicitly; containers never move their state on their own.
- Newer and leaner: OCI LXC ships in 9.1 and is less battle-tested than Docker-in-LXC, and it wants you to be explicit about commands and init behavior.
Why Move Off Docker-in-LXC?
Running Docker inside an LXC container has been a popular homelab pattern for years. You spin up one container, install Docker in it, and stack several services on top. It works, but it rests on a foundation that fights the host, and the cracks usually show up at the worst possible moment—right when you're about to upgrade the host or patch a security hole.
The core problem is nesting. Docker wants to manage cgroups, create its own overlay filesystems, and talk directly to the kernel's container primitives. Inside an unprivileged LXC a lot of that is blocked, which is why most people just made the container privileged. A privileged LXC is the easy way out, but it throws away much of the isolation that made LXC worth using in the first place. You bought into lightweight containers to avoid VM overhead, then quietly accepted VM-level trust for a single container.
Then there's the resource tax. The Docker daemon inside your container is always awake, watching things and holding memory even when nothing is happening. For a handful of small services that overhead adds up quickly, and it's memory that never gets released.
Debugging gets annoying too. When a service dies you're digging through the Docker daemon's logs, then the container's logs, then the host's logs, just to figure out what actually happened. Three layers of logging for what should be one.
This is exactly the kind of setup worth reconsidering. If you want the security tradeoffs spelled out, the seccomp profile guide for Docker and K3s covers the hardening side in depth.
What Native OCI LXC Actually Is
Proxmox VE 9.1 added first-class support for OCI (Open Container Initiative) images. Under the hood it pairs systemd-nspawn, which does the containerization, with podman, which handles image management. Together they let you pull a standard OCI image and run it as a Proxmox-managed container with its own VMID.
This is different from the traditional LXC model, where you download an OS template—Ubuntu, Debian, Alpine—and build your environment inside it. It's also different from Docker-in-LXC, where a full Docker daemon lives one layer deep inside your container.
With OCI LXC, your image is the environment. You import it into Proxmox storage, and the container runs the image's own entrypoint. No nested daemon, no privileged mode required. That's the whole appeal: you get lightweight, image-based containers without the Docker daemon tax or the privileged-mode compromise. Most images on Docker Hub are OCI-compliant and will run as-is, though a handful that lean on Docker-specific runtime features may need tweaks. If you want the broader setup and tradeoffs before starting, the OCI image containers in Proxmox VE 9.1 post is the place to begin.
How to Migrate Your Docker Workloads to OCI LXC
Before you create anything, take inventory of what's actually running. You want a complete list of containers, the images they use, the volumes they depend on, and the published ports.
docker ps -a --format '{{.Names}}\t{{.Image}}\t{{.Ports}}'
docker images --format '{{.Repository}}:{{.Tag}}\t{{.Size}}'
docker volume ls
Keep that list handy. It becomes your migration checklist and stops you from forgetting that one small sidecar that everything else depends on.
Step 1 — Prepare the node
OCI containers need podman on the Proxmox node. It's available through the standard Proxmox package repos, so install it directly:
apt update && apt install -y podman
Then initialize an OCI image store on a storage backend. I'll use local here, but any directory or ZFS storage works:
pct oci-init local
Step 2 — Import your images
Here's where the migration gets concrete. For each service, import its image into the OCI store. Proxmox pulls the image for you through podman:
pct oci-import local docker.io/library/nginx:latest
pct oci-import local docker.io/library/postgres:16
If you'd rather pull first and inspect before importing, you can do that with podman directly:
podman pull docker.io/library/nginx:latest
pct oci-import local docker.io/library/nginx:latest
List what you've got before moving on:
pct oci-list local
A typical nginx image lands around 75 MB, and on a 100 Mbps connection a handful of images imports in well under a minute. Bigger images—a full Postgres or a Java app—take longer, so budget time for those and import them while you set up the rest.
Step 3 — Create and run each container
Run a container by giving it a VMID and the image you imported:
pct oci-run 9001 docker.io/library/nginx:latest
pct oci-run 9002 docker.io/library/postgres:16
The oci-run command both creates the container definition and starts it, so you manage it with the usual pct commands from here on:
pct status 9001
pct exec 9002 -- psql -U postgres -c 'SELECT version();'
pct stop 9002
Here's the first real gotcha: OCI containers don't come with an init system the way Docker does. If your image doesn't define an ENTRYPOINT or CMD, or if you don't supply one, the container starts and exits immediately. Always be explicit about what the container should run.
pct oci-run 9003 docker.io/library/redis:7 redis-server --appendonly yes
Another thing to check before you call it done: whether the container auto-starts on node reboot. OCI containers don't always inherit the autostart behavior that a normal LXC gets, so verify it in the config and set it explicitly if your services need to come back after a host restart.
Step 4 — Migrate your data
Containers are disposable; your data is not. This is the step most people rush and regret.
First, stop the old service and get its data onto Proxmox storage. If the old Docker-in-LXC lived on another node, rsync it over:
rsync -avh root@old-lxc:/var/lib/docker/volumes/pg_data/_data/ /mnt/pool/postgres/
Then push that data into the new container. Postgres expects its data owned by uid 999, so fix ownership after the transfer—files pushed as root land with the wrong owner and the container won't start:
pct push 9002 /mnt/pool/postgres /var/lib/postgresql/data
pct exec 9002 -- chown 999:999 /var/lib/postgresql/data
For the longer term, a bind mount keeps data on Proxmox storage instead of inside the container's writable layer. You can add one with pct set, pointing a path on your storage into the container's data directory, or just keep the push-and-pull pattern for smaller datasets.
Step 5 — Handle networking and dependencies
In the old setup you probably published ports with -p 8080:80. OCI containers in Proxmox don't expose ports the same way—they use Proxmox's networking model, and the practical move for a handful of web services is to put them behind a reverse proxy or firewall rule rather than juggling port mappings.
If your services talk to each other, update the connection strings. A Postgres container now lives at its own container IP or hostname, not at localhost the way it did inside the shared Docker network. This is where your inventory list from the top of the guide actually earns its keep.
Give each service a real request before you tear down the old container. A quick curl through the proxy, or checking output with pct log 9001, is worth more than any checklist:
docker stop nginx postgres app
docker rm nginx postgres app
Is Unprivileged OCI LXC Secure Enough?
This is where the migration pays off in security. Docker-in-LXC almost always meant a privileged container, because Docker needs broad kernel access. An unprivileged OCI container removes that requirement, and that's a real win for a homelab or a multi-tenant setup.
You set it explicitly in the container config:
unprivileged: 1
With an unprivileged container, the container runs as an unmapped user on the host, which means a breakout is far harder to exploit. This pairs naturally with a locked-down seccomp profile to cut down the syscalls the container can make, and with a read-only root filesystem where the image allows it.
One caveat you'll hit quickly: unprivileged containers can't bind to ports below 1024 without extra setup, and images that expect to manage their own cgroups or kernel features will fail. If your workload needs that, you'll either front it with a proxy on a high port or accept a privileged container—and then you're back to the isolation you were trying to escape.
The Honest Tradeoffs
Native OCI LXC is a genuine step forward, but it isn't a drop-in replacement for every Docker-in-LXC setup. The biggest difference is tooling: you lose docker-compose, Docker networks, and the rest of the Docker ecosystem. An OCI container runs one image and one command, not a compose file with five services. If you built your stack on compose, you'll be translating those definitions into individual containers plus their own storage and network config. That translation is real work, and it's the part most people underestimate.
There's also maturity. Docker-in-LXC has years of production wear. OCI LXC ships in Proxmox VE 9.1 and is newer, so you're more likely to hit edge cases and find thin documentation when something goes wrong. That's fine for a homelab or a well-understood workload; it's a different calculation for a production system where downtime costs money.
Here's how the two approaches compare in practice:
| Aspect | Docker-in-LXC | Proxmox VE 9.1 OCI LXC |
|---|---|---|
| Runtime | Docker daemon inside LXC | systemd-nspawn + podman |
| Privilege | Usually privileged LXC | Unprivileged by default |
| Isolation | Nested namespaces + Docker | Native OCI namespaces |
| Image format | Docker images | OCI-compliant images |
| Overhead | Docker daemon + container | Minimal |
| Maturity | Mature, battle-tested | Newer (ships in 9.1) |
If your real goal is just isolating services so backups stay clean, the Docker LXCs on Proxmox post covers that angle instead of a full runtime swap.
Conclusion
Migrating from Docker-in-LXC to native OCI LXC on Proxmox VE 9.1 trades a nested Docker daemon for leaner, unprivileged containers that run your images directly. You take inventory, import the images, recreate each service, move the data, and decommission the old container—accepting that you'll lose compose and the Docker toolchain along the way. Start by pointing one low-risk service at an OCI container, verify it end to end, and only then plan the rest of the migration.


