Table of Contents

1. Why Self-Host? 2. Choosing a VPS Provider 3. Initial Server Setup 4. SSH Keys & Secure Access 5. Firewall & Security Hardening 6. Domains & DNS 7. Reverse Proxy (Caddy & Nginx) 8. SSL/TLS with Let's Encrypt 9. Docker on Your VPS 10. Coolify -- Your Own PaaS 11. Other Self-Hosting Tools 12. Monitoring & Logs 13. Backups 14. Full Example: Deploy a Next.js App 15. Command Cheat Sheet

1. Why Self-Host?

Every time you deploy to Vercel, Netlify, or Railway, you're using someone else's server. Self-hosting means running your own server and controlling the entire stack. Here's why that matters:

Managed Platforms Self-Hosting
Easy to start, expensive to scale Small learning curve, cheap forever
Vendor lock-in (proprietary configs) Standard Linux -- works anywhere
Limited control over infra Full root access, install anything
Bandwidth/build limits on free tiers No artificial limits
$20+/month for Pro features $4-6/month for a full VPS
When to Self-Host vs Use Managed

Self-host when you want to learn infrastructure, save money at scale, host multiple projects on one server, or need full control. Use managed platforms for quick prototypes, when you have zero time for ops, or when your team has no Linux experience and can't afford downtime.

Self-hosting teaches you Linux, networking, Docker, DNS, security, and deployment -- skills that make you a significantly more capable developer and a stronger hire.

2. Choosing a VPS Provider

A VPS (Virtual Private Server) is a virtual machine running on shared hardware. You get root access to your own isolated Linux environment. Here are the best options:

Provider Cheapest Plan Strengths Best For
Hetzner ~$4/mo (2 vCPU, 4GB RAM) Best price-to-performance ratio, EU data centers, great ARM options Most self-hosters, best value
DigitalOcean $6/mo (1 vCPU, 1GB RAM) Excellent docs, simple UI, large community Beginners, good tutorials
Linode (Akamai) $5/mo (1 vCPU, 1GB RAM) Reliable, good support, competitive pricing General purpose
Vultr $6/mo (1 vCPU, 1GB RAM) Many locations, bare metal options Global coverage
Oracle Cloud Free tier (4 ARM cores, 24GB RAM) Insanely generous free tier Learning/experimenting (unreliable availability)
Recommendation

Start with Hetzner. Their CX22 (2 vCPU, 4GB RAM, 40GB SSD) at ~$4/month is unbeatable. Choose Ubuntu 24.04 LTS as your OS. Pick the closest data center to your users (Falkenstein or Helsinki for EU, Ashburn for US East).

3. Initial Server Setup

You just created a VPS. Here's the step-by-step to go from a fresh Ubuntu install to a secure, ready-to-deploy server.

Step 1: Log In as Root

Your provider gives you a root password or lets you add an SSH key during creation. Connect for the first time:

# Replace with your server's IP address
ssh root@YOUR_SERVER_IP

Step 2: Update the System

apt update && apt upgrade -y

Step 3: Create a Non-Root User

Never run everything as root. Create a regular user with sudo privileges:

# Create user
adduser deploy

# Add to sudo group
usermod -aG sudo deploy

Step 4: Set the Hostname

hostnamectl set-hostname myserver

Step 5: Set the Timezone

timedatectl set-timezone UTC
Why UTC?

Always set servers to UTC. Your logs, cron jobs, and timestamps stay consistent regardless of where you or your users are. Convert to local time in your application layer.

Step 6: Install Essential Packages

apt install -y curl wget git unzip htop ufw fail2ban

4. SSH Keys & Secure Access

Passwords are weak. SSH keys are cryptographic key pairs -- a private key (stays on your machine) and a public key (goes on the server). The server can verify you hold the private key without it ever crossing the network.

Generate an SSH Key (On Your Local Machine)

# Generate Ed25519 key (recommended over RSA)
ssh-keygen -t ed25519 -C "your@email.com"

# This creates:
# ~/.ssh/id_ed25519       (private key -- NEVER share this)
# ~/.ssh/id_ed25519.pub   (public key -- goes on server)

Copy the Public Key to Your Server

# Method 1: ssh-copy-id (easiest)
ssh-copy-id deploy@YOUR_SERVER_IP

