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.

Note: Always run 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

Critical: Always allow SSH before enabling UFW or you will lock yourself out of a remote machine.
# 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
Tip: Add your own LAN subnet to 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
Debian users: Replace 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
First launch: Portainer will ask you to create an admin account. Do this promptly — the setup window closes after a few minutes and you will need to restart the container to get it back.

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 compose plugin 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.