Skip to content
← All articles

VPS Initial Setup Checklist: The First 30 Minutes Before You Deploy

Short answer. Before you deploy anything to a fresh VPS, do eight things: verify the resources and virtualization type, create a non-root user with sudo, move your SSH key over and disable password logins, enable a firewall and fail2ban, apply updates and turn on automatic security patching, set hostname, timezone and NTP, and add swap plus file-descriptor limits. The web stack, TLS certificate, backups and monitoring come immediately after — not "later".

Why the first 30 minutes matter more than any later tuning

A freshly provisioned VPS is a machine with a public IP, an open SSH port and, usually, a root password that arrived by email. Scanners find hosts like that within minutes: a typical server starts collecting SSH brute-force attempts before you have even picked a project name. What you do in the first half hour decides whether the box runs for years or becomes someone else's crypto miner in week two.

The second reason is the cost of change. Swapping the distribution, the partition layout, the service account or the backup scheme is trivial on an empty server and painful once production traffic, a database and mail are running on it. The order below follows that logic: irreversible and foundational decisions first, everything else after.

Rule: never deploy a project onto a server that has no firewall, no automatic security updates and no backups yet. A site that is "just a test" gets indexed, scanned and compromised exactly like production.
Timeline of the first thirty minutes of VPS setup: resource check, sudo user, SSH keys, firewall, updates, time sync, swap
Order of operations: irreversible decisions first, application services later

Step 0. Which OS to choose for a server, and why not the newest one

For a web server, pick an LTS branch: Ubuntu Server LTS or Debian stable. The reason is predictability. LTS releases receive security updates for years without bumping major versions of system libraries, so a routine update will not break your stack. Ubuntu LTS ships every two years with roughly five years of standard support; Debian stable lives about three years plus a community-driven LTS extension.

An interim (non-LTS) release is a poor production choice: support lasts months, after which you are forced into a full distribution upgrade on a live server. A newer nginx or PHP version is a problem solved by adding the vendor's official repository, not by changing the operating system.

CriterionUbuntu Server LTSDebian stableInterim release
Support windowYears, fixedYears, fixedMonths
Package freshnessModerateConservativeHigh
Availability of guidesMaximumPlentyScarce
Risk of an update breaking productionLowVery lowHigh
Best fitTypical web projectLong-lived server, minimal churnStaging, experiments

A note on control panels: prebuilt panel images save time on day one but take over your nginx configuration and firewall rules. If you are reading this checklist, you most likely want a clean image. The trade-offs between hosting tiers are covered in shared hosting vs VPS vs dedicated server.

Step 1. Verify what you were actually given

Before installing software, confirm the hardware matches the plan and that you are not on an oversold node with a core shared ten ways. Three minutes of commands can save a day of support tickets.

# virtualization type: kvm is a full VM, lxc/openvz is a container
systemd-detect-virt

# CPU: core count, model, flags
lscpu | head -20

# memory and swap (most VPS images ship without swap)
free -h

# disks, partitions, free space
lsblk
df -hT

# network interfaces and addresses
ip -br address

# kernel and distribution
uname -r
cat /etc/os-release

What to look for:

  • Container virtualization (lxc, openvz) shares the host kernel. You cannot load kernel modules, own swap is frequently unavailable, some sysctl keys are read-only, and certain Docker or nftables features will not work. Usually fine for a website, a blocker for anything unusual.
  • Disk backend. A local NVMe virtio disk has a very different latency profile from a network volume. Quick sanity check: dd if=/dev/zero of=/tmp/t bs=1M count=1024 conv=fdatasync, then delete the file.
  • CPU flags. The presence of aes materially affects TLS handshake throughput; its absence is worth a question to support.

Most providers publish the virtualization type and storage class on the plan page — compare the command output against the specification, not against your expectations.

Step 2. First login, a dedicated user and sudo

The first connection is almost always as root, either with the emailed password or with a key injected at creation time. Living as root is unnecessary: every typo executes without confirmation, and logs cannot tell you who did what.

