A virtual private server (VPS) is easy to deploy and surprisingly easy to leave exposed. A developer provisions a Linux instance, installs Docker, configures a reverse proxy, and deploys an application. Everything works. Six months later, the server has accumulated open ports, unused accounts, outdated containers, and configuration changes nobody remembers making.

The problem is rarely one catastrophic mistake. It is the gradual loss of control over what runs on the server, who can access it, and how it can be recovered.

Effective VPS security comes down to three priorities: reduce what an attacker can reach, limit what compromised software can do, and make recovery predictable.

These practices focus on Ubuntu and similar Linux distributions hosting web applications, APIs, and containers.

1. Start With a Server You Can Rebuild

A common VPS issue is configuration drift. Developers install packages, modify firewall rules, and adjust services directly on the server. Eventually, nobody can reproduce the original configuration.

Treat the VPS as something you should be able to replace.

Keep the initial configuration in Ansible, cloud-init, or another provisioning system. Record required packages, firewall rules, application users, and service configurations.

Separate persistent data from server configuration. Databases, uploaded files, and encryption keys need explicit backup and recovery procedures.

A practical test is to provision a second VPS and deploy the application without copying the original machine's filesystem. If that requires recovering undocumented commands from shell history, the infrastructure is not sufficiently reproducible.

This also improves incident response. When compromise is suspected, rebuilding from trusted images and restoring verified data is generally safer than attempting to remove every possible persistence mechanism.

2. Keep SSH Off the Public Internet When Possible

SSH keys are essential, but authentication alone does not eliminate the risks of exposing an administrative service.

For a VPS administered by a small team, consider making SSH accessible through a private network such as WireGuard or Tailscale. Alternatively, restrict inbound connections to known administrative IP addresses through the hosting provider's firewall.

If SSH must remain public, disable password authentication and direct root login after confirming that a separate administrator account works.

PermitRootLogin no

PasswordAuthentication no

KbdInteractiveAuthentication no

On Ubuntu, check both /etc/ssh/sshd_config and any included files under /etc/ssh/sshd_config.d/. OpenSSH generally uses the first obtained value for each configuration keyword, so adding a setting at the end of the main file may not override an earlier one.

Validate the configuration with sshd -t and inspect effective settings with sshd -T. For conditional Match rules, use sshd -T -C with the relevant connection parameters.

Keep an existing SSH session open until a new administrative connection succeeds.

Protect developers' private SSH keys, preferably with hardware backing, and revoke credentials when people leave the team. Changing SSH from port 22 may reduce automated scanning noise, but it does not replace access restrictions.

3. Check What Is Actually Listening

Developers often configure a firewall once and assume the server remains protected. Meanwhile, a debugging service or database may begin listening on every network interface.

Start with:

sudo ss -lntup

This displays listening TCP and UDP sockets and their associated processes. It helps you find unexpected listeners, but it does not show which services are externally reachable, especially when containers or network address translation are involved.

For a typical web application, public access may only be necessary on ports 80 and 443. Databases, message brokers, metrics endpoints, and administration interfaces should generally remain private.

Use two levels of enforcement: the provider's network firewall and a host firewall such as UFW or nftables. Remember that container networking may introduce additional firewall rules.

Also check IPv6. Restricting access through IPv4 rules does not guarantee that the same service is inaccessible over IPv6.

After modifying firewall rules, test connectivity from an external network. Checking what remote clients can actually reach is more reliable than inspecting firewall configuration alone.

4. Review Docker Port Publishing Carefully

Docker introduces a common security mistake: exposing services that developers intended to keep internal.

Consider this Docker Compose configuration:

services:

app:

ports:

- "8080:8080"

With Docker's default port publishing behavior, this makes the application available through all host interfaces, potentially including public IPv4 and IPv6 addresses.

If a reverse proxy running directly on the host needs access, bind the published port to localhost:

services:

app:

ports:

- "127.0.0.1:8080:8080"

On current Docker Engine versions using standard bridge networking, this restricts access through the published host port to localhost. However, Docker versions older than 28.0.0 had a vulnerability allowing neighboring hosts on the same Layer 2 segment to access ports published to localhost.

Custom Docker network settings also matter. Direct routing and alternative bridge gateway modes can permit access through container IP addresses, independently of the published host address.

If the reverse proxy also runs in Docker, connecting both services through an appropriate Docker network can eliminate the need to publish the application port on the host.

Another important detail: Docker's firewall rules can bypass restrictions that administrators expect UFW to enforce.

Inspect published ports regularly:

docker ps --format "table {{.Names}}\t{{.Ports}}"

Review unnecessary port mappings, privileged containers, Docker socket mounts, and access to host directories. Verify actual exposure externally rather than relying exclusively on firewall status or container listings.

5. Restrict What Compromised Applications Can Do

Even a well-configured firewall cannot prevent exploitation of vulnerabilities in applications that must remain publicly accessible.

The next question is what happens after an attacker controls an application process.

Run services under dedicated accounts without unnecessary permissions. A Node.js API, for example, generally should not need root privileges, write access to application code, or access to another application's configuration files.

