Managing a VPS for a Production Website: Users, Backups, and Maintenance Routines

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
UserRoleAccess
rootBootstrap only — initial setup, kernel-level changesSSH key, no password
deployOwns the application, runs deployssudo, SSH key
backupRead-only access to data for backup jobsrsync/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 -
TaskCadenceWhy
Database dumpDailyPoint-in-time recovery for the app data
File + DB snapshotDailyRestic deduplication keeps storage small
Offsite copyEvery backupSurvives full server loss
Restore drillMonthlyProves 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

FrequencyChecks and actions
DailyCheck backup log tail, df -h, uptime, systemctl --failed
Weeklyapt update && apt upgrade, review /var/log/auth.log for failed logins, verify TLS certificate expiry
MonthlyRestore 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