Home Lab Setup Guide
A minimal, practical reference for hardening a fresh Ubuntu or Debian server and getting a container stack running with Docker CE and Portainer.
This guide assumes a fresh Ubuntu 22.04 / 24.04 LTS or Debian 12 installation with a non-root user that has sudo privileges. Commands are run in a terminal over SSH or directly on the machine. Work through the sections in order — firewall first, then intrusion prevention, then containers.
sudo apt update && sudo apt upgrade -y on a fresh install before proceeding to keep the package index and existing packages current.
01 — UFW — Uncomplicated Firewall
UFW is a front-end for iptables that ships with Ubuntu and is available in the Debian repos. It makes managing ingress and egress rules straightforward without needing to write raw iptables rules.
Install
UFW is usually pre-installed on Ubuntu. On Debian, or if it is missing:
sudo apt update
sudo apt install ufw -y
Set default policies
The safest starting point is to deny all incoming traffic and allow all outgoing traffic. You then punch holes only for the services you actually run.
sudo ufw default deny incoming
sudo ufw default allow outgoing
Allow SSH before enabling
# Allow standard SSH port
sudo ufw allow ssh
# Or explicitly by port number if you use a non-standard port, e.g. 2222
sudo ufw allow 2222/tcp
Common service rules
# HTTP and HTTPS (if running a web server)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Portainer web UI (added later, included here for reference)
sudo ufw allow 9000/tcp
sudo ufw allow 9443/tcp
Enable UFW
sudo ufw enable
Type y when prompted. UFW will now start on boot.
Check status
sudo ufw status verbose
Useful management commands
# List rules with numbers (useful for deletion)
sudo ufw status numbered
# Delete a rule by number
sudo ufw delete 3
# Reload rules after changes
sudo ufw reload
# Disable UFW temporarily
sudo ufw disable
02 — Fail2ban
Fail2ban monitors log files for repeated failed authentication attempts and automatically bans the offending IP addresses using firewall rules. It is an effective layer of defence against brute-force attacks on SSH and other services.
Install
sudo apt update
sudo apt install fail2ban -y
Create a local configuration
Fail2ban ships with /etc/fail2ban/jail.conf but you should never edit this file directly — it will be overwritten on upgrades. Instead, create a local override:
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Configure the SSH jail
Open the local config and locate the [sshd] section, or add the block below. The example below uses sensible defaults for a home lab:
sudo nano /etc/fail2ban/jail.local
Find or add the following under [sshd]:
[sshd]
enabled = true
port = ssh
filter = sshd
backend = systemd
maxretry = 5
findtime = 10m
bantime = 1h
ignoreip = 127.0.0.1/8
ignoreip (e.g. 192.168.1.0/24) so you can never accidentally ban yourself from the local network.
Enable and start Fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
Check status and bans
# Overall service status
sudo systemctl status fail2ban
# Status of the SSH jail specifically
sudo fail2ban-client status sshd
# Unban an IP address
sudo fail2ban-client set sshd unbanip 1.2.3.4
03 — Docker CE
Docker CE (Community Edition) is installed from Docker's official apt repository. The version in the default Ubuntu/Debian repos is often outdated, so always use the upstream source.
Remove old or conflicting packages
for pkg in docker.io docker-doc docker-compose docker-compose-v2 \
podman-docker containerd runc; do
sudo apt-get remove -y $pkg 2>/dev/null
done
Add Docker's official GPG key and repository
# Install prerequisites
sudo apt update
sudo apt install -y ca-certificates curl
# Add Docker's GPG key
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
ubuntu with debian in the GPG URL and the repository line below.
# Add the Docker apt repository (Ubuntu)
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Debian equivalent — replace the URL host:
# https://download.docker.com/linux/debian
Install Docker CE
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
Verify the installation
sudo docker run hello-world
Run Docker without sudo (optional but convenient)
Add your user to the docker group. You will need to log out and back in for the change to take effect.
sudo usermod -aG docker $USER
newgrp docker # activate in current session without logging out
Enable Docker on boot
sudo systemctl enable docker
sudo systemctl enable containerd
Useful Docker commands
# Check Docker version
docker version
# List running containers
docker ps
# List all containers including stopped
docker ps -a
# Pull an image
docker pull nginx
# Remove stopped containers
docker container prune
04 — Portainer
Portainer is a lightweight web UI for managing Docker (and Kubernetes) environments. It runs as a Docker container itself, making installation trivial once Docker CE is in place.
Create the Portainer data volume
Portainer needs a persistent volume to store its database and configuration:
docker volume create portainer_data
Deploy Portainer CE
docker run -d \
--name portainer \
--restart=always \
-p 9000:9000 \
-p 9443:9443 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:latest
Breaking down the flags:
--restart=always— Portainer restarts automatically after a reboot.-p 9000:9000— HTTP access on port 9000.-p 9443:9443— HTTPS access on port 9443 (self-signed cert by default).-v /var/run/docker.sock— Grants Portainer access to the Docker daemon.-v portainer_data:/data— Persists Portainer's own data.
Access the web UI
Open a browser and navigate to:
# Replace with your server's IP address
https://YOUR_SERVER_IP:9443
# HTTP alternative (not recommended for production)
http://YOUR_SERVER_IP:9000
Verify the container is running
docker ps | grep portainer
Updating Portainer
To update Portainer to the latest release:
# Stop and remove the old container (data volume is preserved)
docker stop portainer
docker rm portainer
# Pull the latest image
docker pull portainer/portainer-ce:latest
# Re-run the deploy command from above
docker run -d \
--name portainer \
--restart=always \
-p 9000:9000 \
-p 9443:9443 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:latest
Next Steps
With the basics in place, consider the following additions to your home lab:
- Set up SSH key authentication and disable password login in
/etc/ssh/sshd_config. - Deploy a reverse proxy (Nginx Proxy Manager, Traefik, or Caddy) to route traffic to containers with proper TLS.
- Use Docker Compose (via the
docker composeplugin installed above) to define multi-container stacks. - Set up automatic security updates with
unattended-upgrades. - Configure log rotation and centralise logs with a tool like Loki + Grafana.
- Monitor resource usage with Netdata or the Grafana + Prometheus stack.