Why Do Low-Spec VPS Servers Suffer from OOM?
Many users purchase cheap 1GB or even 512MB memory VPS servers, only to have services frequently crash with OOM (Out of Memory) kills after running a few Docker containers. The root causes are:
- Conservative default Linux swap strategy:
vm.swappiness=60triggers disk swap too early, hurting performance - No zram compressed swap: Traditional swap writes directly to disk—slow and wears out SSDs
- Unlimited Docker containers: A memory-leaking container can take down the entire system
- Untuned Kernel parameters: TCP buffers, file descriptors, and other defaults aren’t optimized for high-concurrency scenarios
This guide walks you through three layers of optimization: zram compressed swap → Kernel parameter tuning → Docker memory limits, systematically solving memory problems on low-spec VPS servers.
1. zram Compressed Swap: Trading CPU for Memory Space
1.1 What is zram?
zram is a Linux kernel module that creates a compressed block device in RAM to use as swap. Data written to zram is compressed, typically achieving 2:1 to 3:1 compression ratios.
Compared to traditional swap (writing to disk):
- Faster: Compression/decompression happens in memory—orders of magnitude faster than disk I/O
- SSD-friendly: No physical disk writes, extending SSD lifespan
- Transparent: Applications are unaware; the kernel manages everything automatically
1.2 Installing and Configuring zram
# Ubuntu/Debian
apt install zram-config -y
# CentOS/Rocky Stream
yum install zram-generator-defaults -y
# or manually
dnf install zram-generator-defaults -y
# Alpine
apk add zram-init
1.3 Manual zram Configuration (Recommended for Full Control)
Create a systemd service for automatic startup:
cat > /etc/systemd/system/zram-swap.service << 'EOF'
[Unit]
Description=Setup zram compressed swap device
After=multi-user.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/zramctl --find --size 2G --algorithm zstd
ExecStart=/usr/bin/mkswap /dev/zram0
ExecStart=/usr/bin/swapon /dev/zram0
ExecStop=/usr/bin/swapoff /dev/zram0
ExecStop=/usr/bin/zramctl --reset /dev/zram0
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable zram-swap.service
systemctl start zram-swap.service
Verify zram is active:
$ swapon --show
NAME TYPE SIZE USED PRIO
/dev/zram0 partition 2G 0B 5
$ free -h
total used free shared buff/cache available
Mem: 976M 412M 89M 28M 475M 441M
Swap: 2.0G 0B 2.0G
1.4 Tune swappiness for Maximum zram Effectiveness
# Apply temporarily
sysctl vm.swappiness=100
# Make permanent
echo 'vm.swappiness=100' >> /etc/sysctl.conf
# Verify
sysctl vm.swappiness
Key parameter explanations:
vm.swappiness=0: Avoid swap entirely (for servers with ample RAM)vm.swappiness=60: Linux defaultvm.swappiness=100: Actively use swap (optimal with zram)vm.swappiness=180: Aggressive mode (for extremely low-memory VPS)
2. Kernel Parameter Tuning: Unlocking Memory Potential
2.1 Core Configuration
Add the following to /etc/sysctl.d/99-memory-optimization.conf:
# ==================== Memory Management ====================
# Reduce kernel cache tendency, minimize buff/cache usage
vm.swappiness=100
vm.vfs_cache_pressure=50
# Allow overcommit: commit_overcommit=2 means you can request up to 2x RAM+Swap
vm.overcommit_memory=2
vm.overcommit_ratio=80
# Compression level (1-12, higher = better compression but more CPU)
vm.zswap.max_pool_percent=30
# ==================== File Descriptors ====================
fs.file-max=100000
fs.nr_open=100000
# ==================== TCP Network Tuning ====================
# Reduce TCP buffers to save memory
net.ipv4.tcp_mem = 65536 131072 262144
net.ipv4.tcp_rmem = 4096 87380 262144
net.ipv4.tcp_wmem = 4096 65536 262144
# Enable TCP Fast Open
net.ipv4.tcp_fastopen = 3
# Enable TIMESTAMPS to avoid sequence number conflicts
net.ipv4.tcp_timestamps = 1
# ==================== OOM Protection ====================
kernel.panic_on_oops = 1
Apply configuration:
sysctl --system
2.2 Parameter Reference
| Parameter | Value | Purpose |
|---|---|---|
vm.swappiness | 100 | Actively use zram swap to prevent memory pressure |
vm.vfs_cache_pressure | 50 | Reduce inode/dentry cache reclamation, protect metadata |
vm.overcommit_memory | 2 | Strict mode, prevent system crash from memory overcommit |
vm.overcommit_ratio | 80 | Allowable memory = RAM × 80% + Swap |
vm.zswap.max_pool_percent | 30 | zswap compression pool max 30% of RAM |
fs.file-max | 100000 | System-wide file descriptor limit |
net.ipv4.tcp_mem | 65536/131072/262144 | TCP memory page pressure thresholds |
net.ipv4.tcp_rmem/wmem | 4096/87380/262144 | TCP read/write buffer min/default/max |
2.3 Monitoring Memory Status
# Detailed memory info
cat /proc/meminfo
# Key metrics to watch:
# MemAvailable: truly available memory
# Buffers/Cache: kernel cache
# SwapFree/SwapTotal: zram swap usage
# Active(anon)/Inactive(anon): anonymous memory (process data)
# Active(file)/Inactive(file): file cache
# Real-time monitoring
htop # Interactive process monitor
smem -tk # Sort by PSS (more accurate memory usage)
3. Docker Container Memory Limits: Preventing Single-Container System Crash
3.1 Why Limit Docker Memory?
Docker containers have no memory limit by default. If a container develops a memory leak, it will consume all host memory and trigger the OOM killer, potentially killing critical services.
3.2 Setting Memory Limits in docker-compose.yml
services:
webapp:
image: myapp:latest
mem_limit: 512m # Hard limit: max 512MB
memswap_limit: 768m # Memory+swap total 768MB (reserve 256MB for swap)
cpus: 1.0 # Limit CPU cores
pids_limit: 200 # Limit process count, prevent fork bombs
database:
image: postgres:16
mem_limit: 256m
memswap_limit: 384m
environment:
- POSTGRES_MAX_CONNECTIONS=50
redis:
image: redis:7-alpine
mem_limit: 128m
command: redis-server --maxmemory 100mb --maxmemory-policy allkeys-lru
3.3 Runtime Memory Limits for Single Containers
# Start with memory limits
docker run -d \
--name myapp \
--memory=512m \
--memory-swap=768m \
--cpus=1.0 \
--pids-limit=200 \
myapp:latest
# Update an existing container
docker update --memory=512m --memory-swap=768m myapp
3.4 Recommended Memory Configuration by Service
| Service | Min Memory | Recommended | Swap Config |
|---|---|---|---|
| Nginx | 64MB | 128MB | 1.5x memory |
| PostgreSQL | 256MB | 512MB | 1.5x memory |
| Redis | 128MB | 256MB | 1.5x memory |
| MySQL/MariaDB | 256MB | 512MB | 1.5x memory |
| Node.js App | 256MB | 512MB | 1.5x memory |
| Python App | 128MB | 256MB | 1.5x memory |
| MongoDB | 256MB | 512MB | 1.5x memory |
| Prometheus | 256MB | 512MB | 1.5x memory |
| Grafana | 128MB | 256MB | 1.5x memory |
3.5 PostgreSQL-Specific Memory Tuning
PostgreSQL is memory-hungry and needs additional tuning:
# Add to docker-compose.yml environment
environment:
- POSTGRES_SHARED_BUFFERS=64MB # Default 128MB, reduced for low-memory
- POSTGRES_EFFECTIVE_CACHE_SIZE=192MB
- POSTGRES_MAINTENANCE_WORK_MEM=32MB
- POSTGRES_WORK_MEM=16MB
- POSTGRES_MAX_CONNECTIONS=50 # ~10MB per connection
# Or adjust via SQL
ALTER SYSTEM SET shared_buffers = '64MB';
ALTER SYSTEM SET effective_cache_size = '192MB';
ALTER SYSTEM SET work_mem = '16MB';
ALTER SYSTEM SET max_connections = '50';
SELECT pg_reload_conf();
4. System-Level Memory Cleanup and Maintenance
4.1 Periodic Cache Cleanup
# Clean dentries and inodes cache (safe, kernel auto-recycles)
sync && echo 2 > /proc/sys/vm/drop_caches
# Clean pagecache, dentries, and inodes (more aggressive)
sync && echo 3 > /proc/sys/vm/drop_caches
# Note: This only frees "reclaimable" cache, won't kill any processes
4.2 Finding Memory Hogs
# Sort processes by RSS
ps aux --sort=-%mem | head -20
# Sort by PSS (more accurate)
smem -tkp | head -20
# Docker container memory usage
docker stats --no-stream
# Find memory-leaking applications
journalctl -u your-service --since "24 hours ago" | grep -i "oom\|killed"
4.3 Automated Memory Monitoring Script
cat > /usr/local/bin/memory-watch.sh << 'EOF'
#!/bin/bash
# VPS Memory Monitoring Alert Script
THRESHOLD_WARN=70
THRESHOLD_CRIT=85
usage=$(free | awk '/^Mem:/ {printf "%.0f", $3/$2 * 100}')
swap_free=$(free | awk '/^Swap:/ {if($2==0) print 0; else printf "%.0f", $3/$2 * 100}')
if [ "$usage" -ge "$THRESHOLD_CRIT" ]; then
echo "CRITICAL: Memory usage at ${usage}% | Swap used: ${swap_free}%" | tee -a /var/log/memory-watch.log
curl -s -X POST "https://api.telegram.org/botTOKEN/sendMessage" \
-d "chat_id=CHATID" \
-d "text=🔴 VPS Memory Alert: ${usage}% used, swap ${swap_free}%" \
--max-time 5
elif [ "$usage" -ge "$THRESHOLD_WARN" ]; then
echo "WARNING: Memory usage at ${usage}% | Swap used: ${swap_free}%" | tee -a /var/log/memory-watch.log
fi
EOF
chmod +x /usr/local/bin/memory-watch.sh
# Add to crontab, check every 5 minutes
(crontab -l 2>/dev/null; echo "*/5 * * * * /usr/local/bin/memory-watch.sh") | crontab -
5. Optimization Results Comparison
| Metric | Before | After | Improvement |
|---|---|---|---|
| Available memory | ~300MB | ~600MB | +100% |
| OOM kill frequency | 2-3x/day | Nearly zero | -90% |
| Service stability | Frequent restarts | Stable | Significant |
| SSD write volume | High | Very low | -80% |
| Response latency | Jagged | Smooth | -40% |
6. Troubleshooting
Q1: zram configured but swap not active?
# Check if kernel module is loaded
lsmod | grep zram
# Load manually if needed
modprobe zram
# Check service status
systemctl status zram-swap.service
journalctl -u zram-swap.service -n 20
Q2: Docker container OOM-killed despite being under mem_limit?
The container’s processes may be allocating memory beyond the limit (e.g., large malloc objects). Check logs:
dmesg | grep -i "oom\|killed"
docker logs --tail 50 your-container
Solution: Relax memswap_limit slightly, or optimize memory usage at the application level.
Q3: Which zram compression algorithm to choose?
# View supported algorithms
cat /sys/block/zram0/comp_algorithm
# Recommended: zstd (high compression ratio, moderate CPU cost)
# Alternative: lz4 (fastest, slightly lower compression ratio)
Q4: How to verify optimization is sufficient?
# Stress test
stress-ng --vm 2 --vm-bytes 256M --timeout 60s
# Monitor memory changes
watch -n 1 'free -h && echo "---" && cat /proc/meminfo | grep -E "MemAvailable|SwapFree|Zswap"'
Conclusion
Through zram compressed swap + Kernel parameter tuning + Docker memory limits three-layer optimization, even a 1GB low-spec VPS can stably run multiple services. The core philosophy: trade compression for space, use limits to prevent失控, and tune for efficiency.
Start your VPS memory health check today!
