If you’ve been running a shared hosting environment for any length of time, you’ve probably had that conversation with a customer: “My WordPress site is slow.” You’ve optimized MySQL, tuned Apache, enabled OPcache—and yet the database queries keep piling up. Enter Redis.
Redis is an in-memory data structure store that can dramatically reduce database load and improve application response times. But integrating Redis into a multi-tenant cPanel environment introduces challenges that don’t exist in single-server deployments. How do you prevent one account from accessing another’s cached data? How do you allocate memory fairly? How do you handle dozens or hundreds of Redis instances without losing your mind?
This guide covers the complete process of deploying Redis on cPanel servers in a way that’s secure, scalable, and actually maintainable.
Understanding Redis in a Shared Hosting Context
Before diving into configuration, it’s worth understanding why Redis in multi-tenant environments requires special consideration.
In a typical single-application deployment, Redis runs as one instance with full access to server memory. Applications connect via a Unix socket or TCP port, often without authentication. This works fine when you control the entire stack.
On a shared cPanel server, this model breaks immediately. You have untrusted users sharing resources, applications that shouldn’t see each other’s data, and wildly varying usage patterns. A single Redis instance with no isolation is a security incident waiting to happen—any user could flush the entire cache or read cached session data from other accounts.
There are two primary approaches to solving this: per-account Redis instances and a single shared instance with ACL-based isolation. Each has tradeoffs worth understanding.
Approach 1: Per-Account Redis Instances
Running a dedicated Redis instance for each cPanel account provides the strongest isolation. Each instance runs under the account’s user, binds to a unique socket, and has its own memory allocation. One account cannot possibly access another’s Redis data because they’re entirely separate processes.
Installation and Base Configuration
Start by installing Redis from your preferred repository. On AlmaLinux/CloudLinux 8+:
dnf install redis -y
Don’t enable the default Redis service—we won’t be using it. Instead, create a template configuration that will be instantiated per-account.
Create the template directory structure:
mkdir -p /etc/redis/templates
mkdir -p /var/run/redis
chmod 755 /var/run/redis
Create the base template at /etc/redis/templates/account.conf.template:
# Redis per-account template
# Variables: %%USER%%, %%HOMEDIR%%, %%UID%%, %%GID%%
daemonize yes
pidfile /var/run/redis/redis-%%USER%%.pid
# Bind to Unix socket only - no TCP exposure
port 0
unixsocket %%HOMEDIR%%/.redis/redis.sock
unixsocketperm 700
# Memory management
maxmemory 128mb
maxmemory-policy allkeys-lru
# Persistence - disable for pure caching
save ""
appendonly no
# Logging
loglevel warning
logfile /var/log/redis/redis-%%USER%%.log
# Security
protected-mode yes
# Database count - single DB is sufficient for most applications
databases 1
# Disable dangerous commands
rename-command FLUSHDB ""
rename-command FLUSHALL ""
rename-command DEBUG ""
rename-command CONFIG ""
rename-command SHUTDOWN REDIS_SHUTDOWN_%%USER%%
# Working directory
dir %%HOMEDIR%%/.redis
Provisioning Script
Create a script that provisions Redis for individual accounts. Save this as /usr/local/bin/cpanel-redis-provision:
#!/bin/bash
# cpanel-redis-provision - Create per-account Redis instance
# Usage: cpanel-redis-provision <username>
set -euo pipefail
USERNAME="${1:-}"
if [[ -z "$USERNAME" ]]; then
echo "Usage: $0 <username>" >&2
exit 1
fi
# Validate user exists and is a cPanel account
if ! id "$USERNAME" &>/dev/null; then
echo "Error: User $USERNAME does not exist" >&2
exit 1
fi
HOMEDIR=$(eval echo "~$USERNAME")
UID_NUM=$(id -u "$USERNAME")
GID_NUM=$(id -g "$USERNAME")
if [[ ! -d "$HOMEDIR" ]]; then
echo "Error: Home directory $HOMEDIR does not exist" >&2
exit 1
fi
# Create Redis directories
REDIS_DIR="$HOMEDIR/.redis"
mkdir -p "$REDIS_DIR"
chown "$USERNAME:$USERNAME" "$REDIS_DIR"
chmod 700 "$REDIS_DIR"
# Create log directory
mkdir -p /var/log/redis
touch "/var/log/redis/redis-$USERNAME.log"
chown "$USERNAME:$USERNAME" "/var/log/redis/redis-$USERNAME.log"
# Generate configuration from template
CONF_FILE="/etc/redis/redis-$USERNAME.conf"
sed -e "s|%%USER%%|$USERNAME|g" \
-e "s|%%HOMEDIR%%|$HOMEDIR|g" \
-e "s|%%UID%%|$UID_NUM|g" \
-e "s|%%GID%%|$GID_NUM|g" \
/etc/redis/templates/account.conf.template > "$CONF_FILE"
chown root:root "$CONF_FILE"
chmod 644 "$CONF_FILE"
# Create systemd unit
cat > "/etc/systemd/system/redis-$USERNAME.service" <<EOF
[Unit]
Description=Redis instance for $USERNAME
After=network.target
[Service]
Type=forking
User=$USERNAME
Group=$USERNAME
PIDFile=/var/run/redis/redis-$USERNAME.pid
ExecStart=/usr/bin/redis-server /etc/redis/redis-$USERNAME.conf
ExecStop=/bin/kill -s TERM \$MAINPID
Restart=on-failure
RestartSec=5s
# Security hardening
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=$HOMEDIR/.redis /var/run/redis /var/log/redis
# Resource limits
MemoryMax=192M
MemoryHigh=160M
[Install]
WantedBy=multi-user.target
EOF
# Reload systemd and start service
systemctl daemon-reload
systemctl enable "redis-$USERNAME.service"
systemctl start "redis-$USERNAME.service"
echo "Redis provisioned for $USERNAME"
echo "Socket: $HOMEDIR/.redis/redis.sock"
Make it executable:
chmod +x /usr/local/bin/cpanel-redis-provision
Deprovisioning Script
You’ll also need a cleanup script for when accounts are terminated. Save as /usr/local/bin/cpanel-redis-deprovision:
#!/bin/bash
# cpanel-redis-deprovision - Remove per-account Redis instance
# Usage: cpanel-redis-deprovision <username>
set -euo pipefail
USERNAME="${1:-}"
if [[ -z "$USERNAME" ]]; then
echo "Usage: $0 <username>" >&2
exit 1
fi
# Stop and disable service
systemctl stop "redis-$USERNAME.service" 2>/dev/null || true
systemctl disable "redis-$USERNAME.service" 2>/dev/null || true
# Remove files
rm -f "/etc/systemd/system/redis-$USERNAME.service"
rm -f "/etc/redis/redis-$USERNAME.conf"
rm -f "/var/run/redis/redis-$USERNAME.pid"
rm -f "/var/log/redis/redis-$USERNAME.log"
systemctl daemon-reload
echo "Redis deprovisioned for $USERNAME"
Hook Integration with cPanel
To automatically provision Redis for new accounts and clean up on termination, integrate with cPanel’s standardized hooks system.
Create /usr/local/cpanel/3rdparty/bin/redis-account-hooks.pl:
#!/usr/bin/perl
use strict;
use warnings;
my $action = $ARGV[0] // '';
my $user = $ENV{'CPANEL_USER'} // $ENV{'user'} // '';
exit 0 unless $user;
if ($action eq 'create') {
system('/usr/local/bin/cpanel-redis-provision', $user);
}
elsif ($action eq 'remove') {
system('/usr/local/bin/cpanel-redis-deprovision', $user);
}
exit 0;
Register the hooks:
/usr/local/cpanel/bin/manage_hooks add script /usr/local/cpanel/3rdparty/bin/redis-account-hooks.pl --category Whostmgr --event Accounts::Create --stage post
/usr/local/cpanel/bin/manage_hooks add script /usr/local/cpanel/3rdparty/bin/redis-account-hooks.pl --category Whostmgr --event Accounts::Remove --stage pre
Approach 2: Shared Redis with ACL Isolation
If running hundreds of Redis instances feels excessive for your environment, Redis 6.0+ ACLs offer an alternative. A single Redis instance serves all accounts, but each account authenticates with unique credentials and can only access keys with their designated prefix.
Shared Instance Configuration
Create /etc/redis/redis-shared.conf:
daemonize yes
pidfile /var/run/redis/redis-shared.pid
# TCP for localhost only
bind 127.0.0.1
port 6379
# Memory for all tenants combined
maxmemory 4gb
maxmemory-policy allkeys-lru
# Persistence disabled
save ""
appendonly no
# Logging
loglevel warning
logfile /var/log/redis/redis-shared.log
# ACL file for per-user permissions
aclfile /etc/redis/users.acl
# Require authentication
requirepass MASTER_ADMIN_PASSWORD_CHANGE_THIS
# Disable dangerous commands for regular users
# Admin can still use them with full auth
databases 16
ACL Management
Redis ACLs let you restrict users to specific key patterns. Create /etc/redis/users.acl:
# Default user disabled - all access requires authentication
user default off
# Admin user for management
user admin on >SECURE_ADMIN_PASSWORD ~* &* +@all
# Template for account users (generated by provisioning script):
# user cpaneluser1 on >generated_password ~cpaneluser1:* &* +@all -@admin -@dangerous
Create an ACL provisioning script at /usr/local/bin/cpanel-redis-acl-provision:
#!/bin/bash
# Add Redis ACL for cPanel account
# Usage: cpanel-redis-acl-provision <username>
set -euo pipefail
USERNAME="${1:-}"
ACL_FILE="/etc/redis/users.acl"
if [[ -z "$USERNAME" ]]; then
echo "Usage: $0 <username>" >&2
exit 1
fi
# Generate secure password
PASSWORD=$(openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c 32)
# Check if user already exists in ACL
if grep -q "^user $USERNAME " "$ACL_FILE"; then
echo "ACL entry for $USERNAME already exists" >&2
exit 1
fi
# Add ACL entry - user can only access keys prefixed with their username
echo "user $USERNAME on >$PASSWORD ~$USERNAME:* &* +@all -@admin -@dangerous -DEBUG -CONFIG -SHUTDOWN -FLUSHALL -FLUSHDB" >> "$ACL_FILE"
# Reload ACL in running Redis
redis-cli -a MASTER_ADMIN_PASSWORD_CHANGE_THIS ACL LOAD 2>/dev/null || true
# Store credentials for the user
HOMEDIR=$(eval echo "~$USERNAME")
CREDS_FILE="$HOMEDIR/.redis_credentials"
cat > "$CREDS_FILE" <<EOF
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_USER=$USERNAME
REDIS_PASSWORD=$PASSWORD
REDIS_PREFIX=${USERNAME}:
EOF
chown "$USERNAME:$USERNAME" "$CREDS_FILE"
chmod 600 "$CREDS_FILE"
echo "Redis ACL provisioned for $USERNAME"
echo "Credentials stored in $CREDS_FILE"
echo "Key prefix: ${USERNAME}:"
Key Prefix Enforcement
With ACL-based isolation, applications must prefix all keys with the username. This is critical—if an application writes to a key without the prefix, Redis will reject the operation.
For WordPress with the Redis Object Cache plugin, configure wp-config.php:
// Load credentials
$redis_creds = parse_ini_file($_SERVER['HOME'] . '/.redis_credentials');
define('WP_REDIS_HOST', $redis_creds['REDIS_HOST']);
define('WP_REDIS_PORT', $redis_creds['REDIS_PORT']);
define('WP_REDIS_USER', $redis_creds['REDIS_USER']);
define('WP_REDIS_PASSWORD', $redis_creds['REDIS_PASSWORD']);
define('WP_REDIS_PREFIX', $redis_creds['REDIS_PREFIX']);
// Required for Redis 6+ ACL authentication
define('WP_REDIS_SCHEME', 'tcp');
Memory Management Strategies
Memory allocation is where multi-tenant Redis deployments get tricky. Unlike CPU, which can be time-sliced, memory is a hard constraint. If Redis exceeds its allocation, things break.
Per-Instance Limits
For per-account instances, set conservative maxmemory limits in the configuration. The systemd unit’s MemoryMax provides a hard backstop if Redis somehow exceeds its configured limit.
A reasonable starting point for shared hosting:
| Hosting Tier | Redis Memory | Systemd MemoryMax |
|---|---|---|
| Basic | 64MB | 96MB |
| Business | 128MB | 192MB |
| Premium | 256MB | 384MB |
Server-Wide Memory Planning
Calculate your maximum Redis memory commitment:
Total Redis Memory = (Accounts with Redis) × (Average Memory Allocation)
Reserve this from your total server memory before allocating to MySQL, Apache, and other services. On a 64GB server, you might reserve 8GB for Redis, supporting ~64 accounts at 128MB each.
Monitor actual usage—many accounts won’t approach their limits. You can typically overcommit by 1.5-2x if your user base has varied usage patterns.
Eviction Policies
The maxmemory-policy setting determines what happens when Redis reaches its memory limit. For caching use cases, allkeys-lru is almost always correct—it evicts the least recently used keys to make room for new ones. The application continues working, just with a smaller effective cache.
Avoid noeviction in shared hosting. When memory fills, writes start failing, and applications break in confusing ways. Users will open tickets about random errors rather than slightly slower page loads.
Application Integration
WordPress
The Redis Object Cache plugin by Till Krüss is the standard choice. Install it via wp-cli for scripted deployments:
sudo -u $USERNAME wp plugin install redis-cache --activate --path=$HOMEDIR/public_html
For per-account instances using Unix sockets, wp-config.php needs:
define('WP_REDIS_SCHEME', 'unix');
define('WP_REDIS_PATH', $_SERVER['HOME'] . '/.redis/redis.sock');
Enable the object cache:
sudo -u $USERNAME wp redis enable --path=$HOMEDIR/public_html
Magento 2
Magento uses Redis for both caching and session storage. Edit app/etc/env.php:
'cache' => [
'frontend' => [
'default' => [
'backend' => 'Magento\\Framework\\Cache\\Backend\\Redis',
'backend_options' => [
'server' => '/home/username/.redis/redis.sock',
'port' => '0',
'database' => '0',
'compress_data' => '1'
]
],
'page_cache' => [
'backend' => 'Magento\\Framework\\Cache\\Backend\\Redis',
'backend_options' => [
'server' => '/home/username/.redis/redis.sock',
'port' => '0',
'database' => '1',
'compress_data' => '1'
]
]
]
],
'session' => [
'save' => 'redis',
'redis' => [
'host' => '/home/username/.redis/redis.sock',
'port' => '0',
'database' => '2',
'disable_locking' => '1'
]
]
Laravel
Laravel’s Redis support is built-in. For per-account instances, configure config/database.php:
'redis' => [
'client' => 'phpredis',
'default' => [
'scheme' => 'unix',
'path' => env('HOME') . '/.redis/redis.sock',
'database' => 0,
],
'cache' => [
'scheme' => 'unix',
'path' => env('HOME') . '/.redis/redis.sock',
'database' => 1,
],
],
Monitoring and Troubleshooting
Health Check Script
Create /usr/local/bin/redis-health-check:
#!/bin/bash
# Check all per-account Redis instances
echo "Redis Instance Health Report"
echo "============================"
echo ""
for conf in /etc/redis/redis-*.conf; do
[[ -f "$conf" ]] || continue
username=$(basename "$conf" .conf | sed 's/redis-//')
[[ "$username" == "shared" ]] && continue
socket=$(grep "^unixsocket " "$conf" | awk '{print $2}')
if systemctl is-active --quiet "redis-$username.service"; then
status="RUNNING"
# Get memory usage
if [[ -S "$socket" ]]; then
memory=$(redis-cli -s "$socket" INFO memory 2>/dev/null | grep "used_memory_human" | cut -d: -f2 | tr -d '\r')
keys=$(redis-cli -s "$socket" DBSIZE 2>/dev/null | awk '{print $2}')
else
memory="N/A"
keys="N/A"
fi
else
status="STOPPED"
memory="-"
keys="-"
fi
printf "%-20s %-10s %12s %10s keys\n" "$username" "$status" "$memory" "$keys"
done
Common Issues
Socket permission denied: The socket is created with permissions from unixsocketperm. Ensure the application’s PHP runs as the cPanel user, not nobody. With CloudLinux CageFS, verify the socket path is accessible within the cage.
Connection refused after server reboot: Check that the systemd unit is enabled and the service started. Look for failed dependencies in journalctl -u redis-username.service.
Memory usage keeps growing: Verify maxmemory is set. Without it, Redis will consume memory until the OOM killer intervenes. Check that maxmemory-policy is set to something other than noeviction.
Slow performance: Run redis-cli -s /path/to/socket SLOWLOG GET 10 to identify slow commands. Large KEYS * operations or huge values are common culprits.
Log Aggregation
Centralize Redis logs for easier troubleshooting:
# /etc/logrotate.d/redis-accounts
/var/log/redis/redis-*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
create 640 root root
}
Security Hardening
Disable TCP Completely (Per-Instance)
The template already binds to Unix socket only with port 0. Verify no TCP exposure:
ss -tlnp | grep redis
Should return nothing if properly configured.
Restrict Dangerous Commands
The template renames or disables commands that could affect other users or crash the instance. Consider also disabling:
rename-command KEYS "" # Prevents slow O(n) scans
rename-command SCAN "" # Same concern
rename-command CLIENT "" # Prevents connection manipulation
rename-command REPLICAOF "" # No replication in shared hosting
rename-command SLAVEOF "" # Legacy command, same concern
File Permissions
Ensure strict permissions on all Redis-related files:
# Configuration files readable only by root
chmod 644 /etc/redis/*.conf
# Socket directory
chmod 755 /var/run/redis
# Per-user directories
chmod 700 /home/*/.redis
# Credentials files
chmod 600 /home/*/.redis_credentials
CageFS Considerations
If running CloudLinux with CageFS, Redis sockets must be accessible within the cage. The recommended approach is placing the socket in the user’s home directory (which the template does).
If using a shared instance with TCP, ensure port 6379 is accessible from within CageFS but not from the public internet.
Offering Redis as a Feature
WHMCS Integration
If you’re billing for Redis access, create a configurable option in WHMCS and provision via the API:
// In your provisioning module
function yourmodule_CreateAccount($params) {
$username = $params['username'];
if ($params['configoptions']['redis'] == 'enabled') {
exec("/usr/local/bin/cpanel-redis-provision " . escapeshellarg($username), $output, $return);
if ($return !== 0) {
return "Failed to provision Redis: " . implode("\n", $output);
}
}
return 'success';
}
Feature Showcase
Create a simple cPanel plugin that shows Redis status and connection information. Users appreciate being able to verify their cache is working without opening a support ticket.
Wrapping Up
Redis on multi-tenant cPanel servers isn’t plug-and-play, but the performance benefits justify the setup complexity. The per-instance approach provides bulletproof isolation at the cost of more resource overhead. The shared ACL approach is more efficient but requires careful attention to key prefixing in applications.
Whichever path you choose, invest time in monitoring and automation. Redis problems tend to manifest as vague application errors rather than clear Redis failures, so proactive health checks save significant troubleshooting time.
The configuration templates and scripts in this guide provide a foundation, but adapt them to your environment. Memory limits should reflect your actual hardware and customer mix. Hook integration might need adjustment for your specific cPanel version. Test thoroughly on a staging server before deploying to production.