# first connection (default port 22)
ssh root@203.0.113.10

# 1. create the user: makes /home/deploy and asks for a password
adduser deploy

# 2. grant administrative rights
#    Debian/Ubuntu use the sudo group, RHEL/AlmaLinux/Rocky use wheel
usermod -aG sudo deploy

# 3. copy root's authorized keys into the new profile
rsync --archive --chown=deploy:deploy /root/.ssh /home/deploy/

# 4. permissions must be strict or sshd will ignore the keys
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys

# 5. VERIFY from a second terminal, keep the current session open
ssh -t deploy@203.0.113.10 'id; sudo true && echo sudo-ok'

# 6. only after a successful check, lock the root password
passwd -l root
Never close your current SSH session until the new access has been verified in a separate window. That is the only protection against "passwords disabled, key rejected, no console available".

SSH keys and disabling password authentication

Passwords can be guessed; keys cannot. Put the hardening directives in a drop-in file rather than the main config, so a package upgrade never overwrites your edits:

# /etc/ssh/sshd_config.d/10-hardening.conf
PermitRootLogin prohibit-password
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
PermitEmptyPasswords no
MaxAuthTries 3
LoginGraceTime 30

# validate the syntax BEFORE applying, or sshd will fail to start
sshd -t

# reload without dropping existing sessions
systemctl reload ssh

Key generation, ed25519 versus RSA, ssh-agent, multiple keys and debugging "Permission denied (publickey)" are covered separately in SSH key authentication setup. Every directive is documented in the official sshd_config(5) manual.

Moving SSH off port 22 is not security, but it does cut log noise from mass scanners dramatically. If you change it, add the firewall rule first, edit sshd second, and update the ufw profile accordingly.

Step 3. Firewall: close the perimeter before installing anything

On a clean system the firewall is usually empty: everything bound to 0.0.0.0 is reachable from the internet. While no services exist yet is exactly the right moment to set a default-deny policy.

# default policy
ufw default deny incoming
ufw default allow outgoing

# allow only what is needed
ufw allow OpenSSH        # profile = 22/tcp; for another port: ufw allow 2222/tcp
ufw allow 80/tcp         # HTTP: needed for redirects and certificate issuance
ufw allow 443/tcp        # HTTPS

# lockout insurance: reset the rules automatically in 10 minutes
systemd-run --unit=ufw-rollback --on-active=10min /usr/sbin/ufw --force reset

ufw enable               # asks for confirmation, warns about SSH disruption
ufw status verbose

# connection alive and rules correct, cancel the insurance
systemctl stop ufw-rollback.timer
The order is mandatory: ufw allow OpenSSH first, ufw enable second. Reversing it is the single most common way to lock yourself out of a new server.

Databases, Redis, admin panels and metrics endpoints do not belong on a public interface: bind them to 127.0.0.1 or a private network. If remote access is genuinely required, tunnel it over SSH or use the provider's private network instead of opening 3306 to the world. What happens when a port is left open is covered in open ports and their risks.

fail2ban in one command

Even with passwords disabled, brute-force traffic keeps knocking and filling your journals. fail2ban reads logs and bans sources through the firewall:

apt install -y fail2ban
printf '[sshd]\nenabled = true\nmaxretry = 3\nbantime = 1h\nfindtime = 10m\n' > /etc/fail2ban/jail.d/sshd.local
systemctl restart fail2ban
fail2ban-client status sshd

Backends for the systemd journal, false positives, custom nginx filters and office IP allowlists are detailed in the full fail2ban setup guide.

Server perimeter diagram: only ports 22, 80 and 443 exposed, database and cache bound to the loopback interface
Public surface: SSH, HTTP and HTTPS only; everything else stays on loopback

Step 4. Updates and automatic security patches

VPS images are built in advance and are typically weeks behind by the time you get one. Right after closing the perimeter, bring the system current:

apt update
apt full-upgrade -y
apt autoremove --purge -y

# is a reboot required (appears after kernel or libc updates)
[ -f /var/run/reboot-required ] && cat /var/run/reboot-required.pkgs

