To make a 512 MB or 1 GB Linux VPS run smoothly, measure what really uses memory, add compressed swap in RAM (zram) on top of a small swap file, cap the systemd journal, and switch off services a server does not need. This guide walks through each step on Debian 12/13 and Ubuntu 22.04/24.04, notes the Rocky Linux and AlmaLinux differences, and shows how to read OOM killer messages when memory still runs out.
Step 1: Measure Before You Change Anything
Find the real memory users first, so you fix the right thing.
free -h
ps -eo pid,user,rss,comm --sort=-rss | head -n 15 # RSS in KB, biggest first
systemd-cgtop -m -n 1 # memory per service
cat /proc/pressure/memory # memory pressure (PSI)
ps lists the largest processes, and systemd-cgtop adds up memory per service, which is clearer when a service runs many worker processes. In /proc/pressure/memory, an avg60 value that stays above about 10 means tasks are often waiting for memory. (On Rocky/AlmaLinux, PSI is off unless the kernel is booted with psi=1.)
RSS counts shared libraries in full for every process, so the sum looks larger than reality. smem shows PSS, which splits shared memory fairly between processes:
apt install smem # Rocky / AlmaLinux: from EPEL
smem -tk -s pss -r | head -n 15
Step 2: Read free -h Correctly: available, Not free
total used free shared buff/cache available
Mem: 961Mi 402Mi 74Mi 5.0Mi 484Mi 410Mi
Swap: 1.0Gi 12Mi 1.0Gi
A small free value is normal. Linux uses spare RAM as page cache (buff/cache) to keep files in memory, and gives it back as soon as programs need it. The number that matters is available: an estimate of how much memory new work can get without swapping. In the example above, only 74 MB is "free" but 410 MB is available, so the server is fine. Start worrying when available stays below roughly 10% of RAM and swap use keeps growing.
Step 3: Add zram (Compressed Swap in RAM)
zram creates a swap device inside RAM that stores pages compressed, often at 2-3 times smaller size. Swapping to it is far faster than swapping to disk, so it is a very effective way to stretch a small plan. The cost is a little CPU for compression.
Check that your kernel has zram
modprobe zram && echo "zram is available"
swapon --show
If modprobe says Module zram not found, your kernel was built without it (some minimal cloud kernels are). Skip to Step 4 and use a swap file only.
Option A: zram-tools (Debian, Ubuntu 22.04 and 24.04)
apt install zram-tools
cp /etc/default/zramswap /etc/default/zramswap.bak
cat > /etc/default/zramswap <<'EOF'
ALGO=zstd
PERCENT=50
PRIORITY=100
EOF
systemctl restart zramswap
zramctl
swapon --show
PERCENT=50 sizes the zram device at half of RAM (uncompressed capacity); the RAM it really uses is the compressed size. PRIORITY=100 makes the kernel use zram before any disk swap.
Option B: systemd zram-generator (Debian 12/13, Ubuntu 24.04, Rocky/AlmaLinux 9)
apt install systemd-zram-generator # Rocky / AlmaLinux 9: dnf install zram-generator
cat > /etc/systemd/zram-generator.conf <<'EOF'
[zram0]
zram-size = min(ram / 2, 2048)
compression-algorithm = zstd
swap-priority = 100
EOF
systemctl daemon-reload
systemctl start [email protected]
zramctl
On Rocky/AlmaLinux, if dnf cannot find the package, enable EPEL first (dnf install epel-release).
zstd is rejected, use lz4, which is also faster on a busy single-core plan.Step 4: Keep a Small Swap File as a Backup and Tune swappiness
zram lives in RAM, so a small disk swap file is still useful as a last reserve: 512 MB-1 GB is enough on these plans. Our guide How to Add Swap Space on a Linux VPS covers creating it, fstab and resizing step by step. A swap file has a lower default priority than the zram device, so it is only used when zram is full.
With zram, raise vm.swappiness: swapping compressed pages into RAM is cheaper than dropping file cache and reading it back from disk. vm.page-cluster = 0 stops the kernel from reading several swap pages at once, which only helps on disks.
cat > /etc/sysctl.d/99-zram.conf <<'EOF'
vm.swappiness = 100
vm.page-cluster = 0
EOF
sysctl --system
sysctl vm.swappiness vm.vfs_cache_pressure
Files in /etc/sysctl.d/ are read in alphabetical order, so 99-zram.conf overrides the vm.swappiness=10 from our swap guide's 99-swap.conf. Without zram, keep 10. The vm.vfs_cache_pressure=50 from that guide is fine; never set it to 0, because the kernel would then never free file system metadata.
Step 5: Limit journald
The systemd journal stores logs in /var/log/journal on disk, or in /run/log/journal when the journal is not persistent. /run is a tmpfs, so that copy lives in RAM. Cap both:
journalctl --disk-usage
mkdir -p /etc/systemd/journald.conf.d
cat > /etc/systemd/journald.conf.d/size.conf <<'EOF'
[Journal]
SystemMaxUse=100M
RuntimeMaxUse=20M
MaxRetentionSec=1month
EOF
systemctl restart systemd-journald
journalctl --vacuum-size=100M
journalctl --disk-usage
SystemMaxUse limits the disk journal, RuntimeMaxUse the in-memory one, and MaxRetentionSec drops entries older than a month. On Ubuntu, rsyslog also writes the same messages to /var/log/syslog; it is rotated by logrotate, so it is usually fine to keep.
Step 6: Disable Services the Server Does Not Need
Check what runs before you stop anything, and stop only what you can identify:
systemctl list-units --type=service --state=running
systemctl status multipathd --no-pager # what is it and what started it?
systemctl list-dependencies --reverse multipathd.service # does anything need it?
| Service | Found on | Safe to disable when |
|---|---|---|
snapd | Ubuntu | snap list shows no snap you use (see below) |
multipathd | Ubuntu Server | multipath -ll prints nothing, which is normal on a VPS |
ModemManager | Some Ubuntu images | Always on a VPS: there is no modem |
tuned | Rocky / AlmaLinux | Optional; saves a little RAM. Keep it if unsure |
# Ubuntu: multipath storage daemon (not needed without SAN/multipath disks)
multipath -ll # no output = not in use
systemctl disable --now multipathd.service multipathd.socket
# Modem manager - a VPS has no modem
systemctl disable --now ModemManager
# Undo at any time:
systemctl enable --now multipathd.service multipathd.socket
snapd uses tens of MB and mounts a loop device for each snap. Removing it also removes every installed snap with its data, so check snap list first. If, for example, certbot was installed as a snap, install it from apt before you remove snapd. The pin file stops apt from installing snapd again as a dependency.
snap list # what is installed as a snap?
apt purge snapd
cat > /etc/apt/preferences.d/no-snapd.pref <<'EOF'
Package: snapd
Pin: release a=*
Pin-Priority: -10
EOF
ssh/sshd, systemd's own services (systemd-journald, systemd-logind, systemd-networkd, systemd-resolved), networking or NetworkManager, cron, time sync (chrony or systemd-timesyncd) and your firewall (ufw, firewalld, nftables). Also leave qemu-guest-agent and cloud-init in place if they are installed; the platform may rely on them, and they use little or no memory once the server has booted. Prefer disable over mask, so you can turn a service back on easily.Step 7: Move Automatic Updates to a Quiet Hour
Do not disable unattended-upgrades: security updates matter more than a few MB of RAM. The memory peak comes from apt running, so schedule it for a quiet time instead. By default apt downloads at a random time within a 12-hour window and upgrades between about 06:00 and 07:00; these drop-ins run the download at 04:00 and the upgrade at 04:30 server time (check with timedatectl):
mkdir -p /etc/systemd/system/apt-daily.timer.d /etc/systemd/system/apt-daily-upgrade.timer.d
cat > /etc/systemd/system/apt-daily.timer.d/override.conf <<'EOF'
[Timer]
OnCalendar=
OnCalendar=*-*-* 04:00
RandomizedDelaySec=15m
EOF
cat > /etc/systemd/system/apt-daily-upgrade.timer.d/override.conf <<'EOF'
[Timer]
OnCalendar=
OnCalendar=*-*-* 04:30
RandomizedDelaySec=15m
EOF
systemctl daemon-reload
systemctl list-timers 'apt-*'
On Rocky/AlmaLinux with dnf-automatic, change dnf-automatic.timer the same way.
Step 8: Choose Lighter Software
- Keep OpenSSH. It uses only a few MB while idle. Replacements such as Dropbear save very little, lack some security features and are updated less often by distributions, so they are not worth the risk.
- Prefer nginx over Apache with mod_php. With Apache's prefork model, every child process carries a full PHP interpreter. nginx with PHP-FPM, or at least Apache with
mpm_eventand PHP-FPM, uses far less memory. - Run PHP-FPM with
pm = ondemand. Workers start when requests arrive and exit after being idle, instead of waiting in memory. - Size the database to the plan. MySQL and MariaDB defaults assume more RAM: lower the buffer pool and
max_connections, and turn offperformance_schema.
; /etc/php/8.2/fpm/pool.d/www.conf (Rocky / AlmaLinux: /etc/php-fpm.d/www.conf)
pm = ondemand
pm.max_children = 5
pm.process_idle_timeout = 10s
pm.max_requests = 500
The PHP version in the path depends on the system: 8.2 on Debian 12, 8.4 on Debian 13, 8.1 on Ubuntu 22.04 and 8.3 on Ubuntu 24.04. Set pm.max_children to the memory you can give PHP divided by the average worker size, for example 200 MB / 40 MB = 5. Measure the size, then test and reload:
# Average size of a PHP-FPM process
ps -eo rss,comm | awk '/php-fpm/ {s+=$1; n++} END {if (n) printf "%d processes, average %.0f MB\n", n, s/n/1024}'
php-fpm8.2 -t && systemctl reload php8.2-fpm # Rocky / AlmaLinux: php-fpm -t && systemctl reload php-fpm
Step 9: Understand the OOM Killer and Find OOM Events
When RAM and swap are both exhausted, the kernel's OOM killer ends one process to save the system, usually the one with the highest oom_score, which is mostly the largest. A service that "randomly" restarts or a database that vanished is often an OOM kill. Search the kernel log:
journalctl -k | grep -i -E 'out of memory|oom-kill|killed process'
dmesg -T | grep -i -E 'out of memory|killed process'
journalctl -k -b -1 | grep -i 'killed process' # previous boot (persistent journal only)
A typical line looks like this:
Out of memory: Killed process 812 (mariadbd) total-vm:1325400kB, anon-rss:301220kB, file-rss:0kB, shmem-rss:0kB, UID:106
It names the process and how much memory it held (anon-rss, in kB). If the same service is killed repeatedly, shrink its settings, add swap or zram, or cap it as in Step 10. On Ubuntu, also check systemctl status systemd-oomd: when that service is active, it can stop whole services under memory pressure before the kernel does.
Step 10: Cap a Hungry Service with systemd MemoryMax
A memory limit on one service protects the rest of the server: if PHP-FPM leaks memory, only PHP-FPM is reined in, while SSH and the database stay up. MemoryHigh is a soft limit, above which the kernel slows the service down and reclaims its memory; MemoryMax is the hard limit, at which the OOM killer acts inside that service only.
stat -fc %T /sys/fs/cgroup # cgroup2fs = cgroup v2 (required)
mkdir -p /etc/systemd/system/php8.2-fpm.service.d
cat > /etc/systemd/system/php8.2-fpm.service.d/memory.conf <<'EOF'
[Service]
MemoryHigh=250M
MemoryMax=300M
# A killed worker is replaced by the master instead of stopping the service
OOMPolicy=continue
Restart=on-failure
EOF
systemctl daemon-reload
systemctl restart php8.2-fpm
systemctl show php8.2-fpm -p MemoryHigh -p MemoryMax -p MemoryCurrent
Debian 12/13, Ubuntu 22.04/24.04 and Rocky/AlmaLinux 9 use cgroup v2 by default. On older cgroup v1 systems such as Rocky/AlmaLinux 8, use MemoryLimit= instead; MemoryHigh works only with v2. Avoid hard limits on the database itself: a sudden kill forces crash recovery. Size its settings instead.
Step 11: Know When to Upgrade
Tuning helps when memory peaks now and then. If available stays low, swap is always busy and vmstat 1 10 shows constant swap-in and swap-out (si/so), the workload simply needs more RAM. JUSTG cloud servers can be upgraded from the service page; see How to Upgrade VPS RAM, SSD Disk or IP.
FAQ
Is zram better than a swap file?
For speed, yes: it works at memory speed and avoids disk I/O. But it is limited by RAM, so the best setup on small plans is both: zram with high priority for everyday use and a small swap file as a last reserve.
Does zram use a lot of CPU?
Compression costs some CPU, usually little. If the server swaps to zram all the time on a single-core plan, switch to lz4 and reduce the workload, because constant swapping means memory is too small.
Can I run Docker on the 512 MB mini plan?
A container or two with small apps can work, but the Docker daemon and container runtime take memory of their own. Set memory limits on each container, use zram and swap, and consider 1 GB or more for several containers.
If services keep being killed and you cannot find the cause, open a ticket at https://www.justg.com/submitticket.php with the server IP and the output of free -h and journalctl -k | grep -i oom. JUSTG offers 24/7 AI support by ticket, and engineers follow up on issues that need a person.