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.
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.
Features
section where you can check boxes or via CLI: pct set container-ID --features nesting=1,keyctl=1
.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
Enter the Container. Use Proxmox’s console (via CLI: pct exec Container-ID -- bash
) or SSH into the container (ssh root@Container-ID).
Update and Install Dependencies: apt update && apt upgrade -y
.
Install Docker Engine: apt install -y docker.io
Enable and start Docker so it runs on boot: systemctl enable --now docker
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.
[...]
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
[...]
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.
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
docker stop container-id/name
(Gracefully stop a running container), docker kill container-id/name
(Force stop a container (when it’s unresponsive)).docker pause/unpause container-id/name
(pauses a container/resume a paused container).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
.docker logs/inspect container-id/name
# 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: