Introduction to Void Linux Performance Tuning for Web Servers
Void Linux is a lightweight, independent distribution built around the xbps package manager and the runit init system. Unlike systemd-based distributions, Void boots fast, runs fewer background services by default, and gives administrators fine-grained control over what executes on the system. These characteristics make it an excellent foundation for high-performance web servers, but out-of-the-box settings are tuned for general-purpose use rather than production workloads. This tutorial walks through the practical steps required to transform a fresh Void Linux installation into a lean, responsive web server capable of handling significant traffic.
Why Performance Tuning Matters
Default kernel parameters, network stack settings, and service configurations are conservative. They prioritize stability across diverse hardware rather than throughput for a specific workload. On a web server, this means you may be leaving substantial performance on the table. Proper tuning improves request latency, increases concurrent connection capacity, reduces memory waste, and makes the system more resilient under load spikes. On Void specifically, the absence of systemd means you also avoid the overhead of socket activation daemons, D-Bus sessions, and other background machinery that consumes CPU cycles and RAM even when idle.
System Preparation and Baseline Measurement
Before changing anything, establish a baseline. You cannot improve what you cannot measure. Install benchmarking and monitoring tools, record current performance, and then compare after each change.
# Update the system
sudo xbps-install -Su
# Install monitoring and benchmarking tools
sudo xbps-install -S htop iotop sysstat iproute2 curl apachebench wrk
# Record baseline network and system stats
sar -n DEV 1 5 > baseline_network.txt
vmstat 1 5 > baseline_vm.txt
Keep these baseline files. After applying tuning changes, generate new reports and compare them. Look for reduced context switches, lower wait CPU percentages, higher network throughput, and reduced memory pressure.
Kernel and Network Stack Tuning
The Linux kernel exposes tunable parameters through /proc/sys. On Void, the standard way to persist these is via /etc/sysctl.d/ configuration files. Create a dedicated file for web server tuning.
Creating the Sysctl Configuration
sudo tee /etc/sysctl.d/99-webserver.conf << 'EOF'
# Increase TCP max buffer size
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.core.rmem_default = 262144
net.core.wmem_default = 262144
# TCP buffer auto-tuning
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
# Increase connection queue sizes
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
net.ipv4.tcp_max_syn_backlog = 65535
# Enable TCP Fast Open
net.ipv4.tcp_fastopen = 3
# Reduce TIME_WAIT and enable reuse
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_max_tw_buckets = 1048576
# Keepalive tuning
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 5
# Disable ICMP redirects and enable reverse path filtering
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.rp_filter = 1
# Increase local port range for outbound connections
net.ipv4.ip_local_port_range = 10240 65535
# Increase file descriptor limits at kernel level
fs.file-max = 2097152
fs.nr_open = 2097152
# Inotify tuning for applications that watch files
fs.inotify.max_user_watches = 524288
fs.inotify.max_user_instances = 512
# Virtual memory and swap behavior
vm.swappiness = 10
vm.vfs_cache_pressure = 50
vm.dirty_ratio = 10
vm.dirty_background_ratio = 5
EOF
# Apply immediately
sudo sysctl --system
Understanding Key Parameters
- net.core.somaxconn: Controls the maximum number of queued connections waiting for
accept(). The default of 128 or 4096 is too low for busy servers. - tcp_fastopen: Eliminates one round trip on the initial TCP handshake for repeat clients. The value 3 enables it for both clients and servers.
- tcp_tw_reuse: Allows reuse of sockets in TIME_WAIT state for outbound connections, reducing port exhaustion under heavy load.
- fs.file-max: Sets the system-wide limit on open file descriptors. Web servers handling many connections need this raised significantly.
- vm.swappiness: Lower values make the kernel prefer keeping application data in RAM rather than swapping it out, which is critical for latency-sensitive web workloads.
File Descriptor and Process Limits
Beyond the kernel-level fs.file-max, individual processes are constrained by user-level limits configured through PAM. Void uses the standard /etc/security/limits.conf mechanism.
sudo tee /etc/security/limits.d/99-webserver.conf << 'EOF'
# Raise limits for the web server user
nginx soft nofile 1048576
nginx hard nofile 1048576
nginx soft nproc 65535
nginx hard nproc 65535
# Apply to all users if you run multiple services
* soft nofile 65535
* hard nofile 65535
EOF
Verify the limits are applied by logging in as the service user and checking:
# Check soft and hard limits
su - nginx -s /bin/sh -c "ulimit -n"
su - nginx -s /bin/sh -c "ulimit -Hn"
Service Management with Runit
Void uses runit for service supervision. Services live in /etc/sv/ and are enabled by symlinking them into /var/service/. The runit model is simple: each service directory contains a run script that execs the daemon in the foreground. If the process exits, runit restarts it automatically.
Disabling Unnecessary Services
A lean system runs only what is required. Audit enabled services and disable anything unnecessary.
# List enabled services
ls -la /var/service/
# Disable a service (example: remove agetty on serial if unused)
sudo rm /var/service/agetty-ttyS0
# Stop a service without removing the symlink
sudo sv down <servicename>
# Check status of all services
sudo sv status /var/service/*
Common services to review include additional agetty instances, sshd if you use key-only access on a non-standard port, dhcpcd if you use static IPs, and nanoklogd if you do not need kernel log buffering.
Creating a Custom Nginx Service
If you need custom environment variables or resource limits for your web server, create a tailored service directory rather than modifying the default.
sudo mkdir -p /etc/sv/nginx-custom
sudo tee /etc/sv/nginx-custom/run << 'EOF'
#!/bin/sh
# Set resource limits before exec
ulimit -n 1048576
ulimit -u 65535
# Set performance-related environment variables
export LANG=C
export LC_ALL=C
exec chpst -u nginx:nginx nginx -g 'daemon off;'
EOF
sudo chmod +x /etc/sv/nginx-custom/run
# Enable the custom service
sudo ln -s /etc/sv/nginx-custom /var/service/nginx-custom
The chpst command from the runit suite changes the process user and group. The daemon off; directive keeps Nginx in the foreground so runit can supervise it properly.
Installing and Tuning Nginx
Nginx is the most common web server choice on Void for high-traffic deployments. Install it and configure it for performance.
sudo xbps-install -S nginx
# Enable the default service
sudo ln -s /etc/sv/nginx /var/service/nginx
Worker Process and Connection Tuning
Edit the main Nginx configuration to match your hardware. The key is to set worker processes equal to the number of CPU cores and to raise the worker connection limit.
# Determine CPU core count
nproc
sudo tee /etc/nginx/nginx.conf << 'EOF'
user nginx;
worker_processes auto;
worker_rlimit_nofile 1048576;
pid /run/nginx.pid;
events {
worker_connections 65535;
use epoll;
multi_accept on;
}
http {
# Basic settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
keepalive_requests 1000;
types_hash_max_size 2048;
server_tokens off;
# Buffer sizes
client_body_buffer_size 16k;
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;
client_max_body_size 8m;
# Connection limiting
reset_timedout_connection on;
send_timeout 30;
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types
text/plain
text/css
text/xml
application/json
application/javascript
application/xml
application/xml+rss
image/svg+xml;
# Logging - disable or buffer for performance
access_log off;
error_log /var/log/nginx/error.log warn;
# Open file cache
open_file_cache max=10000 inactive=5m;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
include /etc/nginx/mime.types;
default_type application/octet-stream;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
EOF
# Test and reload
sudo nginx -t
sudo sv reload nginx
Explanation of Key Directives
- worker_processes auto: Nginx automatically detects and uses all CPU cores, avoiding context-switching overhead from over-provisioning workers.
- multi_accept on: Each worker accepts all new connections at once rather than one at a time, reducing syscall overhead under connection bursts.
- sendfile on: Uses the kernel
sendfile()syscall to transfer files directly from disk to socket, bypassing user-space buffer copies. - tcp_nopush: Packs response headers and the start of the file body into a single TCP segment, reducing packet overhead.
- open_file_cache: Caches file descriptors and metadata, eliminating repeated disk lookups for frequently requested static files.
- access_log off: Disabling access logging eliminates disk I/O on every request. If you need logging, use a buffered format or send logs to a remote syslog server.
PHP-FPM Tuning for Dynamic Content
If your web server runs PHP applications, PHP-FPM is the standard process manager. Its default pool configuration is tuned for small workloads. Adjust it based on available RAM and expected traffic.
sudo xbps-install -S php-fpm
sudo ln -s /etc/sv/php-fpm /var/service/php-fpm
Calculate how many PHP worker processes you can run. Divide available RAM (minus what Nginx and the OS need) by the average memory consumption per PHP process. A typical PHP process uses 30 to 80 MB depending on the application.
sudo tee /etc/php/php-fpm.d/www.conf << 'EOF'
[www]
user = nginx
group = nginx
listen = /run/php-fpm/php-fpm.sock
listen.owner = nginx
listen.group = nginx
# Dynamic process management
pm = dynamic
pm.max_children = 120
pm.start_servers = 12
pm.min_spare_servers = 6
pm.max_spare_servers = 24
pm.max_requests = 1000
# Slow log for debugging
slowlog = /var/log/php-fpm/slow.log
request_slowlog_timeout = 5s
# Status page (restrict access in Nginx)
pm.status_path = /fpm-status
ping.path = /ping
EOF
sudo sv restart php-fpm
The pm.max_requests setting is important: it recycles worker processes after handling a set number of requests, preventing memory leaks in long-running PHP applications from accumulating indefinitely.
Database Tuning with PostgreSQL
For web servers running a database locally, PostgreSQL is a strong choice on Void. Install and tune it for a dedicated server scenario.
sudo xbps-install -S postgresql
sudo ln -s /etc/sv/postgresql /var/service/postgresql
Edit the main PostgreSQL configuration file. The exact path depends on the version, typically /var/lib/postgresql/data/postgresql.conf after initialization.
# Initialize if not already done
sudo -u postgres initdb -D /var/lib/postgresql/data
sudo tee -a /var/lib/postgresql/data/postgresql.conf << 'EOF'
# Performance tuning
max_connections = 100
shared_buffers = 2GB
effective_cache_size = 6GB
work_mem = 32MB
maintenance_work_mem = 512MB
checkpoint_completion_target = 0.9
wal_buffers = 16MB
default_statistics_target = 100
random_page_cost = 1.1
effective_io_concurrency = 200
# Connection pooling hint
listen_addresses = '127.0.0.1'
EOF
sudo sv restart postgresql
These values assume a server with 8 GB of RAM. Adjust shared_buffers to roughly 25% of total RAM and effective_cache_size to 75% of total RAM. The random_page_cost of 1.1 is appropriate for SSD storage, signaling to the query planner that random reads are nearly as cheap as sequential reads.
Transparent Huge Pages and CPU Governor
Transparent Huge Pages (THP) can cause latency spikes on web servers due to kernel compaction stalls. Disable THP for predictable performance.
# Check current THP status
cat /sys/kernel/mm/transparent_hugepage/enabled
# Disable immediately
echo never | sudo tee /sys/kernel/mm/transparent_hugepage/enabled
echo never | sudo tee /sys/kernel/mm/transparent_hugepage/defrag
# Persist across reboots via rc.local
sudo tee /etc/rc.local << 'EOF'
#!/bin/sh
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
EOF
sudo chmod +x /etc/rc.local
Set the CPU governor to performance mode to prevent frequency scaling latency from affecting request handling.
# Check available governors
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors
# Set performance governor
echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
# Persist via rc.local (append to existing file)
echo 'for cpu in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do echo performance > "$cpu"; done' | sudo tee -a /etc/rc.local
I/O Scheduler Selection
The I/O scheduler affects disk latency and throughput. For SSD and NVMe storage, the none (formerly noop) or mq-deadline scheduler is optimal because these devices have negligible seek time and can handle reordering themselves.
# Check current scheduler
cat /sys/block/sda/queue/scheduler
# Set to none for NVMe/SSD
echo none | sudo tee /sys/block/sda/queue/scheduler
# Persist via rc.local
echo 'for dev in /sys/block/sd*/queue/scheduler /sys/block/nvme*/queue/scheduler; do [ -w "$dev" ] && echo none > "$dev"; done' | sudo tee -a /etc/rc.local
For traditional spinning disks, keep the default mq-deadline scheduler, which optimizes request ordering to minimize seek overhead.
Firewall Configuration with nftables
Void includes nftables as the preferred firewall framework. A properly configured firewall not only secures the server but can also mitigate certain denial-of-service patterns by rate-limiting connections.
sudo xbps-install -S nftables
sudo ln -s /etc/sv/nftables /var/service/nftables
sudo tee /etc/nftables.conf << 'EOF'
#!/usr/sbin/nft -f
flush ruleset
table inet webserver {
set blacklist {
type ipv4_addr
flags timeout
timeout 1h
}
chain input {
type filter hook input priority 0; policy drop;
# Allow loopback
iif "lo" accept
# Allow established connections
ct state established,related accept
# Allow SSH with rate limiting
tcp dport 22 ct state new limit rate 5/minute burst 10 packets accept
# Allow HTTP and HTTPS
tcp dport { 80, 443 } accept
# Drop invalid packets
ct state invalid drop
# ICMP rate limiting
icmp type echo-request limit rate 1/second accept
# Drop everything else
counter drop
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
EOF
sudo nft -f /etc/nftables.conf
sudo sv restart nftables
Memory and Swap Configuration
For a dedicated web server, swap should exist as a safety net but should rarely be used. The vm.swappiness setting in the sysctl configuration already addresses this. Additionally, consider using zram for compressed swap in RAM, which is faster than disk swap and useful for handling memory spikes.
sudo xbps-install -S zram
# Create a zram swap service
sudo mkdir -p /etc/sv/zram-swap
sudo tee /etc/sv/zram-swap/run << 'EOF'
#!/bin/sh
# Create a 2GB zram device
modprobe zram num_devices=1
echo lz4 > /sys/block/zram0/comp_algorithm
echo 2G > /sys/block/zram0/disksize
mkswap /dev/zram0
swapon -p 100 /dev/zram0
# Keep the service running
exec chpst -b zram-swap pause
EOF
sudo tee /etc/sv/zram-swap/finish << 'EOF'
#!/bin/sh
swapoff /dev/zram0 2>/dev/null
echo 1 > /sys/block/zram0/reset 2>/dev/null
EOF
sudo chmod +x /etc/sv/zram-swap/run /etc/sv/zram-swap/finish
sudo ln -s /etc/sv/zram-swap /var/service/zram-swap
The lz4 compression algorithm offers an excellent balance between compression ratio and CPU overhead, making it ideal for zram swap on production servers.
Monitoring and Ongoing Maintenance
Tuning is not a one-time activity. Continuously monitor the server to detect regressions and identify new bottlenecks as traffic patterns evolve.
Setting Up Lightweight Monitoring
# Install monitoring tools
sudo xbps-install -S sysstat procps-ng
# Enable sysstat data collection
sudo sed -i 's/false/true/' /etc/default/sysstat
sudo ln -s /etc/sv/sysstat /var/service/sysstat
# Create a simple monitoring script
sudo tee /usr/local/bin/server-health.sh << 'EOF'
#!/bin/sh
echo "=== System Load ==="
uptime
echo -e "\n=== Memory ==="
free -h
echo -e "\n=== Top Processes by CPU ==="
ps aux --sort=-%cpu | head -10
echo -e "\n=== Top Processes by Memory ==="
ps aux --sort=-%mem | head -10
echo -e "\n=== Network Connections ==="
ss -s
echo -e "\n=== Disk Usage ==="
df -h
echo -e "\n=== Nginx Connections ==="
ss -tnp | grep nginx | wc -l
EOF
sudo chmod +x /usr/local/bin/server-health.sh
Log Rotation
Logs can fill disk space and cause performance degradation. Configure log rotation to keep logs manageable.
sudo xbps-install -S logrotate
sudo tee /etc/logrotate.d/webserver << 'EOF'
/var/log/nginx/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
postrotate
sv reload nginx 2>/dev/null || true
endscript
}
/var/log/php-fpm/*.log {
weekly
rotate 8
compress
delaycompress
missingok
notifempty
postrotate
sv reload php-fpm 2>/dev/null || true
endscript
}
EOF
# Add a daily cron-like runit service for logrotate
sudo mkdir -p /etc/sv/logrotate-daily
sudo tee /etc/sv/logrotate-daily/run << 'EOF'
#!/bin/sh
while true; do
sleep 86400
/usr/sbin/logrotate /etc/logrotate.conf
done
EOF
sudo chmod +x /etc/sv/logrotate-daily/run
sudo ln -s /etc/sv/logrotate-daily /var/service/logrotate-daily
Best Practices Summary
- Measure before and after: Always establish baselines and compare metrics after each change. Tune one variable at a time to isolate effects.
- Keep the system lean: Disable every service you do not actively need. Void makes this easy with runit's symlink-based activation model.
- Persist all changes: Use
/etc/sysctl.d/,/etc/rc.local, and custom runit services to ensure settings survive reboots. - Match worker counts to hardware: Nginx workers should equal CPU cores. PHP-FPM children should fit within available RAM. Over-provisioning causes context-switch overhead.
- Use caching aggressively: Enable Nginx open_file_cache, opcode caching in PHP, and application-level caching. The fastest request is one that never reaches the backend.
- Monitor continuously: Set up sysstat collection and review trends weekly. Performance degradation often happens gradually.
- Secure while tuning: A fast server that is compromised serves no one. Configure nftables, use SSH keys, and disable password authentication.
- Test under load: Use
wrkorapachebenchto simulate traffic before deploying changes to production.
Load Testing Your Configuration
After applying all tuning changes, run load tests to validate the improvements.
# Basic Apache Bench test - 1000 requests, 100 concurrent
ab -n 1000 -c 100 http://localhost/
# More realistic wrk test - 30 seconds, 50 threads, 200 connections
wrk -t 50 -c 200 -d 30s http://localhost/
# Test with keepalive
wrk -t 50 -c 200 -d 30s --latency http://localhost/
Compare the requests-per-second and latency percentiles against your baseline. A well-tuned Void Linux server should show significantly higher throughput and lower p99 latency than the default configuration.
Conclusion
Void Linux provides an excellent foundation for high-performance web servers thanks to its minimal footprint, fast boot times, and the simplicity of runit service management. By systematically tuning the kernel network stack, raising file descriptor limits, configuring Nginx and PHP-FPM to match your hardware, optimizing database parameters, disabling Transparent Huge Pages, selecting the right I/O scheduler, and setting up a lightweight firewall with rate limiting, you can extract substantially more performance from the same hardware. The key principles are to measure first, change one variable at a time, persist all configurations, and monitor continuously. With these practices in place, your Void Linux web server will handle traffic efficiently, respond with low latency, and remain stable under load.