For system services managed by systemd, inspect the available hardening restrictions:

systemd-analyze security myapp.service

Then consider settings such as:

[Service]

User=myapp

NoNewPrivileges=yes

PrivateTmp=yes

ProtectSystem=strict

ProtectHome=yes

ReadWritePaths=/var/lib/myapp

This assumes the application data directory exists and has appropriate filesystem permissions.

These controls restrict privilege escalation and filesystem access. They do not provide complete process isolation. In particular, ProtectSystem=strict does not make the API filesystems /dev, /proc, and /sys read-only, and filesystem restrictions do not prevent communication through accessible Unix sockets.

Test the configuration against normal application behavior before deploying it to production. Some applications require additional writable directories or access to system resources.

For containers, follow the same principle: run processes as nonroot where practical, remove unnecessary Linux capabilities, and avoid privileged mode.

The goal is to give each service only the access required to perform its job.

6. Patch More Than the Operating System

Automatic Linux updates are necessary, but they do not cover every component running on the VPS.

Ubuntu's unattended-upgrades can install distribution security updates, but third-party repositories require appropriate configuration. Application dependencies, downloaded binaries, and container images need separate update processes.

Track three maintenance areas: the host operating system, supporting infrastructure such as Docker Engine, and application dependencies.

Rebuild container images regularly using maintained base images. Scan dependencies during development and deploy updated images through the normal release process.

Also plan restarts. Installing a patched library does not necessarily replace vulnerable code already loaded into memory.

On Ubuntu 24.04 LTS, the needrestart package, when installed and invoked through APT, automatically restarts affected services by default, subject to configured exclusions. This also applies to unattended upgrades and can unexpectedly interrupt production workloads.

Review its behavior and configure exclusions for critical services when necessary. Schedule their restarts during maintenance windows rather than leaving vulnerable code running indefinitely.

Kernel updates may require a reboot unless an appropriate live patching mechanism covers the vulnerability.

7. Stop Treating Environment Files as Secret Storage Solutions

Secrets frequently leak through deployment shortcuts. Developers copy .env files between servers, embed tokens in scripts, or accidentally expose credentials through logs.

Environment files can be an acceptable configuration mechanism, but they are not a dedicated security boundary.

Use a secrets manager where practical. For smaller deployments, restricted configuration files or systemd credentials can be reasonable alternatives.

Avoid placing secrets in command arguments, where they may appear in process information or operational logs. Environment variables are convenient, but they can also be exposed through application diagnostics, process inspection under appropriate privileges, or a compromised application.

Also check whether the hosting platform provides instance credentials or sensitive information through a metadata service. On applicable cloud platforms, a server-side request forgery (SSRF) vulnerability may allow attackers to access metadata that the application can reach.

Use the provider's metadata protections, restrict unnecessary access, and assign instance credentials only the permissions required.

Finally, make credential rotation routine. Replacing a leaked database password should not require rediscovering the entire deployment procedure.

8. Collect Logs That Help Investigate Incidents

A VPS may record authentication attempts and application errors, but local logs have limited value if attackers can erase them after gaining privileged access.

Forward important events to a separate logging destination. Prioritize authentication activity, privilege escalation, application errors, reverse proxy requests, and changes to important configurations.

Build alerts around events requiring attention. Repeated failed SSH logins are common on publicly accessible servers. A successful administrative login from an unexpected source is usually more useful to investigate.

Monitor disk consumption too. Container logs without appropriate rotation can exhaust available storage and disrupt healthy applications.

Configure log retention, protect sensitive information in log records, and make sure the logging system does not become another source of outages.

9. Make Backups Survive a Server Compromise

Provider snapshots are convenient, but they should not be the only recovery mechanism.

If an attacker compromises the hosting account, snapshots controlled by that account may also be exposed or deleted.

Maintain separate backups of databases, uploaded files, and essential configuration. Encrypt them and use storage with independent credentials. Where supported, enable retention controls that prevent backups from being immediately modified or deleted.

For databases, use backup methods that ensure consistency rather than assuming a simple filesystem copy is always sufficient.

Most importantly, test restoration.

Provision a clean VPS, restore recent data, start the application, and verify that it works. Check more than whether the restore command completed. Confirm that the application can read its data and perform important operations.

This reveals missing files, expired credentials, and undocumented dependencies before a real incident occurs.

10. Automate a Small Security Audit

VPS hardening deteriorates as developers add services and modify configurations. A short recurring audit is more useful than a lengthy checklist performed only during initial deployment.

Automate checks for unexpected open ports, outdated packages, expiring TLS certificates, failed backups, and important configuration changes.

A weekly review should answer four questions:

  1. Has anything become publicly reachable that was previously private?
  2. Are important security updates pending or waiting for a restart?
  3. Have privileged users, service permissions, or deployment credentials changed?
  4. Can the application still be restored from a recent backup?

Run external checks where possible. Monitoring from another system can detect problems even when the VPS is unavailable or compromised.

Compare the deployed configuration against your provisioning files. Investigate unexpected differences rather than letting them become the new undocumented baseline.