JustToThePoint English Website Version
JustToThePoint en español
Colaborate with us

Running Docker Inside an LXC Container on Proxmox: A Step-by-Step Guide

Reading computer manuals without the hardware is as frustrating as reading sex manuals without the software, Arthur C. Clarke

Proxmox is a powerful, complete, open-source server platform for enterprise virtualization to deploy and manage multiple virtualized environments on a single bare metal server (under one unified roof). It is great for home labs because: it is open-source and free; it tightly integrates the KVM hypervisor (Kernel-based Virtual Machine, an open-source hypervisor that allows you to run multiple virtual machines on a Linux system) and Linux Containers (LXC) providing a robust, scalable, and flexible environment for your virtual machines and containers, e.g., comprehensive backup and restore options for both VMs and containers, advanced storage and networking features, a built-in high availability clustering, and web-based management.

Each LXC container runs a full Linux distribution but shares the host’s Linux kernel. This makes them very lightweight and efficient, therefore ideal for tasks that need a full Linux environment without the overhead of hardware emulation.

Creating VM using scripts

Proxmox is widely used in the IT community for setting up home labs because it makes it easy to try out new services without high hardware requirements.

Docker is a software platform designed to make it easier to create, deploy, and run applications in isolated environments. It offers OS-level virtualization to deliver software in packages called containers. Docker images typically contain a single application or service (plus its dependencies). Like LXC, Docker containers share the host kernel (so they are fast and efficient), but they operate at the application level (often on top of a stripped-down OS). Docker shines in portability: you can pull an image and run it the same way on any Docker-compatible system.

It is recommended to use a VM for Docker for simpler isolation. Running Docker in an LXC container requires extra configuration (privileges, AppArmor settings) and is generally not a good idea.

Creating an LXC Container for Docker

  1. Choose a Linux Template. In Proxmox’s web GUI, go to Datacenter, Node (your host, e.g., myserver), Create CT. Pick a unique container ID and hostname (e.g., docker-container). For the OS template, select a supported distribution and allocate resources (CPU 1 or 2, memory 4096). Give it a root password (or add SSH keys) so you can log in later.
  2. Storage and Disk. Choose local storage (e.g. local-zfs or local-lvm) and give it enough disk (e.g., 20–80 GB).
  3. Network. On the Network tab, enable eth0 on the default bridge (usually vmbr0). If your DHCP server will assign it an IP, keep it at “DHCP” (or set a static IP if that’s what you want or need). This way, the container will appear on your LAN and you can easily SSH into it.
  4. Important features for Docker inside LXC. Enable Nest­ing and Keyctl (unprivileged only) features. This allows the LXC to run nested containers and handle kernel keyrings (manage cryptographic keys in a secure and efficient manner). In Proxmox’s web UI, go to Datacenter, Node (your host, e.g., myserver), Options, under a Features section where you can check boxes or via CLI: pct set container-ID --features nesting=1,keyctl=1.
  5. Click Finish to create the container. By default, Proxmox creates unprivileged containers (for security).
  6. Configuring the Container for Docker. Docker’s daemon by default uses the host’s AppArmor profile (docker-default). Inside an LXC, this can cause permission issues. We disable AppArmor confinement by setting the profile to unconfined. I choose to run a privileged LXC for Docker, but that comes with added security risk.

    AppArmor uses profiles to define what resources (files, network access, etc.) an application can access

# Edit the container’s config file (on the Proxmox host) at /etc/pve/lxc/Container-ID.conf,
# and add these lines at the very end.
lxc.apparmor.profile: unconfined # Remove AppArmor restrictions.
lxc.cgroup2.devices.allow: a # Allow all device access within the container.
lxc.cap.drop:

# Restart the Container
pct restart Container-ID

Installing Docker Inside the LXC

  1. Enter the Container. Use Proxmox’s console (via CLI: pct exec Container-ID -- bash) or SSH into the container (ssh root@Container-ID).

  2. Update and Install Dependencies: apt update && apt upgrade -y.

  3. Install Docker Engine: apt install -y docker.io

  4. Enable and start Docker so it runs on boot: systemctl enable --now docker

  5. Check that Docker is running and can launch containers: docker run --rm hello-world.
    docker run: Create and start a new container.
    ‐‐rm: This flag tells Docker to automatically remove the container when it exits.
    hello-world: This is the name of the image to use, a simple test image that outputs a message to confirm that Docker is working correctly.

    docker run --rm hello-world
    
    Unable to find image 'hello-world:latest' locally
    latest: Pulling from library/hello-world
    e6590344b1a5: Pull complete
    Digest: sha256:dd01f97f252193ae3210da231b1dca0cffab4aadb3566692d6730bf93f123a48
    Status: Downloaded newer image for hello-world:latest
    
    Hello from Docker!
    This message shows that your installation appears to be working correctly.
    [...]
    
  6. Find its IP address to SSH or access services. On the Proxmox web UI, host (e.g., myserver), Console: pct exec Container-ID -- ip addr show eth0. Once you know the IP, you can do ssh root@dirIpContainer.

    pct exec 105 -- ip addr show eth0
    [...]
    inet 192.168.1.48/24 brd 192.168.1.255 scope global dynamic eth0
    [...]
    
  7. Run a Web Server. docker run -d -p 80:80 nginx.
    docker run: basic command to create and start a new container.
    -d: This flag stands for “detached mode.” It runs the container in the background, allowing you to continue using the terminal.
    -p 80:80: This option maps port 80 of the host machine to port 80 of the container, meaning that any traffic directed to port 80 on your host will be forwarded to port 80 inside the container.
    nginx: This specifies the official NGINX image to use for the container.
    Finally, open a browser to the container’s IP and see the Nginx welcome page.