Then enable unattended installation of security updates specifically. Fully automatic upgrades of every package on a web server are risky — a major PHP or nginx version could land overnight. The security pocket is far safer and closes the bulk of published CVEs.

apt install -y unattended-upgrades apt-listchanges

# enables the periodic jobs and writes 20auto-upgrades
dpkg-reconfigure --priority=low unattended-upgrades

# key settings in /etc/apt/apt.conf.d/50unattended-upgrades:
#   Unattended-Upgrade::Allowed-Origins - keep the security origin only
#   Unattended-Upgrade::Mail "admin@example.com";
#   Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
#   Unattended-Upgrade::Automatic-Reboot "false";
#   Unattended-Upgrade::Automatic-Reboot-Time "04:00";

# dry run: shows exactly what would be installed right now
unattended-upgrade --dry-run --debug

# confirm the timers are active and when they fire
systemctl list-timers apt-daily.timer apt-daily-upgrade.timer

A kernel update is not applied until you reboot — until then you are running the vulnerable kernel with the patch merely installed. Make a habit of checking /var/run/reboot-required and rebooting in an agreed window. Parameter reference lives in the Debian documentation.

Step 5. Hostname, timezone and NTP

The hostname shows up in logs, outgoing mail and monitoring alerts. Set it once, in FQDN form:

hostnamectl set-hostname web01.example.com

# /etc/hosts should carry both the short and the fully qualified name
# 127.0.1.1   web01.example.com web01
hostname -f

Time is an underrated source of outages. Clock skew breaks TLS certificate validity checks, TOTP two-factor codes and signed API requests to cloud providers, and it turns your journals into useless noise during an incident review.

# timezone: UTC is easier on servers, local time is easier in reports
timedatectl set-timezone UTC

# synchronization status
timedatectl status

# for systemd-timesyncd
systemctl enable --now systemd-timesyncd
timedatectl show-timesync --all | head

# if chrony is in use
chronyc tracking
chronyc sources -v
A skew of only a few minutes produces "certificate is not yet valid" errors for some clients and breaks two-factor logins. Verify synchronization immediately, not when users start complaining.

Step 6. Swap, limits and unnecessary services

A swap file on a low-memory VPS

On plans with 1-2 GB of RAM, swap is not "slow memory" — it is a shock absorber. Without it, a traffic spike makes the kernel invoke the OOM killer, and the victim is usually the fattest process: the database or PHP-FPM. With swap, the same spike costs you latency instead of an outage.

# is swap already present?
swapon --show
free -h

# create the file (fallocate is fine on ext4; use dd on XFS/btrfs)
fallocate -l 2G /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=2048 status=progress
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile

# persist across reboots
echo '/swapfile none swap sw 0 0' >> /etc/fstab

# make the kernel less eager to swap out
printf 'vm.swappiness=10\nvm.vfs_cache_pressure=50\n' > /etc/sysctl.d/99-swap.conf
sysctl --system

# verify
swapon --show
cat /proc/sys/vm/swappiness

Sizing rule of thumb: with 1-2 GB of RAM use roughly the size of memory, with 4-8 GB use half, beyond that 4 GB is almost always enough. On btrfs the file needs special preparation (no copy-on-write), and under container virtualization your own swap may be unavailable — there the only lever is tuning application memory limits.

Open file limits

Default descriptor limits are easy to exhaust with nginx plus PHP-FPM plus a database under load. The symptom is "too many open files" in the logs while CPU and memory look idle.

# current value for the shell user
ulimit -n

# for user sessions: /etc/security/limits.d/90-nofile.conf
# *  soft  nofile  65535
# *  hard  nofile  65535

# systemd services do NOT read limits.conf; the unit wins,
# so change it through an override
systemctl edit nginx
#   [Service]
#   LimitNOFILE=65535

systemctl show nginx -p LimitNOFILE

What is listening

Images often ship extras: a mail daemon, rpcbind, debugging helpers. Every listening socket is attack surface.

