Run these steps on both VMs unless noted otherwise. A script version is available at scripts/bootstrap.sh.
sudo apt update && sudo apt upgrade -y
sudo apt install -y \
curl \
git \
htop \
ufw \
tmux \
unattended-upgradessudo dpkg-reconfigure -plow unattended-upgrades
# Select "Yes" when promptedThis ensures security patches are applied automatically without manual intervention.
UFW runs at the OS level inside the VM, complementing the OCI NSG.
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow SSH (key-based auth only — see step below)
sudo ufw allow 22/tcp
# Allow HTTPS and HTTP (for Caddy ACME challenges)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Enable UFW
sudo ufw enable
# Verify
sudo ufw status verboseUFW's port 22 rule is a fallback. The primary SSH restriction is at the OCI NSG level — the SSH ingress rule should be source-restricted to your IP(s) in the NSG, not open to
0.0.0.0/0.
This step is critical. OCI Ubuntu images ship with default iptables rules that include REJECT rules blocking inbound traffic on ports 80 and 443, even after NSG and UFW allow them.
Check current rules:
sudo iptables -L INPUT -n --line-numbersIf you see REJECT or DROP rules for ports 80/443 (often appearing as a blanket REJECT at the end of INPUT chain), fix them:
# Option A: Flush the INPUT chain (nuclear, use carefully)
sudo iptables -F INPUT
# Option B: Delete specific reject rules by line number
# Find the line number from the output above, then:
sudo iptables -D INPUT <line_number>
# Make the change persistent
sudo apt install -y iptables-persistent
sudo netfilter-persistent saveVerify by testing HTTP from your workstation after Caddy is running:
curl -I http://<VM_PUBLIC_IP>
# Should get a redirect, not a connection refused# Remove any old Docker packages
sudo apt remove -y docker docker-engine docker.io containerd runc 2>/dev/null
# Install dependencies
sudo apt install -y ca-certificates curl gnupg lsb-release
# Add Docker GPG key and repository
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo 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" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker Engine + Compose plugin
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginVerify installation:
docker --version
docker compose versionAvoid running containers as root or ubuntu. Create a dedicated non-root user:
sudo useradd -m -s /bin/bash deploy
sudo usermod -aG docker deploy
# Set a password or configure SSH key for this user
sudo passwd deploy
# OR
sudo mkdir -p /home/deploy/.ssh
sudo cp ~/.ssh/authorized_keys /home/deploy/.ssh/
sudo chown -R deploy:deploy /home/deploy/.ssh
sudo chmod 700 /home/deploy/.sshSwitch to the deploy user for all subsequent operations:
sudo su - deploycd ~
git clone https://github.com/daedalus410/homelab-cloud-stack.git
cd homelab-cloud-stackdocker run --rm hello-worldIf this succeeds, Docker is correctly installed and the deploy user has the necessary permissions.
An automated version of the above is available:
# On each VM, after SSH-ing in:
bash <(curl -fsSL https://raw.githubusercontent.com/daedalus410/homelab-cloud-stack/main/scripts/bootstrap.sh)