Cloud Architecture·8 min read·

Single-VPS Architecture That Still Has Layers

Treat one VPS as stacked layers — edge, app, data, and ops — so processes, disks, and bind addresses fail independently instead of as one blob.

NB

Netbay Engineering

Netbay Engineering

On this page

A VPS is one machine and one public IP. That fact tempts people to treat it as one process, one user, and one directory. The result is a start script that launches the web server, the app, the database, Redis, a cron job, and a backup as if they were one organism. When anything fails, everything fails, and the only recovery path is reboot and hope. Architecture on one box is not about buying more machines. It is about drawing layers that can fail, restart, and be observed independently even though they share a kernel.

Four layers that fit on one host

Every production VPS that stays healthy longer than a weekend ends up with the same four layers, even when the team swears they are just running a small app.

  1. Edge: the process that owns ports 80 and 443, terminates TLS, and is the only thing the public internet is allowed to talk to.
  2. Application: one or more workers that speak HTTP on a loopback port. They hold no durable state of their own.
  3. Data: Postgres, Redis, or files that must survive a process restart. They bind privately and write to dedicated directories.
  4. Ops: journald, backups, timers, and the health endpoint that tells you which of the other three is lying.

The layers are not containers and they are not a cluster. They are process boundaries, filesystem boundaries, and network bind addresses. Those three tools are enough to make a 4 GB Ubuntu instance behave like a tiny platform instead of a pile of scripts.

Four layers on one VPS EDGE — nginx on :443 public IP, TLS, the only open ports APP — workers on 127.0.0.1:3000 stateless processes, one systemd unit DATA — Postgres and Redis on loopback dedicated dirs, private binds, High-Speed SSD OPS — journald, timers, /healthz observe and recover each layer on its own

Process boundaries beat a start script

If you have a shell file that launches everything with ampersands, you do not have architecture. You have a race. systemd units give each layer its own lifecycle: Restart=on-failure, a dedicated user, a WorkingDirectory, and a TimeoutStartSec that fails loudly instead of hanging the boot.

ini
# /etc/systemd/system/app.service
[Unit]
Description=App workers
After=network.target postgresql.service
Requires=postgresql.service

[Service]
User=app
WorkingDirectory=/srv/app/current
EnvironmentFile=/etc/app/env
ExecStart=/usr/bin/node dist/server.js
Restart=on-failure
RestartSec=2
MemoryMax=768M

[Install]
WantedBy=multi-user.target

Nginx, the app, and Postgres each get a file like this. The After and Requires lines are the architecture: they encode that the app does not start until the database socket exists, without a sleep 5 in a shell script. MemoryMax keeps a leaky worker from taking the database with it. That is a layer, not a comment in a README.

Filesystem and bind-address boundaries

Put each layer on a path that another layer is not allowed to write. /var/www holds edge static files. /srv/app holds releases. /var/lib/postgresql holds the database. Backups never write into those trees; they read from them and write to /var/backups or an off-box target. A full disk in /var/log should not take down Postgres, and a full WAL directory should not take down nginx. Separate trees make that possible to observe, even before you add extra block devices.

Network is the cheapest firewall you have. The app listens on 127.0.0.1:3000. Postgres listens on 127.0.0.1:5432. Redis listens on 127.0.0.1:6379. Only nginx binds 0.0.0.0:443. If a worker is compromised, it still cannot accept inbound connections from the internet because nothing is listening for them.

nginx
# /etc/nginx/sites-available/app
server {
    listen 443 ssl http2;
    server_name app.example.in;
    ssl_certificate     /etc/letsencrypt/live/app.example.in/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.in/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

The listen line on the edge and the proxy_pass to loopback are the contract between layers. Do not let the app bind *:3000 "just for debugging" and then forget. That single mistake collapses the edge layer into a decoration.

What layers buy you during an incident

When the app OOMs, systemd restarts the app unit. Nginx keeps serving a 502 with a static page. Postgres is untouched. When you need a schema migration, you stop the app unit, migrate, start it. The edge stays up. When a disk fills, you know which tree filled because each layer has its own directory and its own journal. A 4 GB VPS on Intel Xeon Platinum with High-Speed SSD storage in Lucknow DC01 is plenty for this layout. You are not missing a cluster. You are missing named boundaries. Draw them in systemd, in nginx, and in the filesystem, then keep them honest with a health check that reports each layer separately instead of a single boolean that hides which piece is on fire.

You can spin up an Ubuntu 24.04 instance on Netbay in under 60 seconds and lay these four layers down the same afternoon — netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance