Getting a website live on a VPS takes an afternoon. Keeping it running for years takes a maintenance routine: clean user management, automated backups you actually test, log and disk hygiene, and a predictable update cadence. This guide covers the concrete commands and checklists that turn a freshly deployed server into a well-managed production machine.
Start with Clean User Management
Do not run your application as root. Create a dedicated deploy user with sudo rights, install your SSH key, and disable password login for root:
adduser deploy
usermod -aG sudo deploy
mkdir -p /home/deploy/.ssh
echo "ssh-ed25519 AAAA... your-key" >> /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh && chmod 700 /home/deploy/.ssh && chmod 600 /home/deploy/.ssh/authorized_keys
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
systemctl restart ssh
| User | Role | Access |
|---|---|---|
| root | Bootstrap only — initial setup, kernel-level changes | SSH key, no password |
| deploy | Owns the application, runs deploys | sudo, SSH key |
| backup | Read-only access to data for backup jobs | rsync/SSH restricted |
Automate Backups with cron and restic
A backup that has never been restored is a hope, not a backup. Use restic for deduplicated snapshots and drive it from cron, then restore-test monthly. First, initialize the repository and write a small script:
restic -r sftp:[email protected]:/backups init
cat > /usr/local/bin/backup.sh <<'EOF'
#!/bin/bash
set -euo pipefail
mysqldump --all-databases | gzip > /var/backups/db-$(date +%F).sql.gz
restic -r sftp:[email protected]:/backups backup /var/www /var/backups
restic -r sftp:[email protected]:/backups forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
EOF
chmod +x /usr/local/bin/backup.sh
echo "30 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1" | crontab -
| Task | Cadence | Why |
|---|---|---|
| Database dump | Daily | Point-in-time recovery for the app data |
| File + DB snapshot | Daily | Restic deduplication keeps storage small |
| Offsite copy | Every backup | Survives full server loss |
| Restore drill | Monthly | Proves the backups work |
Run Services the Way systemd Expects
Resist the urge to start web servers or workers with nohup ... &. A proper systemd unit gives you automatic restarts, dependency ordering, and a clean way to read logs. A minimal unit for a PHP-FPM or Node application looks like this:
# /etc/systemd/system/app.service
[Unit]
Description=Application server
After=network-online.target mysql.service
Wants=network-online.target
[Service]
User=deploy
Group=deploy
WorkingDirectory=/var/www/app
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=3
LimitNOFILE=65535
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
Restart=on-failure recovers from crashes without you, LimitNOFILE prevents “too many open files” errors under load, and NoNewPrivileges hardens the process. Enable the unit with systemctl enable --now app so it survives reboots, then check health with systemctl status app and logs with journalctl -u app -f.
Security Maintenance That Runs Itself
Two packages remove most routine security work. unattended-upgrades applies security patches to the OS automatically, and fail2ban blocks the SSH brute-force attempts that show up within hours of any server going live:
apt install -y unattended-upgrades fail2ban ufw
dpkg-reconfigure -plow unattended-upgrades
ufw default deny incoming && ufw allow OpenSSH && ufw allow 'Nginx Full' && ufw enable
systemctl enable --now fail2ban
fail2ban-client status sshd
Reboot the server after major kernel upgrades — running an old kernel while the packages are updated is a common gap. The weekly checklist below includes this so it does not get forgotten.
Renew Certificates Before They Expire
An expired TLS certificate takes your site down silently — browsers refuse to connect, and nothing in your logs screams “fix me”. If you use Let’s Encrypt, install the certbot timer and test renewal in dry-run mode:
apt install -y certbot python3-certbot-nginx
certbot --nginx -d example.com -d www.example.com
certbot renew --dry-run
systemctl list-timers | grep certbot
The certbot.timer unit checks twice daily and renews automatically 30 days before expiry. Add a calendar reminder to verify the dry run still passes after major nginx or certbot upgrades — that is the failure mode that catches people out.
Keep Disk and Logs Under Control
A full disk is the most common silent killer of production VPSes. Logrotate handles the classic log files, and journald needs its own cap:
journalctl --vacuum-size=200M
echo "SystemMaxUse=200M" >> /etc/systemd/journald.conf
systemctl restart systemd-journald
df -h /
Add a cron job that alerts you (mail or a webhook) when disk usage passes 85% so you act before the site goes read-only. A one-liner that checks and alerts is enough — the point is that disk fills up slowly and predictably, so a daily check plus an alert gives you weeks of warning instead of a surprise outage.
df -h / | awk 'NR==2 && +$5 > 85 { system("curl -fsS -X POST https://alert.example.com/disk -d msg=disk_full") }'
The Weekly and Monthly Checklist
| Frequency | Checks and actions |
|---|---|
| Daily | Check backup log tail, df -h, uptime, systemctl --failed |
| Weekly | apt update && apt upgrade, review /var/log/auth.log for failed logins, verify TLS certificate expiry |
| Monthly | Restore drill from backups, review open ports (ss -tulpn), audit user list, reboot to apply kernel updates |
Monitoring That Tells You Something
Install fail2ban and unattended-upgrades so the boring security work happens without you. Then add a lightweight uptime check that pings the site from outside — that catches the failures no server-side monitor can see. The management burden is the main reason some teams choose managed hosting options, but a documented routine like this one gets most of the benefit for free on any VPS you control yourself.




Leave a Reply
You must be logged in to post a comment.