Basic Docker Container Management

  1. List running containers (docker ps) or all containers (docker ps -a, this list includes stopped ones).
    root@docker-container:~# docker ps
    CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS          PORTS                               NAMES
    17506b11b1b9   nginx     "/docker-entrypoint.…"   17 minutes ago   Up 17 minutes   0.0.0.0:80->80/tcp, :::80->80/tcp   competent_easley
    
  2. Stop containers: docker stop container-id/name (Gracefully stop a running container), docker kill container-id/name (Force stop a container (when it’s unresponsive)).
  3. Pause/unpause containers: docker pause/unpause container-id/name (pauses a container/resume a paused container).
  4. Remove containers. docker rm container-id/name removes a stopped container. docker rm -f container-id/name forces remove a running container, e.g., docker rm -f 17506b11b1b9.
  5. Container logs and (detailed) info: docker logs/inspect container-id/name

A more comprehensive example: MySQL container

# This gives you a MySQL database server accessible on port 3306.
docker run -d --name mysql -e MYSQL_ROOT_PASSWORD=mysecretpassword -p 3306:3306 mysql

# Connect to the MySQL Container
# Access the MySQL CLI inside the container
# Enter password when prompted: mysecretpassword
root@docker-container:~# docker exec -it mysql mysql -u root -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 9
Server version: 9.3.0 MySQL Community Server - GPL

Copyright (c) 2000, 2025, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
# Create a Database
mysql> CREATE DATABASE my_database;
Query OK, 1 row affected (0.015 sec)

mysql> USE my_database;
Database changed
# Create a Table
mysql> CREATE TABLE users (
    ->     id INT AUTO_INCREMENT PRIMARY KEY,
    ->     name VARCHAR(50) NOT NULL,
    ->     email VARCHAR(100) NOT NULL UNIQUE,
    ->     created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    -> );
Query OK, 0 rows affected (0.063 sec)
# Insert Data
mysql> INSERT INTO users (name, email)
    -> VALUES
    ->     ('Bob', 'bob@example.com'),
    ->     ('Charlie', 'charlie@example.com');
Query OK, 2 rows affected (0.020 sec)
Records: 2  Duplicates: 0  Warnings: 0
# Query Data
mysql> SELECT * FROM users;
+----+---------+---------------------+---------------------+
| id | name    | email               | created_at          |
+----+---------+---------------------+---------------------+
|  1 | Bob     | bob@example.com     | 2025-05-21 10:32:59 |
|  2 | Charlie | charlie@example.com | 2025-05-21 10:32:59 |
+----+---------+---------------------+---------------------+
2 rows in set (0.000 sec)

mysql> SELECT COUNT(*) FROM users;
+----------+
| COUNT(*) |
+----------+
|        2 |
+----------+
1 row in set (0.000 sec)
# Update Data
mysql> UPDATE users
email = 'new_bob@example.com'
WHERE name = 'Bob';    -> SET email = 'new_bob@example.com'
    -> WHERE name = 'Bob';
Query OK, 1 row affected (0.012 sec)
Rows matched: 1  Changed: 1  Warnings: 0
# Delete Data
mysql> DELETE FROM users
    -> WHERE name = 'Charlie';
Query OK, 1 row affected (0.009 sec)

mysql> exit
Bye
# List running containers
root@docker-container:~# docker ps
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS          PORTS                                                  NAMES
80805bf79dc6   mysql     "docker-entrypoint.s…"   10 minutes ago   Up 10 minutes   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp   mysql
# Remove the container
root@docker-container:~# docker rm -f 80805bf79dc6
80805bf79dc6

Persistent Storage. If you want data to survive container restarts, add a volume when creating the container:

docker run -d --name mysql -e MYSQL_ROOT_PASSWORD=mysecretpassword -v mysql_data:/var/lib/mysql -p 3306:3306 mysql
root@docker-container:~# docker exec -it mysql mysql -u root -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 10
Server version: 9.3.0 MySQL Community Server - GPL
[...]
mysql> CREATE DATABASE my_database;
mysql> USE my_database;
Database changed
mysql> CREATE TABLE users (
    ->     id INT AUTO_INCREMENT PRIMARY KEY,
    ->     name VARCHAR(50) NOT NULL,
    ->     email VARCHAR(100) NOT NULL UNIQUE,
    ->     created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    -> );
Query OK, 0 rows affected (0.068 sec)

mysql> INSERT INTO users (name, email)
    -> VALUES
    ->     ('Bob', 'bob@example.com'),
    ->     ('Charlie', 'charlie@example.com');
Query OK, 2 rows affected (0.021 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> exit
Bye
root@docker-container:~# docker exec mysql sh -c 'exec mysqldump --all-databases -uroot -p"$MYSQL_ROOT_PASSWORD"' > backup.sql

The last instruction is a backup and requires a better explanation:

Bitcoin donation

JustToThePoint Copyright © 2011 - 2025 Anawim. ALL RIGHTS RESERVED. Bilingual e-books, articles, and videos to help your child and your entire family succeed, develop a healthy lifestyle, and have a lot of fun. Social Issues, Join us.

This website uses cookies to improve your navigation experience.
By continuing, you are consenting to our use of cookies, in accordance with our Cookies Policy and Website Terms and Conditions of use.