# who listens, and which process owns it
ss -tulpn

# running services
systemctl list-units --type=service --state=running

# disable and remove what you do not need (example)
systemctl disable --now rpcbind.socket rpcbind
apt purge -y rpcbind

# journals must not eat the disk
journalctl --disk-usage
journalctl --vacuum-time=14d
# permanent cap: SystemMaxUse=500M in /etc/systemd/journald.conf
VPS memory diagram: RAM, a swap file on disk and the swappiness parameter controlling page eviction
Swap does not make a server faster, but it prevents processes from being killed during spikes

Step 7. Web stack, TLS, backups and monitoring — in that order

With the base in place, the server is ready for the project. Installation order matters, because each layer depends on the previous one.

  1. Web server. nginx at the front: TLS termination, static files, rate limits, proxying. A from-scratch configuration walkthrough is in the nginx configuration guide; the security side is in web server hardening.
  2. Application. PHP-FPM, Node.js or Python workers run as their own user, over a loopback socket, without write access to the code directory wherever that is feasible.
  3. Database. Bound to the local interface, one dedicated account per application, credentials outside the repository.
  4. TLS. Issue the certificate before production traffic, together with automatic renewal and an HTTP redirect. Step by step in free Let's Encrypt certificate setup.
  5. Backups. Configured BEFORE the first deploy. Schedule, restore testing and off-server copies are in the website backup guide.
  6. Monitoring. Turned on launch day: availability, certificate expiry, response time.
A backup you have never restored is not a backup. Test the restore on this same clean server while it holds no production data — you will never get a more convenient moment.

The full checklist: step, command, purpose, verification

StepCommand or filePurposeHow to confirm it applied
Resources and virtualizationlscpu, free -h, df -hT, systemd-detect-virtMatch the plan, know container limitsOutput matches the plan specification
Sudo useradduser, usermod -aG sudoStop working as rootssh -t deploy@host 'sudo true' succeeds
SSH keys/home/deploy/.ssh/authorized_keysBrute-force-resistant loginLogin with no password prompt from a new window
Password login off/etc/ssh/sshd_config.d/10-hardening.confRemoves an entire attack classsshd -t clean; password login refused
Root passwordpasswd -l rootRoot cannot authenticate by passwordpasswd -S root reports status L
Firewallufw allow OpenSSH, ufw enableOnly intended ports exposedufw status verbose plus an external scan
fail2ban/etc/fail2ban/jail.d/sshd.localAutomatic bans for brute forcefail2ban-client status sshd
Updatesapt full-upgradeCloses known vulnerabilitiesapt list --upgradable is empty
Auto patchingunattended-upgradesSecurity fixes land without a humanunattended-upgrade --dry-run --debug
Hostnamehostnamectl, /etc/hostsMeaningful logs and mail headershostname -f returns the FQDN
Timetimedatectl, chronyValid TLS, working TOTP, usable logstimedatectl status: synchronized = yes
Swap/swapfile, /etc/fstabOOM protection on small plansswapon --show after a reboot
Limitslimits.d, LimitNOFILENo "too many open files" errorssystemctl show nginx -p LimitNOFILE
Extra servicesss -tulpn, apt purgeMinimal attack surfaceOnly expected sockets in the output
Journalsjournald.conf, logrotateDisk is not filled by logsjournalctl --disk-usage within the cap
TLS and backupsACME client, backup jobHTTPS and recoverability from day oneA test restore actually completed

How to verify the setup from the outside

Local commands show intent; an external check shows reality. Reboot the server once, then run it through the outside contour:

  • What is really exposed. Scan the host from an external network with the open port scanner. Expected result: 22 (or your SSH port), 80, 443 — and nothing else.
  • Certificate and chain. Chain completeness, expiry and protocol support via the SSL checker. A missing intermediate is the most common defect: browsers stay silent, mobile clients and curl do not.
  • Security headers. HSTS, X-Content-Type-Options, frame policy — the security audit plus a line-by-line view in the HTTP header analyzer.
  • Response time. The TTFB of a clean server is the baseline you will later compare degradation against: website speed test.
  • Ongoing observation. Uptime monitoring with downtime and certificate-expiry alerts belongs on launch day, not after the first incident.
