Featured image of post VPS Memory Optimization Complete Guide: zram Compressed Swap + Kernel Tuning + Docker Memory Limits

VPS Memory Optimization Complete Guide: zram Compressed Swap + Kernel Tuning + Docker Memory Limits

Can a 1GB VPS run smoothly? With zram compressed swap, Kernel parameter tuning, and Docker memory limits, say goodbye to OOM kills on low-spec servers.

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=60 triggers 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

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 default
  • vm.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

ParameterValuePurpose
vm.swappiness100Actively use zram swap to prevent memory pressure
vm.vfs_cache_pressure50Reduce inode/dentry cache reclamation, protect metadata
vm.overcommit_memory2Strict mode, prevent system crash from memory overcommit
vm.overcommit_ratio80Allowable memory = RAM × 80% + Swap
vm.zswap.max_pool_percent30zswap compression pool max 30% of RAM
fs.file-max100000System-wide file descriptor limit
net.ipv4.tcp_mem65536/131072/262144TCP memory page pressure thresholds
net.ipv4.tcp_rmem/wmem4096/87380/262144TCP 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
ServiceMin MemoryRecommendedSwap Config
Nginx64MB128MB1.5x memory
PostgreSQL256MB512MB1.5x memory
Redis128MB256MB1.5x memory
MySQL/MariaDB256MB512MB1.5x memory
Node.js App256MB512MB1.5x memory
Python App128MB256MB1.5x memory
MongoDB256MB512MB1.5x memory
Prometheus256MB512MB1.5x memory
Grafana128MB256MB1.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

MetricBeforeAfterImprovement
Available memory~300MB~600MB+100%
OOM kill frequency2-3x/dayNearly zero-90%
Service stabilityFrequent restartsStableSignificant
SSD write volumeHighVery low-80%
Response latencyJaggedSmooth-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!

📺 看视频版教程 → DuckDB Lab YouTube

Subscribe for more DuckDB & AI automation tutorials