# Method 2: Manual
cat ~/.ssh/id_ed25519.pub | ssh root@YOUR_SERVER_IP "mkdir -p /home/deploy/.ssh && cat >> /home/deploy/.ssh/authorized_keys"

Lock Down SSH (On the Server)

Edit the SSH config to disable password login and root login:

sudo nano /etc/ssh/sshd_config

Find and change these lines:

# Disable password authentication
PasswordAuthentication no

# Disable root login
PermitRootLogin no

# Optional: change default port (makes automated scanners miss you)
# Port 2222

Restart SSH:

sudo systemctl restart sshd
Don't Lock Yourself Out

Before disabling password auth, open a second terminal and verify you can log in with your SSH key as the deploy user. If you disable passwords and your key doesn't work, you'll be locked out permanently.

SSH Config File (On Your Local Machine)

Save connection details so you can type ssh myserver instead of the full command:

# ~/.ssh/config
Host myserver
    HostName YOUR_SERVER_IP
    User deploy
    IdentityFile ~/.ssh/id_ed25519
    # Port 2222  (if you changed the port)

Now just run:

ssh myserver

5. Firewall & Security Hardening

UFW (Uncomplicated Firewall)

UFW is a frontend for iptables that makes firewall rules human-readable:

# Allow SSH (do this FIRST or you'll lock yourself out)
sudo ufw allow OpenSSH

# Allow HTTP and HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Enable the firewall
sudo ufw enable

# Check status
sudo ufw status verbose
UFW Status Output
Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
80/tcp                     ALLOW       Anywhere
443/tcp                    ALLOW       Anywhere

Fail2Ban

Fail2Ban watches log files and bans IPs that show malicious signs (like brute-force SSH attempts):

# It's already installed from our earlier step
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

# Check banned IPs
sudo fail2ban-client status sshd

Automatic Security Updates

sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
Security Checklist

1. SSH key auth only (no passwords). 2. Non-root user with sudo. 3. UFW firewall enabled. 4. Fail2Ban running. 5. Auto security updates. 6. Keep software updated. This covers 95% of threats for a personal VPS.

6. Domains & DNS

DNS (Domain Name System) translates human-readable domain names to IP addresses. To point your domain at your VPS, you need to create DNS records.

Where to Buy Domains

Registrar Why
Cloudflare Registrar At-cost pricing (no markup), free DNS, free proxy/CDN
Namecheap Good prices, easy UI, free WhoisGuard
Porkbun Cheapest for many TLDs, clean interface

Essential DNS Records

Record Type Name Value What It Does
A @ YOUR_SERVER_IP Points yourdomain.com to your server
A * YOUR_SERVER_IP Wildcard -- points *.yourdomain.com to your server
CNAME www yourdomain.com Points www.yourdomain.com to root domain
Example: Cloudflare DNS Setup
Type    Name    Content           Proxy    TTL
A       @       203.0.113.42      DNS only Auto
A       *       203.0.113.42      DNS only Auto
CNAME   www     yourdomain.com    DNS only Auto

Set proxy to "DNS only" (grey cloud) if you're handling SSL yourself with Caddy or Let's Encrypt. Use "Proxied" (orange cloud) if you want Cloudflare's CDN and DDoS protection.

Verify DNS Propagation

# Check if your domain resolves to your IP
dig yourdomain.com +short

# Or use nslookup
nslookup yourdomain.com
DNS Propagation

DNS changes can take 5 minutes to 48 hours to propagate worldwide, but usually it's under 30 minutes. If you're using Cloudflare DNS, changes are nearly instant.

7. Reverse Proxy (Caddy & Nginx)

A reverse proxy sits in front of your applications and routes incoming requests to the right service. It handles SSL, load balancing, and lets you run multiple apps on one server.

How It Works
Internet
   │
   ▼
┌──────────────────┐
│   Reverse Proxy   │  ← Listens on ports 80 & 443
│   (Caddy/Nginx)   │
└──────┬───────────┘
       │
   ┌───┼────────────────┐
   ▼   ▼                ▼
:3000  :8080           :5000
App 1  App 2           App 3
(Next) (API)           (Flask)

app1.yourdomain.com → :3000
api.yourdomain.com  → :8080
app3.yourdomain.com → :5000

Option A: Caddy (Recommended)

Caddy is a modern web server that automatically handles SSL via Let's Encrypt. Zero config for HTTPS. This is the easiest option.

Install Caddy

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

