Skip to content

Linux & Infrastructure

Lessons from Deploying Applications on Small VPS Instances

Memory, swap, disk and backups behave differently on a one or two gigabyte server. A checklist for keeping small machines boring and predictable.

3 min read

  • vps
  • linux
  • self-hosting
  • operations

A one or two gigabyte VPS can run a surprising amount of software. It can also fall over in ways that never happen on a laptop with plenty of headroom. Small machines punish the same few oversights again and again: no swap, logs that grow forever, builds that need more memory than the server has, and backups nobody has tested.

These are the habits that keep a small server boring.

Know your memory budget

Start by looking at what is actually free once your services are running:

Shell
free -h
ps -eo pid,comm,rss --sort=-rss | head -n 10

When memory runs out, the kernel's out-of-memory killer picks a process and ends it. On a small server that is often your database or your application. Two things make this less likely.

Add a swap file

Swap is slow, but it turns a sudden crash into a slowdown you can notice and fix:

Shell
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Then tell the kernel to prefer RAM and only use swap under pressure:

Shell
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swappiness.conf
sudo sysctl --system

Give every service a ceiling

A memory limit means one misbehaving service gets restarted instead of taking the whole machine with it. With Docker Compose:

YAMLcompose.yaml
services:
  app:
    image: ghcr.io/example/app:latest
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 512M

For a service managed by systemd, MemoryMax= does the same job:

systemd/etc/systemd/system/app.service
[Unit]
Description=Example application
After=network-online.target
Wants=network-online.target

[Service]
User=app
WorkingDirectory=/opt/app
ExecStart=/opt/app/bin/server
Restart=on-failure
RestartSec=5
MemoryMax=512M

[Install]
WantedBy=multi-user.target

Build somewhere else

Compiling a modern web application can need more memory than the server it will run on. Build in CI or on your own machine, ship an image or an artifact, and let the server only run things. Deploys get faster and a build can never knock the live site over.

Stop logs from filling the disk

Disks on small plans are small, and logs grow quietly. Cap the system journal with a drop-in file, which survives package upgrades better than editing the main config:

INI/etc/systemd/journald.conf.d/size.conf
[Journal]
SystemMaxUse=200M
Shell
sudo systemctl restart systemd-journald

Docker keeps container logs forever unless told otherwise. Rotate them in /etc/docker/daemon.json, then restart Docker. The setting applies to containers created after the change:

JSON/etc/docker/daemon.json
{
    "log-driver": "json-file",
    "log-opts": {
        "max-size": "10m",
        "max-file": "3"
    }
}

Old images add up too. docker system prune reclaims them; read what it will delete before you confirm.

Patch automatically, reboot deliberately

Security updates should not wait for you to remember them:

Shell
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

Kernel updates still need a reboot. Pick a quiet time and do it on purpose, rather than discovering months later that the server never restarted.

Back up, then prove it

A backup only counts once you have restored from it. Whatever tool you use, schedule the backup, send it off the machine, and practise a restore into a scratch directory now and then. Write down the restore steps while you still remember them, because you will need them on a bad day.

Watch from the outside

A process that has stopped cannot tell you it stopped. Use an external uptime check that alerts you when the site does not answer, plus a simple disk check:

Shellcheck-disk.sh
#!/usr/bin/env bash
set -euo pipefail

usage=$(df --output=pcent / | tail -n 1 | tr -dc '0-9')

if [ "$usage" -ge 85 ]; then
    echo "Disk usage on / is ${usage}%" >&2
    exit 1
fi

Run it from cron or a systemd timer and route failures to wherever you will actually see them.

The short version

Risk Habit
Out of memory Swap file, low swappiness, memory limits per service
Heavy builds Build in CI, deploy artifacts
Full disk Capped journal, rotated container logs, pruned images
Unpatched system Unattended security upgrades, planned reboots
Lost data Off-site backups and a restore you have practised
Silent failures External uptime checks and simple alerts

Small servers are not fragile by nature. They are just honest about limits that bigger machines let you ignore.

End

Share

New articles arrive in the RSS feed. Questions or corrections are welcome on the contact page.