In this lab you will build a centralized Docker management platform from scratch. You will provision a Proxmox LXC container, install Docker Engine inside it, and deploy Portainer Community Edition as a containerized service. Once your Portainer server is running you will register an existing remote Docker host using the Portainer Agent, giving you a unified web interface to manage container workloads across multiple hosts.
Create an Ubuntu 22.04 LXC container on Proxmox and install Docker Engine with nesting enabled. Deploy Portainer CE as a Docker container with persistent storage.
Deploy the Portainer Agent on the existing Docker host at 192.168.128.92 so the agent listens on TCP/9001 and Portainer CE can reach it.
Register the remote Docker host as a Portainer Environment and perform full container lifecycle operations (start, stop, logs) from the Portainer web UI.
Use a lightweight GNS3 Alpine Linux probe node to confirm Layer 3 reachability and port connectivity before and after agent deployment.
All devices reside on VLAN 1 of the lab management network (192.168.128.0/24) with default gateway 192.168.128.1. The GNS3 Cloud node bridges to Proxmox vmbr0 so the Alpine probe participates in the same Layer 2 domain.
| Device Name | Type | IP Address | Subnet | VLAN | Key Ports | Notes |
|---|---|---|---|---|---|---|
| proxmox-host | Proxmox VE Hypervisor | 192.168.128.191 | 255.255.254.0 | 1 | 8006/tcp | Physical or VM Proxmox host. Student must have shell or web UI access to create LXC. |
| portainer-lxc | Proxmox LXC (Ubuntu 22.04) | 192.168.129.11 | 255.255.254.0 | 1 | 9443/tcp · 9000/tcp | Created by student. Docker Engine + Portainer CE installed inside. nesting=1 required. |
| docker-host-01 | Existing Docker Host (Linux) | 192.168.128.92 | 255.255.254.0 | 1 | 9001/tcp | Pre-existing lab host. Portainer Agent deployed here by student. Has nginx + sample containers. |
| alpine-probe-01 | GNS3 Alpine Linux Node | 192.168.128.200 | 255.255.254.0 | 1 | — | Lightweight GNS3 node. Bridges to vmbr0 via GNS3 Cloud node. Used for ping + nc port probes. |
Complete all items below before beginning the Configuration section. A missing prerequisite will cause failures mid-lab.
Comfortable with file navigation, text editing (nano or vi), and systemctl commands. You will run multi-line shell commands and edit configuration files.
Ability to navigate to nodes, open a shell, and create LXC containers in the Proxmox web UI at port 8006.
Conceptual understanding of containers vs virtual machines. No Docker CLI expertise required — this lab builds those skills.
IP addressing, default gateways, TCP ports, and the ability to use SSH to connect to Linux hosts.
Verify you can access the Proxmox web UI at https://192.168.128.191:8006 and open a shell on the node.
Confirm the Ubuntu 22.04 LXC template is available. Run the following on the Proxmox shell:
pveam list local | grep ubuntu-22.04
If no output, download the template:
pveam update pveam download local ubuntu-22.04-standard_22.04-1_amd64.tar.zst
Confirm available storage on local-lvm (need at least 10 GB free):
pvesm status
Verify Docker Engine is installed and running:
docker --version systemctl is-active docker
Confirm at least two sample containers exist (nginx + hello-world or equivalent):
docker ps -a
If no containers exist, create quick samples:
docker run -d --name nginx-lab --restart unless-stopped -p 80:80 nginx docker run -d --name whoami-lab --restart unless-stopped -p 8080:8080 containous/whoami
Note the OS and firewall tool in use (UFW or firewalld) — you will need it during agent deployment:
uname -a systemctl is-active ufw 2>/dev/null || systemctl is-active firewalld 2>/dev/null || echo "No firewall detected"
Open GNS3 and confirm the docker-portainer-network-probe project is available. Start the project and start both the Cloud node and the Alpine node.
Open a console on alpine-probe-01 and verify its IP address and default route:
ip addr show eth0 ip route show
Expected: 192.168.128.200/24 and default route via 192.168.128.1.
Run a quick reachability test to the lab gateway and Docker host:
ping -c 3 192.168.128.1 ping -c 3 192.168.128.92
Both should reply before proceeding.
Follow the four phases below in order. Each phase builds on the last — do not skip ahead.
You can use either the automated provisioning script (recommended) or the manual steps below.
Copy the script setup-docker-portainer-lxc.sh to the Proxmox host root home directory:
scp setup-docker-portainer-lxc.sh root@192.168.128.191:/root/
SSH to the Proxmox host and execute the script:
ssh root@192.168.128.191 chmod +x /root/setup-docker-portainer-lxc.sh /root/setup-docker-portainer-lxc.sh
The script creates CT 200 (portainer-lxc), enables nesting, and starts the container. Review the output summary for IP and credentials.
In the Proxmox web UI, click your node → Create CT. Set the following values:
| Field | Value |
|---|---|
| CT ID | 200 |
| Hostname | portainer-lxc |
| Template | ubuntu-22.04-standard_22.04-1_amd64.tar.zst |
| Disk Size | 10 GB (local-lvm) |
| CPU Cores | 2 |
| RAM | 2048 MB |
| Network Bridge | vmbr0 |
| IP | 192.168.129.11/24 |
| Gateway | 192.168.128.1 |
| Unprivileged | No (must be privileged for Docker) |
After creating the container, select it in the tree, go to Options → Features and enable Nesting.
Start the container and open a shell:
pct start 200 pct enter 200
All commands below run inside the LXC container shell (pct enter 200 from the Proxmox host, or SSH to 192.168.129.11).
Update the package index and install prerequisites:
apt-get update -y apt-get install -y ca-certificates curl gnupg lsb-release
Add Docker's official GPG key and repository:
install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ | gpg --dearmor -o /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" \ | tee /etc/apt/sources.list.d/docker.list > /dev/null
Install Docker Engine, CLI, containerd, and the Compose plugin:
apt-get update -y apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Enable Docker to start on boot and start it now:
systemctl enable docker systemctl start docker
Verify Docker is running and test with hello-world:
systemctl is-active docker docker run --rm hello-world
You should see the "Hello from Docker!" message. If you get a permission or cgroup error, double-check that nesting is enabled on CT 200.
Still inside the LXC shell. Portainer CE will run as a Docker container and persist its data in a named volume.
Create the named Docker volume for Portainer persistent data:
docker volume create portainer_data
Verify the volume was created:
docker volume ls | grep portainer_data
Pull the latest Portainer CE image:
docker pull portainer/portainer-ce:latest
Deploy the Portainer CE container. This single command exposes HTTP (9000) and HTTPS (9443) and mounts the Docker socket so Portainer can manage the local environment:
docker run -d \ --name portainer \ --restart always \ -p 8000:8000 \ -p 9443:9443 \ -p 9000:9000 \ -v /var/run/docker.sock:/var/run/docker.sock \ -v portainer_data:/data \ portainer/portainer-ce:latest
Key flags explained:
Verify Portainer is running:
docker ps --filter name=portainer
Expected: STATUS shows Up X seconds or minutes.
Open a browser on your workstation and navigate to:
https://192.168.129.11:9443
Accept the self-signed certificate warning. The Portainer initial setup screen should appear.
Create your admin account. Choose a strong password (minimum 12 characters). Record the credentials in your lab password vault before clicking Create User.
After logging in, Portainer will prompt you to configure the initial environment. Select Get Started to use the local Docker socket environment (the LXC's own Docker daemon). You will add the remote environment in Phase 4.
SSH to the Docker host at 192.168.128.92. You can use the automated script or run the commands manually.
Copy and run the preflight agent script on docker-host-01:
scp setup-docker-portainer-agent.sh user@192.168.128.92:/tmp/ ssh user@192.168.128.92 "sudo bash /tmp/setup-docker-portainer-agent.sh"
SSH to the Docker host and ensure Docker starts on boot:
ssh user@192.168.128.92 sudo systemctl enable docker
Open TCP/9001 in the host firewall:
# If using UFW: sudo ufw allow 9001/tcp comment 'Portainer Agent' sudo ufw status
# If using firewalld: sudo firewall-cmd --permanent --add-port=9001/tcp sudo firewall-cmd --reload
Deploy the Portainer Agent container. Use the same version tag as your Portainer CE instance:
docker run -d \ --name portainer_agent \ --restart always \ -p 9001:9001 \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /var/lib/docker/volumes:/var/lib/docker/volumes \ portainer/agent:latest
Key flags explained:
Verify the agent is running and port 9001 is listening:
docker ps --filter name=portainer_agent ss -tlnp | grep 9001
Open the console on alpine-probe-01 in GNS3 and run the following connectivity checks:
# Ping both hosts ping -c 3 192.168.129.11 ping -c 3 192.168.128.92 # Test Portainer HTTPS port nc -zv 192.168.129.11 9443 # Test Portainer Agent port on Docker host nc -zv 192.168.128.92 9001
All four tests must succeed before proceeding to register the remote environment.
In the Portainer web UI, navigate to Settings → Environments (or click the Environments link in the left sidebar).
Click Add environment. When prompted for environment type, select Docker Standalone then click Start Wizard.
Select the Agent connection method (not "API" or "Socket"). Fill in the fields:
| Field | Value |
|---|---|
| Name | docker-host-01 |
| Environment URL | tcp://192.168.128.92:9001 |
Click Connect. Portainer will attempt to reach the agent. After a moment, the environment should show status Up.
Click Close to return to the Environments list. You should now see two environments:
Click on docker-host-01 to switch to that environment. Navigate to Containers. You should see the existing containers (nginx-lab, whoami-lab) running on the remote host.
Perform the following lifecycle operations from the Portainer UI to confirm full management capability:
Use this section when something does not work as expected. Issues are grouped by symptom.
systemctl start docker fails or docker run hello-world returns a cgroup or permission error.
Nesting feature not enabled on the LXC container, or the container is running as unprivileged without the required AppArmor profile overrides.
Exit the LXC shell. On the Proxmox host, verify nesting is enabled:
pct config 200 | grep features
Expected output: features: nesting=1
If nesting is not set, apply it and restart the container:
pct set 200 --features nesting=1 pct stop 200 && pct start 200
If running unprivileged, check for AppArmor issues:
journalctl -u docker --no-pager -n 30
Browser cannot reach https://192.168.129.11:9443 — connection refused or timeout.
Verify the Portainer container is running:
docker ps --filter name=portainer docker logs portainer --tail 30
Confirm the port is listening:
ss -tlnp | grep 9443
Check for a host firewall blocking 9443 on the LXC:
ufw status iptables -L -n | grep 9443
If UFW is active and blocking: ufw allow 9443/tcp
If the Portainer container exited, inspect why and restart:
docker inspect portainer | grep -A5 '"Status"' docker start portainer
Navigating to the Portainer URL shows the login screen instead of the initial setup wizard — the 5-minute setup window expired.
# Restart the Portainer container to reset the timer docker restart portainer # Then immediately open the browser to: # https://192.168.129.11:9443
Complete account creation within 5 minutes of the container starting.
After adding docker-host-01 as an environment, the status shows Down or the connection wizard returns an error.
Confirm the Portainer Agent container is running on docker-host-01:
docker ps --filter name=portainer_agent docker logs portainer_agent --tail 20
Test port 9001 reachability from inside the LXC (Portainer's perspective):
nc -zv 192.168.128.92 9001
If this fails, the issue is network or firewall — not Portainer configuration.
Check the firewall on docker-host-01:
sudo ufw status | grep 9001 sudo ss -tlnp | grep 9001
Add the rule if missing: sudo ufw allow 9001/tcp
Verify the Environment URL in Portainer is exactly tcp://192.168.128.92:9001 (include the tcp:// prefix).
Check for version mismatch between Portainer CE and the Agent:
# On portainer-lxc: docker inspect portainer | grep -i '"Image"' | head -1 # On docker-host-01: docker inspect portainer_agent | grep -i '"Image"' | head -1
Both should reference the same version tag. Update the agent image to match if they differ.
Cannot ping 192.168.129.11 from other hosts on the lab network.
Verify the container's network configuration from the Proxmox host:
pct config 200 | grep net
Should show bridge=vmbr0,ip=192.168.129.11/24,gw=192.168.128.1
Inside the LXC, verify the interface is up and has the correct IP:
ip addr show eth0 ip route show ping -c 3 192.168.128.1
If the interface is missing or shows a different IP, edit the network config inside the LXC:
cat /etc/netplan/*.yaml # If IP is wrong, edit and apply: netplan apply
Pings from alpine-probe-01 time out to lab hosts.
Verify the GNS3 Cloud node is linked to vmbr0 (not a different bridge). In GNS3, right-click the Cloud node → Configure → check the interface mapping.
Confirm the Alpine node has the correct static IP:
ip addr show eth0 # If missing, set manually: ip addr add 192.168.128.200/24 dev eth0 ip route add default via 192.168.128.1
Verify the GNS3 project is started (all nodes green) and the Cloud → Alpine link is connected (cable icon visible).
Complete every verification step in order to confirm full lab success. Screenshot or record evidence for your lab documentation.
Run from the GNS3 alpine-probe-01 console:
# L3 reachability ping -c 4 192.168.129.11 ping -c 4 192.168.128.92 # Port connectivity nc -zv 192.168.129.11 9443 nc -zv 192.168.129.11 9000 nc -zv 192.168.128.92 9001
Open a browser and navigate to https://192.168.129.11:9443.
Log in with your admin credentials. Confirm you reach the Portainer dashboard without errors.
Navigate to Environments in the Portainer left sidebar.
Confirm both environments are visible:
| Environment Name | Type | Expected Status |
|---|---|---|
| local | Docker Standalone (socket) | Up |
| docker-host-01 | Docker Standalone (agent) | Up |
📸 Take a screenshot of this page for your lab documentation.
Click on the docker-host-01 environment to switch context.
Navigate to Containers in the left sidebar.
Confirm the containers pre-deployed on docker-host-01 (e.g., nginx-lab, whoami-lab) appear in the list with status Running.
Select the nginx-lab container (checkbox) and click Stop.
Confirm status changes to Exited.
Select it again and click Start. Confirm status returns to Running.
Click the container name to open the detail view. Click the Logs tab. Confirm log output appears.
Confirm the portainer_data volume exists and Portainer configuration survives a container restart:
# Verify volume exists docker volume inspect portainer_data # Restart Portainer and confirm settings persist docker restart portainer sleep 10
After restart, log into the Portainer UI again — your environments and admin account should still be present (not reset).
systemctl is-enabled docker
systemctl is-enabled docker
| # | Check | Expected Result |
|---|---|---|
| 1 | Ping 192.168.129.11 from Alpine probe | Replies |
| 2 | Ping 192.168.128.92 from Alpine probe | Replies |
| 3 | nc -zv 192.168.129.11 9443 | Succeeded |
| 4 | nc -zv 192.168.128.92 9001 | Succeeded |
| 5 | Portainer UI loads at https://192.168.129.11:9443 | Login page / dashboard |
| 6 | Admin login succeeds | Dashboard visible |
| 7 | Environments page shows "local" as Up | Green Up badge |
| 8 | Environments page shows "docker-host-01" as Up | Green Up badge |
| 9 | Remote containers visible in docker-host-01 environment | nginx-lab, whoami-lab listed |
| 10 | Stop/Start container from UI | State changes correctly |
| 11 | View container logs from UI | Log output visible |
| 12 | portainer_data volume exists | docker volume inspect succeeds |
| 13 | Docker enabled on portainer-lxc | systemctl: enabled |
| 14 | Docker enabled on docker-host-01 | systemctl: enabled |
Test your understanding of the concepts covered in this lab. Select the best answer for each question and click Check Answer.
1. Why must the nesting=1 feature be enabled on a Proxmox LXC container before installing Docker Engine inside it?
2. What is the purpose of mounting /var/run/docker.sock into the Portainer CE container?
3. Which Portainer connection method is used in this lab to manage the remote Docker host at 192.168.128.92, and on which TCP port does it communicate?
4. What is the primary benefit of using a named Docker volume (portainer_data) rather than a bind mount for Portainer's /data directory?
5. In this lab, what is the role of the GNS3 Alpine Linux probe node (192.168.128.200)?
6. You have successfully deployed Portainer CE and added the remote Docker host. A junior team member notices that Portainer shows environment status "Down" for docker-host-01 after the environment was working earlier. Which of the following is the MOST likely cause and first check?
7. Which docker run flag ensures the Portainer CE container automatically restarts if the LXC is rebooted or Docker daemon is restarted?
8. Portainer has a 5-minute timeout window during initial setup. What happens if this window expires before you create the admin account, and how do you recover?