Caddyfile Configuration

# /etc/caddy/Caddyfile

# App 1 -- Next.js running on port 3000
app1.yourdomain.com {
    reverse_proxy localhost:3000
}

# App 2 -- API running on port 8080
api.yourdomain.com {
    reverse_proxy localhost:8080
}

# Static site
yourdomain.com {
    root * /var/www/mysite
    file_server
}

That's it. Caddy automatically provisions SSL certificates. Reload with:

sudo systemctl reload caddy

Option B: Nginx

Nginx is the industry standard. More config required, but maximum flexibility.

Install Nginx

sudo apt install -y nginx

Nginx Site Configuration

# /etc/nginx/sites-available/app1
server {
    listen 80;
    server_name app1.yourdomain.com;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}
# Enable the site
sudo ln -s /etc/nginx/sites-available/app1 /etc/nginx/sites-enabled/
sudo nginx -t          # Test config
sudo systemctl reload nginx
Caddy vs Nginx

Use Caddy for personal projects and when you want zero SSL hassle. Use Nginx when you need advanced features (rate limiting, caching, complex rewrites) or when you're in a team that already uses it. Both are production-ready.

8. SSL/TLS with Let's Encrypt

SSL/TLS encrypts traffic between your users and your server. Let's Encrypt provides free, auto-renewing SSL certificates.

If You're Using Caddy

Skip this section. Caddy handles SSL automatically -- it provisions and renews Let's Encrypt certificates for every domain in your Caddyfile with zero configuration.

Certbot (For Nginx)

# Install Certbot
sudo apt install -y certbot python3-certbot-nginx

# Get a certificate (Certbot auto-configures Nginx)
sudo certbot --nginx -d app1.yourdomain.com -d api.yourdomain.com

# Test auto-renewal
sudo certbot renew --dry-run

Certbot adds a cron job that automatically renews certificates before they expire (every 90 days).

Verify SSL

# Check certificate details
curl -vI https://app1.yourdomain.com 2>&1 | grep -A5 "Server certificate"

# Or use openssl
openssl s_client -connect app1.yourdomain.com:443 -servername app1.yourdomain.com

9. Docker on Your VPS

Docker is the standard way to deploy applications on a VPS. Instead of installing dependencies directly on the server, you package everything into containers. Check the Docker page for fundamentals -- this section covers VPS-specific setup.

Install Docker

# Install Docker using the official convenience script
curl -fsSL https://get.docker.com | sh

# Add your user to the docker group (no sudo needed for docker commands)
sudo usermod -aG docker deploy

# Log out and back in for group changes to take effect
exit
ssh myserver

# Verify
docker --version
docker compose version

Docker Compose on a VPS

A typical production setup with Docker Compose:

# docker-compose.yml
services:
  app:
    build: .
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://user:pass@db:5432/myapp
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=myapp

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:
# Deploy
docker compose up -d

# View logs
docker compose logs -f app

# Update and redeploy
git pull
docker compose up -d --build
Environment Variables in Production

Never hardcode secrets in docker-compose.yml. Use a .env file (and add it to .gitignore) or Docker secrets. The example above uses inline values for clarity only.

10. Coolify -- Your Own PaaS

Coolify is an open-source, self-hosted alternative to Vercel, Netlify, and Heroku. It gives you a web UI to deploy applications, databases, and services with automatic SSL, GitHub integration, and zero-downtime deployments -- all running on your own VPS.

What Coolify Replaces
Vercel / Netlify    → Frontend deployments with preview URLs
Heroku / Railway    → Backend app hosting with env vars
PlanetScale / Neon  → Managed databases (Postgres, MySQL, Redis)
GitHub Actions      → Build & deploy pipelines

All of this in ONE tool, on YOUR server, for $0/month in software costs.

Install Coolify

