I set up my own VPS, documented every step, and ended up with a repeatable deployment pipeline. This is both a checklist for my future self and a guide for anyone curious about self-hosting. Along the way I'll explain why I picked Hetzner and Coolify, and how they compare with other options like DigitalOcean, AWS, Render, or Fly.io.
This comprehensive checklist covers every essential step for setting up a secure, production-ready VPS. Each section includes commands, verification steps, and troubleshooting tips based on real-world experience.
Pre-Setup Checklist
Before You Begin:
- Choose your VPS provider (Hetzner recommended for price/performance)
- Select server specifications (minimum 1GB RAM, 20GB storage)
- Note down server IP address and root credentials
- Prepare your local machine with SSH client
- Have a strong password generator ready
Picking the VPS provider
- Chose Hetzner Cloud (cheap, fast, reliable in Europe)
- Alternatives I considered:
- DigitalOcean → smoother onboarding, great docs, slightly more expensive
- AWS Lightsail → decent for small apps, but tied to AWS ecosystem (complex for beginners)
- Linode → reliable, but Hetzner wins on price/performance
- Render/Fly.io → easier PaaS, but more opinionated and costly at scale
Why Hetzner?
- 2–3x cheaper for the same specs compared to DO/AWS
- Strong European datacenter presence (latency advantage for my use case)
- Transparent pricing and no surprise bills
Initial Server Setup Checklist
First Login and System Updates
- Initial login as root
ssh root@your-server-ip- Update package lists and upgrade system
apt update && apt upgrade -y- Verify system information
uname -a
cat /etc/os-releaseRoot Account Security
- Change root password
passwd- Use strong password with mixed case, numbers, symbols
- Store securely in password manager
- Create secondary user account
adduser your-username- Choose descriptive username (not 'admin' or 'user')
- Set strong password
- Add user to sudo group
usermod -aG sudo your-username- Verify user groups
groups your-username- Should show: `your-username : your-username sudo`
- Test sudo access
su - your-username
sudo whoami- Should return: `root`
SSH Key Authentication Setup
- Generate SSH keys on LOCAL machine (not server)
#### Ed25519 (recommended)
ssh-keygen -t ed25519 -C "your-email@example.com"
##### Or RSA if Ed25519 not supported
ssh-keygen -t rsa -b 4096 -C "your-email@example.com"- Display public key on local machine
cat ~/.ssh/id_ed25519.pub
#### or
cat ~/.ssh/id_rsa.pub- Copy public key to clipboard
- Create .ssh directory on server (as your user, not root)
mkdir -p ~/.ssh
chmod 700 ~/.ssh- Create authorized_keys file
nano ~/.ssh/authorized_keys- Paste your public key
- Save and exit
- Set correct permissions
chmod 600 ~/.ssh/authorized_keys- Test SSH key login (from local machine)
ssh your-username@your-server-ip- Should login without password prompt
Disable Password Authentication
- Edit SSH configuration
sudo nano /etc/ssh/sshd_config- Modify these settings:
PasswordAuthentication no
PubkeyAuthentication yes- Check cloud-init config if exists
sudo nano /etc/ssh/sshd_config.d/50-cloud-init.conf- Set `PasswordAuthentication no` here too if file exists
- Test SSH configuration
sudo sshd -t- Should show no errors
- Restart SSH service
sudo systemctl restart ssh
#### or
sudo service ssh restart- Verify service status
sudo systemctl status ssh- Should show active (running) with green dot
Disable Root Login
- Edit SSH configuration
sudo nano /etc/ssh/sshd_config- Change root login setting
PermitRootLogin no- Restart SSH service
sudo systemctl restart ssh- Test root login is blocked (from another terminal)
ssh root@your-server-ip- Should get "Permission denied"
Firewall Configuration Checklist
UFW (Uncomplicated Firewall) Setup
- Check UFW status
sudo ufw status- Set default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing- Allow SSH before enabling firewall
sudo ufw allow ssh
#### or if you changed SSH port:
sudo ufw allow 2022/tcp- Allow HTTP and HTTPS for web apps
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp- Enable firewall
sudo ufw enable- Type 'y' when prompted
- Verify firewall rules
sudo ufw status verboseAdvanced Firewall Configuration
- Restrict SSH to your IP (optional but recommended)
sudo ufw allow from YOUR_IP_ADDRESS to any port 22
sudo ufw delete allow ssh- Change default SSH port (optional security through obscurity)
sudo nano /etc/ssh/sshd_config- Change `Port 22` to `Port 2022` (or your chosen port)
- Update firewall: `sudo ufw allow 2022/tcp`
- Remove old rule: `sudo ufw delete allow 22/tcp`
- Restart SSH: `sudo systemctl restart ssh`
Automatic Updates Setup Checklist
Unattended Upgrades Configuration
- Install unattended-upgrades
sudo apt install unattended-upgrades apt-listchanges- Enable automatic updates
sudo dpkg-reconfigure unattended-upgrades- Select "Yes" in the dialog
- Configure update settings
sudo nano /etc/apt/apt.conf.d/50unattended-upgrades- Uncomment security updates line
"${distro_id}:${distro_codename}-security";- Configure email notifications (optional)
Unattended-Upgrade::Mail "your-email@example.com";- Enable automatic reboots if needed
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "02:00";- Test configuration
sudo unattended-upgrades --dry-run- Check service status
sudo systemctl status unattended-upgradesProduction Application Deployment Checklist
Node.js Production Setup
- Install Node.js LTS
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs- Verify installation
node --version
npm --version- Install PM2 globally
sudo npm install -g pm2- Upload your application files
scp -r ./your-app your-username@your-server-ip:~/- Install dependencies
cd ~/your-app
npm install --production- Create production build
npm run buildProcess Manager Configuration
- Start application with PM2
NODE_ENV=production pm2 start app.js --name "your-app"- Configure PM2 for clustering (optional)
pm2 start app.js -i max --name "your-app-cluster"- Save PM2 configuration
pm2 save- Enable PM2 startup
pm2 startup
#### Run the command it outputs- Test application restart
pm2 restart all
pm2 statusReverse Proxy Setup (Nginx)
- Install Nginx
sudo apt install nginx- Create site configuration
sudo nano /etc/nginx/sites-available/your-app- Basic Nginx configuration
server {
listen 80;
server_name your-domain.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 site
sudo ln -s /etc/nginx/sites-available/your-app /etc/nginx/sites-enabled/- Test Nginx configuration
sudo nginx -t- Restart Nginx
sudo systemctl restart nginxSSL Certificate Setup Checklist
Let's Encrypt with Certbot
- Install Certbot
sudo apt install certbot python3-certbot-nginx- Obtain SSL certificate
sudo certbot --nginx -d your-domain.com- Test automatic renewal
sudo certbot renew --dry-run- Verify SSL grade
- Visit: https://www.ssllabs.com/ssltest/
- Should get A or A+ rating
Monitoring and Maintenance Checklist
Basic Monitoring Setup
- Install monitoring tools
sudo apt install htop iotop netstat-nat- Check system resources
htop
df -h
free -h- Monitor logs
sudo tail -f /var/log/syslog
sudo tail -f /var/log/auth.log- Set up log rotation
sudo nano /etc/logrotate.d/your-appBackup Strategy
- Create backup script
nano ~/backup.sh- Sample backup script
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
tar -czf ~/backups/app_backup_$DATE.tar.gz ~/your-app
#### Add database backup commands if needed- Make script executable
chmod +x ~/backup.sh- Set up automated backups
crontab -e- Add: `0 2 * * * /home/username/backup.sh`
Troubleshooting Checklist
Common Issues and Solutions
SSH Connection Problems:
- Check firewall rules:
sudo ufw status - Verify SSH service:
sudo systemctl status ssh - Check SSH logs:
sudo tail -f /var/log/auth.log - Test from different network
Permission Denied Errors:
- Check file permissions:
ls -la - Verify user groups:
groups username - Check sudo configuration:
sudo -l
Service Not Starting:
- Check service status:
sudo systemctl status service-name - View service logs:
sudo journalctl -u service-name - Check configuration files syntax
High Resource Usage:
- Identify processes:
htop - Check disk usage:
df -h - Monitor network:
netstat -tulpn - Review application logs
Final Verification Checklist
Security Verification
- Test SSH key authentication works
- Verify password authentication is disabled
- Confirm root login is blocked
- Check firewall is active and configured
- Verify automatic updates are working
- Test application runs in production mode
- Confirm SSL certificate is valid
- Verify backups are being created
Performance Testing
- Run basic load test
#### Install Apache Bench
sudo apt install apache2-utils
#### Test with 100 requests, 10 concurrent
ab -n 100 -c 10 http://your-domain.com/- Monitor resource usage during load
htop- Check application logs for errors
pm2 logsQuick Reference Commands
System Information:
htop # System monitor
df -h # Disk usage
free -h # Memory usage
uname -a # System infoProcess Management:
pm2 status # PM2 process status
pm2 restart all # Restart all processes
pm2 logs # View logs
pm2 monit # Real-time monitoringSecurity:
sudo ufw status # Firewall status
sudo fail2ban-client status # Fail2ban status
sudo lynis audit system # Security auditServices:
sudo systemctl status nginx # Service status
sudo systemctl restart nginx # Restart service
sudo journalctl -u nginx # Service logsFinal thoughts
This checklist provides a complete approach to VPS setup and management. This isn’t just about saving money. It’s about control and understanding. By self-hosting with Hetzner + Coolify, I built muscle memory for devops that paid off in confidence and freedom.
If you’ve been meaning to try VPS hosting, consider this a nudge.
