A quick run through of setting up a simple VPS for hosting sites.
Find a file
2026-08-19 19:57:35 +09:30
README.md First commit 2026-08-19 19:57:35 +09:30

VPS App Hosting

It's a basic setup running docker compose for each application, as well as Caddy which handles routing and TLS certs.

NOTE: The hardening and setup is designed for Debian.

Hardening

# Update things & reboot if you got kernal updates.
apt update && apt full-upgrade -y

# Create non-root user.
adduser deploy
usermod -aG sudo deploy

# Give user your ssh key. If you logged in with a key:
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
# If you logged in with a password:
ssh-copy-id deploy@YOUR_SERVER_IP

# Test user in new shell (keeping current one open).
ssh deploy@YOUR_SERVER_IP
sudo whoami

# Hardening ssh.
sudo tee /etc/ssh/sshd_config.d/99-hardening.conf > /dev/null <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
EOF

sudo systemctl reload ssh

# Test another shell login.

# Firewall.
sudo apt install -y ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

# Fail2ban
sudo apt install -y fail2ban

# Automatic patch updates.
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

Setup

# Clear any existing docker stuff.
for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do
  sudo apt remove -y $pkg
done

# Add Docker GPG keys and repo.
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

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

# Install Docker Engine and Compose
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# Run Docker as deploy user.
sudo usermod -aG docker $USER

# Verify.
docker run hello-world
docker compose version
# Then kill the hello-word container and image.

Structure

├── apps
│   ├── caddy
│   │   ├── caddy
│   │   │   └── Caddyfile
│   │   └── docker-compose.yml
│   ├── test
│   │   ├── docker-compose.yml
│   │   ├── Dockerfile
│   │   ├── go.mod
│   │   ├── go.sum
│   │   ├── hello.templ
│   │   └── main.go
│   └── whoami
│       └── docker-compose.yml
└── README.md

Adding Apps

To add a new app:

  • Create the app subdirectory (or from a git clone ...).
  • Update ~apps/caddy/caddy/Caddyfile with the new domain or subdomain and port.
  • Add the DNS record for the domain/subdomain to point to server IP.
  • Check domain progress with dig +short sub.domain.tld expecting to see the IP.
  • Build and start the new app container docker compose up -d
  • Run cd ~/apps/caddy && docker compose up -d --force-recreate.

Or once the app code is there and DNS is in place:

cd ~/apps/new-app && docker compose up -d
docker compose -f ~/apps/caddy/docker-compose.yml exec caddy caddy reload --config /etc/caddy/Caddyfile
docker logs new-app -f --tail 30

Update App

Run docker compose pull && docker compose up -d to pull the latest image versions (if :latest tags are used).

Git (Forgejo)

Because I use git over ssh port 2222 is required, so it cannot run through Caddy. Instead I ran the following to expose it through the firewall.

sudo ufw allow 2222/tcp comment 'Forgejo git ssh'