Coolify has a one-line installer. Run this on a fresh VPS (or one where ports 80/443 aren't already in use):

curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash

After installation, access the Coolify dashboard at http://YOUR_SERVER_IP:8000. Set up your admin account on first visit.

What Coolify Can Deploy

Type Examples
Apps from Git Next.js, Nuxt, SvelteKit, Express, Django, Rails, Go, Rust -- anything with a Dockerfile or Nixpacks
Docker Compose Paste your docker-compose.yml and Coolify manages it
Databases PostgreSQL, MySQL, MariaDB, MongoDB, Redis, Dragonfly
One-Click Services Plausible Analytics, Umami, Ghost, WordPress, Gitea, Minio, n8n, Uptime Kuma

Deploy a Next.js App with Coolify

1
Connect GitHub
Settings → Sources → Add GitHub App. Authorize Coolify to access your repos.
2
Add New Resource
Projects → Add Resource → Public/Private Repository. Select your Next.js repo.
3
Configure
Set build pack to Nixpacks (auto-detects Next.js), add environment variables, set the domain.
4
Deploy
Click Deploy. Coolify builds your app, provisions SSL, and starts the container. Future pushes auto-deploy.

Coolify Features

Coolify System Requirements

Minimum: 2 CPU cores, 2GB RAM. Recommended: 2+ CPU cores, 4GB+ RAM. Coolify itself uses Docker, Traefik (as its built-in reverse proxy), and a SQLite/PostgreSQL database. A Hetzner CX22 ($4/mo) is perfect for this.

11. Other Self-Hosting Tools

Deployment & Management

Tool What It Does When to Use
Coolify Full PaaS -- deploy apps, databases, services from Git You want a Vercel-like experience on your own server
Dokku Mini Heroku -- Git push to deploy with buildpacks You love Heroku's git push workflow
CapRover PaaS with web UI, one-click apps, and cluster support Alternative to Coolify with Docker Swarm support
Portainer Docker management UI -- see containers, logs, networks You want a visual way to manage Docker

Monitoring & Analytics

Tool What It Does
Uptime Kuma Self-hosted uptime monitoring with notifications (Slack, Discord, email)
Plausible Analytics Privacy-friendly Google Analytics alternative
Umami Simple, fast, privacy-focused web analytics
Grafana + Prometheus Full metrics collection and dashboard visualization

Useful Self-Hosted Services

Tool What It Does
Gitea / Forgejo Self-hosted Git (lightweight GitHub alternative)
Minio S3-compatible object storage
Vaultwarden Self-hosted Bitwarden password manager
Traefik Cloud-native reverse proxy with auto-discovery for Docker containers
n8n Self-hosted workflow automation (Zapier alternative)
Ghost Self-hosted blog/newsletter platform
One Server, Many Services

With Docker and a reverse proxy, you can run 10+ services on a single $4/month VPS. Each gets its own subdomain (e.g., git.yourdomain.com, analytics.yourdomain.com, status.yourdomain.com) and automatic SSL.

12. Monitoring & Logs

Basic Server Monitoring

# Check disk usage
df -h

# Check memory usage
free -h

# Check CPU and processes
htop

# Check who's logged in
who

# Check system uptime
uptime

Docker Logs

# View logs for a specific container
docker logs -f --tail 100 container_name

# View logs for all services in a compose stack
docker compose logs -f

# View logs with timestamps
docker compose logs -f -t

Uptime Kuma (Recommended)

Deploy with a single Docker command:

docker run -d \
  --name uptime-kuma \
  --restart unless-stopped \
  -p 3001:3001 \
  -v uptime-kuma:/app/data \
  louislam/uptime-kuma:1

Then point status.yourdomain.com at port 3001 via your reverse proxy. You get a status page and notifications for when your apps go down.

13. Backups

If it's not backed up, it doesn't exist. Here's a practical backup strategy for a VPS.

3-2-1 Backup Rule:

3 copies of your data
2 different storage media (e.g., SSD + external HDD, or local + cloud)
1 offsite copy (not in the same physical location)

Test your backups. A backup you've never restored is not a backup — it's a hope.

What to Back Up

Database Backups

# PostgreSQL dump
docker exec my-postgres pg_dumpall -U postgres > backup_$(date +%Y%m%d).sql

# MySQL dump
docker exec my-mysql mysqldump -u root -p --all-databases > backup_$(date +%Y%m%d).sql

Automated Backup Script

#!/bin/bash
# /home/deploy/backup.sh

BACKUP_DIR="/home/deploy/backups"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"

# Dump PostgreSQL
docker exec postgres pg_dumpall -U postgres > "$BACKUP_DIR/db_$DATE.sql"

# Compress
gzip "$BACKUP_DIR/db_$DATE.sql"

# Delete backups older than 7 days
find "$BACKUP_DIR" -name "*.gz" -mtime +7 -delete

echo "Backup complete: db_$DATE.sql.gz"
# Make it executable
chmod +x /home/deploy/backup.sh

# Add to crontab (runs daily at 3 AM)
crontab -e
# Add this line:
0 3 * * * /home/deploy/backup.sh >> /home/deploy/backups/backup.log 2>&1

Off-Site Backups

Local backups aren't enough -- if the VPS dies, so do your backups. Send them off-site:

# Sync to another server
rsync -avz /home/deploy/backups/ backup-user@other-server:/backups/

# Or upload to S3-compatible storage (Backblaze B2, Cloudflare R2, Minio)
# Using rclone:
rclone sync /home/deploy/backups/ remote:my-backups/
Coolify Backups

If you're using Coolify, it has built-in scheduled database backups to S3-compatible storage. Configure it in the Coolify dashboard under each database's settings -- no scripts needed.

14. Full Example: Deploy a Next.js App

Let's put it all together. You have a Next.js app on GitHub and a fresh VPS. Here's the manual approach (without Coolify).

Step-by-Step

# 1. SSH into your server
ssh myserver

# 2. Install Docker (if not already)
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker deploy
exit && ssh myserver

# 3. Install Caddy
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install caddy

# 4. Clone your repo
cd /home/deploy
git clone https://github.com/yourusername/my-nextjs-app.git
cd my-nextjs-app

# 5. Create a Dockerfile (if you don't have one)
cat << 'DOCKERFILE' > Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
DOCKERFILE

# 6. Create docker-compose.yml
cat << 'COMPOSE' > docker-compose.yml
services:
  app:
    build: .
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
COMPOSE

# 7. Build and start
docker compose up -d --build

# 8. Configure Caddy
sudo tee /etc/caddy/Caddyfile << 'CADDY'
myapp.yourdomain.com {
    reverse_proxy localhost:3000
}
CADDY

sudo systemctl reload caddy

Your app is now live at https://myapp.yourdomain.com with automatic SSL.

Simple Redeployment

# Create a deploy script
cat << 'DEPLOY' > /home/deploy/my-nextjs-app/deploy.sh
#!/bin/bash
cd /home/deploy/my-nextjs-app
git pull
docker compose up -d --build
echo "Deployed at $(date)"
DEPLOY

chmod +x /home/deploy/my-nextjs-app/deploy.sh

# Redeploy anytime:
./deploy.sh
Or Just Use Coolify

The manual approach above is great for learning. But for day-to-day use, Coolify does all of this automatically -- connect your repo, set a domain, and push to deploy. No scripts, no manual Docker builds.

15. Command Cheat Sheet

Server Management

CommandWhat It Does
ssh myserverConnect to your server
sudo apt update && sudo apt upgrade -yUpdate all packages
df -hCheck disk space
free -hCheck memory usage
htopInteractive process viewer
sudo rebootRestart the server
uptimeCheck how long server has been running

Firewall (UFW)

CommandWhat It Does
sudo ufw statusShow firewall rules
sudo ufw allow 80/tcpOpen a port
sudo ufw deny 8080/tcpBlock a port
sudo ufw delete allow 8080/tcpRemove a rule

Docker (VPS-Specific)

CommandWhat It Does
docker compose up -dStart all services in background
docker compose up -d --buildRebuild and restart
docker compose logs -fFollow logs for all services
docker compose downStop all services
docker system prune -afClean up unused images/containers
docker statsLive resource usage per container

Caddy

CommandWhat It Does
sudo systemctl reload caddyReload config without downtime
sudo systemctl restart caddyFull restart
sudo caddy validate --config /etc/caddy/CaddyfileValidate config syntax
journalctl -u caddy -fView Caddy logs

SSL & DNS

CommandWhat It Does
dig yourdomain.com +shortCheck DNS resolution
sudo certbot --nginx -d domain.comGet SSL cert for Nginx
sudo certbot renew --dry-runTest certificate renewal
curl -I https://yourdomain.comCheck SSL headers
The $4/Month Stack

Here's what you can run on a single Hetzner CX22 ($4/month):

Coolify          → Deploy & manage everything
Next.js App      → Your main web app
PostgreSQL       → Database
Redis            → Caching
Uptime Kuma      → Monitoring
Plausible        → Analytics
Vaultwarden      → Password manager

Total cost: ~$4/month + ~$10/year for a domain
vs. Vercel Pro + PlanetScale + Railway = $60+/month