Launch order diagram: web server, application, database, then certificate, backups and uptime monitoring
Each layer builds on the previous one: TLS and backups are part of launch, not a follow-up

Common first-hour mistakes

  • Enabling ufw before allowing SSH. A classic. Only the provider console saves you.
  • Disabling passwords without testing the key. Always test from a second window before closing the first.
  • Leaving the database on 0.0.0.0. Even with a password this is an invitation: bots hammer MySQL and Redis credentials around the clock.
  • Skipping the reboot after a kernel update. The patch is installed but not in effect.
  • Setting up backups "after launch". The very first migration mistake lands in the window with no copies.
  • Storing backups on the same disk. One volume failure destroys the data and the copies together.
  • Installing a control panel over hand-written configs. The panel will rewrite nginx and firewall rules, quietly.

Frequently asked questions

How long does initial VPS setup really take?

Steps 0-6 of this checklist take 20-40 minutes if your SSH key is ready. Installing the web stack, issuing a certificate and configuring backups adds another one to two hours. If you do this regularly, wrap the steps in a script or an Ansible role and it collapses to minutes.

Do I need swap if I have enough RAM?

Yes — a small file helps even with headroom, because it lets the kernel evict unused pages and absorb short spikes without the OOM killer. The exception is workloads with hard latency requirements where any eviction is unacceptable.

Should I move SSH off port 22?

It is noise reduction, not security: mass scanners hit 22, so a non-standard port keeps logs readable. It will not stop a targeted attacker. Real protection comes from keys only, plus fail2ban, plus a default-deny firewall.

Root or sudo — does it matter on a single-admin server?

Yes. A separate account gives you auditable actions, protection from accidental destructive commands, and the ability to revoke one person's access without rotating a shared password. It also lets you keep PermitRootLogin prohibit-password and drop an entire category of login attempts.

Can I install a control panel after manual setup?

Technically yes, practically it is a conflict generator: panels rewrite web server configuration, firewall rules and cron jobs to match their own model. Pick one approach — a panel from the start on a clean system, or manual control throughout.

How do I know the server is correctly configured rather than merely working?

By external evidence: a scan shows only expected ports, the certificate is valid with a complete chain, security headers are present, monitoring sees the host, and a restore from backup has actually been performed. Everything else is an assumption.

Pre-deploy checklist

  • An LTS distribution is chosen and its version is recorded in project documentation.
  • Resources, virtualization type and free disk space have been verified.
  • A sudo user exists and key-based login was tested in a separate session.
  • Password authentication is off, the root password is locked, sshd -t is clean.
  • The firewall is enabled with deny-incoming policy; only SSH, 80 and 443 are open.
  • fail2ban runs with an active SSH jail.
  • The system is fully updated and unattended-upgrades installs security patches.
  • Hostname is set as an FQDN, timezone chosen, time synchronization confirmed.
  • Swap and swappiness are configured; descriptor limits raised for services.
  • Unnecessary services are removed; ss -tulpn holds no surprises.
  • Journal size is capped and logrotate is working.
  • The web stack is installed, a certificate issued, HTTP redirects to HTTPS.
  • Backups are configured and restored once before any production data lands.
  • Uptime and certificate-expiry monitoring is connected.
  • External checks of ports, TLS, headers and speed passed after a reboot.

Check your website right now

Monitor your server →
More articles: Infrastructure
Infrastructure
Database Connection Pooling: How It Works and Best Practices
16.03.2026 · 436 views
Infrastructure
API Versioning Strategies: URL, Header, and Query Parameter Approaches
16.03.2026 · 375 views
Infrastructure
Load Balancing Algorithms: Round Robin, Least Connections, and More
16.03.2026 · 346 views
Infrastructure
Multi-CDN Strategy: Failover, Cost Optimization, and Traffic Splitting
16.03.2026 · 272 views