Environment

  • Hardware: Raspberry Pi
  • Operating System: Debian GNU/Linux 13 (Trixie)
  • Access Method: SSH (headless setup)
  • Container Runtime: Docker Engine
  • Container Management: Portainer

Note: This lab avoids convenience installers (curl | sh) in favor of signed repositories for better security and reproducibility.

Step 1 — Updates & Cockpit Install

Step 1.1 — Update Package Lists

Purpose: Refresh the system’s package index (similar to refreshing an App Store catalog) so it knows which software updates and new packages are available.

sudo apt update

Step 1.2 — Apply System Upgrades

Purpose: Install available updates (similar to clicking “Update All” in an App Store) to bring the operating system and installed packages up to date with current security and stability fixes.

sudo apt upgrade

Step 1.3 — Install Cockpit

Purpose: Install Cockpit for web-based system management interface.

sudo apt install cockpit

Step 1.4 — Verify Cockpit Access

Purpose: Confirm Cockpit is accessible and functioning properly - log in with your Pi credentials.

In your browser, go to https://YOUR-PI-IP-ADDRESS:9090 Log in using the username and password for your Pi.

Cockpit login page

Step 2: Install Docker & Portainer

Step 2.1 — Remove Conflicting Packages

Purpose: Ensure no legacy container runtimes or conflicting packages are present.

sudo apt-get remove -y docker.io docker-doc docker-compose podman-docker containerd runc

Step 2.2 — Install Prerequisites

Purpose: Enable secure package downloads and cryptographic verification.

sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg

Step 2.3 — Add Docker GPG Key

Purpose: Ensure Docker packages are cryptographically signed and trusted.

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

Step 2.4 — Add Docker Repository

Purpose: Install Docker from Docker's official repository rather than Debian defaults.

echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/debian trixie stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Step 2.5 — Install Docker Engine and Plugins

sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Step 2.6 — Enable and Verify Docker

Purpose

Ensure the Docker service automatically starts on boot and confirm that the Docker engine can successfully download, run, and clean up containers. This validates that Docker is fully operational before deploying any services.

Commands

sudo systemctl enable --now docker
docker run --rm hello-world
docker compose version

Expected Outcome

Docker starts immediately and is configured to persist after system reboots.

The hello-world container is successfully downloaded, executed, and removed, confirming that the Docker daemon is running, image pulls from Docker Hub are functioning correctly, and container lifecycle operations (pull → run → stop → remove) work as expected.

Docker Compose reports a valid installed version, indicating the system is ready to support multi-container applications.

Step 2.7 — Deploy Portainer (Container Management UI)

Purpose

Deploy Portainer to provide a web-based management interface for Docker, making it easier to monitor containers, images, volumes, and networks without relying solely on the command line.

Commands

docker volume create portainer_data

docker run -d \
  -p 9000:9000 \
  -p 9443:9443 \
  --name portainer \
  --restart=always \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v portainer_data:/data \
  portainer/portainer-ce:latest

Expected Outcome

A persistent Docker volume is created to store Portainer configuration data, and the Portainer container starts in detached mode with automatic restart enabled.

The service listens on ports 9000 (HTTP) and 9443 (HTTPS) and is accessible from the local network.

Step 2.8 — Create Portainer Account

Purpose: On first login to Portainer, an admin account will be created; be sure to use a strong non-default username and complex password.

Portainer login page

Step 2.9 — Network & Exposure Validation

Purpose

Verify the host's network configuration and identify any active listening services to understand the system's local exposure and confirm that no services are unintentionally accessible from the internet.

Commands

ip route
sudo ss -tulpn

Expected Outcome

Confirm the system is operating on a local LAN and is not directly reachable from the public internet.

Only expected services, such as Docker and Portainer are listening, indicating that the system's attack surface is minimal at this stage.

Step 3 — Deploy a Hardened Kali Linux Container (Docker)

Purpose

Deployed a hardened Kali Linux container using Docker and Portainer ensuring the container introduces no additional attack surface to the host or local network. This allows offensive tooling to be available for lab exercises while maintaining a defensive security posture. While the Kali container supports an optional browser-based desktop via internal web services, this functionality was intentionally not exposed. The container is only accessible via docker exec and not browser/KasmVNCto avoid publishing ports or introducing additional web-facing services into the lab environment.

Note:

I was unable to create the container directly in Portainer, it would cause my PI to freeze up. So I pre-pulled the Kali image via the Docker CLI and then created the container in Portainer.

Container configuration settings:

  • Image: lscr.io/linuxserver/kali-linux:latest
  • Network mode: bridge (default Docker isolation)
  • Published ports: None
  • Volumes / bind mounts: None
  • Privileged mode: Disabled
  • Restart policy: No
  • Environment variables:
    • PUID=1000
    • PGID=1000
    • TZ=<local timezone>