Docker Redis Compose Guide: Persistence, ACL Authentication & Security Hardening (2026)
Most “docker redis” tutorials stop at docker run -p 6379:6379 redis. That command works, but it gives you a Redis server with no authentication, no durable storage you control, a port that may be reachable from your whole network, and no way for Docker to know whether Redis is actually healthy.
This guide builds a Redis 8 deployment with Docker Compose that you can leave running. You will set up a dedicated ACL user for your application, persistence with AOF and RDB snapshots, a healthcheck, container hardening flags, a localhost-only published port, and a backup routine. You also get a troubleshooting table for the errors people hit most often.
Table of Contents
- What you are deploying (Redis 8, licensing, Valkey)
- Architecture overview
- Project layout
- Step 1: Create ACL users
- Step 2: Write redis.conf
- Step 3: Write docker-compose.yml
- Step 4: Start and verify
- Step 5: Connect an application container
- Persistence: RDB vs AOF
- Backup and restore
- Security hardening checklist
- Host tuning for Redis
- Troubleshooting
- FAQ
- Related guides
1. What you are deploying
Redis 8 merged the former Redis Stack modules (JSON, time series, probabilistic data structures, and the query engine) into the core “Redis Open Source” distribution, and it is available as an official Docker image. The redis:8-alpine tag is a good default for a small footprint.
Licensing: Redis 8 is offered under a tri-license: RSALv2, SSPLv1, and AGPLv3. AGPLv3 is OSI-approved open source, but some organizations have policies against AGPL software. If that applies to you, Valkey (the Linux Foundation fork, BSD-licensed) is wire-compatible for most workloads and works with the same Compose layout. Replace the image and the binary names (valkey-server, valkey-cli).
| Option | Image example | License | Notes |
|---|---|---|---|
| Redis 8 | redis:8-alpine | RSALv2 / SSPLv1 / AGPLv3 | Includes JSON, time series, vector sets (beta) |
| Valkey | valkey/valkey:8-alpine | BSD-3-Clause | Drop-in for most Redis workloads |
Pin your image.
8-alpinemoves as new minor versions ship. For production, pull the image once, then pin by digest:docker image inspect redis:8-alpine --format '{{index .RepoDigests 0}}'. Scan it before use with the workflow in our Trivy image scanning guide.
2. Architecture overview
Docker host (Rocky Linux 10 / Ubuntu)
+--------------------------------------------------------------+
| |
| 127.0.0.1:6379 (optional, admin access only) |
| | |
| +-----v------------------- backend network ------------+ |
| | | |
| | +-----------+ redis://appuser@redis:6379 +------+---+ |
| | | app | ---------------------------> | redis | |
| | | container | | (ACL on) | |
| | +-----------+ +----+-----+ |
| | | |
| +--------------------------------------------------+-------+
| | |
| named volume: redis-data | | (/data: AOF + RDB) | +--------------------------------------------------------------+
The application reaches Redis by service name over a private Compose network. The host only sees Redis through a port bound to 127.0.0.1, or not at all if you remove the ports: entry.
3. Project layout
mkdir -p ~/redis-stack/redis && cd ~/redis-stack
touch docker-compose.yml redis/redis.conf redis/users.acl
tree
# .
# โโโ docker-compose.yml
# โโโ redis
# โโโ redis.conf
# โโโ users.acl
4. Step 1: Create ACL users
Redis ACLs let you disable the all-powerful default user and give each client only what it needs. We will create three users:
| User | Purpose | Permissions |
|---|---|---|
admin | You, for maintenance | Everything |
appuser | Your application | Keys prefixed app:, no dangerous commands |
healthcheck | Docker healthcheck | PING only |
default | Built-in | Disabled |
Generate strong passwords and their SHA-256 hashes. Redis ACL files can store hashes (prefixed with #), so the plaintext never needs to sit in the file.
# Generate two random passwords and keep them in your password manager
ADMIN_PASS=$(openssl rand -base64 32)
APP_PASS=$(openssl rand -base64 32)
# printf (not echo) avoids hashing a trailing newline
ADMIN_HASH=$(printf '%s' "$ADMIN_PASS" | sha256sum | awk '{print $1}')
APP_HASH=$(printf '%s' "$APP_PASS" | sha256sum | awk '{print $1}')
cat > redis/users.acl <<EOF
user default off
user admin on #${ADMIN_HASH} ~* &* +@all
user appuser on #${APP_HASH} ~app:* &* +@all -@dangerous
user healthcheck on nopass -@all +ping
EOF
# The container's redis user (UID 999) must be able to read the file
sudo chown 999:999 redis/users.acl
sudo chmod 400 redis/users.acl
echo "admin password: $ADMIN_PASS"
echo "appuser password: $APP_PASS"
Rule order matters in ACL lines. +@all -@dangerous first allows everything and then removes the dangerous category (which includes KEYS, FLUSHALL, CONFIG, and similar). Reversing the order would re-allow them.
Why not
requirepass?requirepasssets one shared password on thedefaultuser. ACLs give you per-client credentials, key-pattern limits, and the ability to revoke one client without rotating everything. Do not mixuserlines inredis.confwith anaclfile; Redis expects one or the other.
5. Step 2: Write redis.conf
# redis/redis.conf
bind 0.0.0.0
port 6379
protected-mode yes
# Authentication: ACL file only (default user is disabled there)
aclfile /usr/local/etc/redis/users.acl
# Persistence
dir /data
appendonly yes
appenddirname "appendonlydir"
appendfsync everysec
save 900 1
save 300 10
save 60 10000
# Memory: keep below the container limit to leave room for fork and AOF rewrite
maxmemory 512mb
maxmemory-policy noeviction
# Housekeeping
tcp-keepalive 300
loglevel notice
bind 0.0.0.0 is safe here because it applies inside the container. What the outside world can reach is decided by the Compose ports: section and the Docker network, not by this line.
Choosing maxmemory-policy:
| Policy | Use when |
|---|---|
noeviction | Redis holds data you cannot lose (queues, sessions, job state). Writes fail when full instead of silently dropping keys. |
allkeys-lru | Redis is a pure cache. Old keys are evicted automatically. |
volatile-lru | Mixed use: only keys with a TTL can be evicted. |
6. Step 3: Write docker-compose.yml
# docker-compose.yml
name: redis-stack
services:
redis:
image: redis:8-alpine # pin by digest for production
container_name: redis
restart: unless-stopped
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
# Publish to the HOST loopback only. Remove this block if only containers need Redis.
ports:
- "127.0.0.1:6379:6379"
volumes:
- redis-data:/data
# Add :Z on SELinux hosts (Rocky Linux, RHEL, Fedora) for bind mounts
- ./redis/redis.conf:/usr/local/etc/redis/redis.conf:ro,Z
- ./redis/users.acl:/usr/local/etc/redis/users.acl:ro,Z
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "redis-cli --user healthcheck -a x --no-auth-warning ping | grep -q PONG"]
interval: 10s
timeout: 3s
retries: 5
start_period: 10s
# Container hardening
security_opt:
- no-new-privileges:true
read_only: true
cap_drop:
- ALL
cap_add: # needed by the image entrypoint to chown /data and drop to the redis user
- CHOWN
- SETUID
- SETGID
# Resource limits (keep maxmemory in redis.conf well below mem_limit)
mem_limit: 768m
cpus: 1.0
pids_limit: 100
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
networks:
backend:
name: redis-backend
volumes:
redis-data:
What the hardening flags do:
| Setting | Effect |
|---|---|
no-new-privileges | Blocks privilege escalation through setuid binaries inside the container |
read_only: true | Root filesystem is immutable; only the /data volume is writable |
cap_drop: ALL + minimal cap_add | Removes Linux capabilities Redis never needs |
mem_limit / pids_limit | Limits blast radius of a runaway process |
127.0.0.1: port prefix | Prevents the published port from listening on every host interface |
If the container will not start after you add hardening flags, remove them one at a time and check
docker compose logs redis. Image entrypoints change between releases, so verify this combination against the image tag you pin.
7. Step 4: Start and verify
docker compose up -d
docker compose ps
You should see the container as healthy after about ten seconds.
Export the admin password for redis-cli instead of typing it on the command line, where it would land in your shell history and process list:
export REDISCLI_AUTH='paste-the-admin-password-here'
docker compose exec -e REDISCLI_AUTH redis redis-cli --user admin ACL WHOAMI
# "admin"
docker compose exec -e REDISCLI_AUTH redis redis-cli --user admin ACL LIST
Confirm the default user is locked out:
docker compose exec redis redis-cli PING
# (error) NOAUTH Authentication required.
Confirm persistence survives a restart:
docker compose exec -e REDISCLI_AUTH redis redis-cli --user admin SET app:test "hello"
docker compose restart redis
docker compose exec -e REDISCLI_AUTH redis redis-cli --user admin GET app:test
# "hello"
docker compose exec redis ls -la /data /data/appendonlydir
Confirm the port is bound to loopback only:
ss -ltnp | grep 6379
# LISTEN 0 4096 127.0.0.1:6379 ...
8. Step 5: Connect an application container
Add your application to the same network and give it the appuser credentials through an environment file that is not committed to Git.
# addition to docker-compose.yml
app:
image: your-app:latest
depends_on:
redis:
condition: service_healthy
environment:
REDIS_URL: redis://appuser:${APP_REDIS_PASSWORD}@redis:6379/0
networks:
- backend
# .env (add to .gitignore, chmod 600)
APP_REDIS_PASSWORD=paste-the-appuser-password-here
If the password contains characters such as /, +, or = (common in Base64), URL-encode it in the connection string or generate hex passwords instead: openssl rand -hex 32.
Remember that appuser can only touch keys that start with app:. Name your application’s keys accordingly, or widen the key pattern in users.acl and reload with ACL LOAD.
For Python or Node clients, pass the username explicitly. Libraries that only accept a password will authenticate as default, which is disabled in this setup.
9. Persistence: RDB vs AOF
| RDB snapshots | AOF (append-only file) | |
|---|---|---|
| How it works | Point-in-time dump at intervals | Logs every write command |
| Data loss on crash | Up to minutes of writes | About 1 second with appendfsync everysec |
| Restart speed | Fast | Slower on large datasets |
| File size | Compact | Larger, rewritten in the background |
| Best for | Backups, fast restore | Durability |
This guide enables both. On restart Redis loads the AOF because it is the more complete record, and the RDB file remains useful as a compact backup artifact.
10. Backup and restore
Create a backup:
# 1. Trigger a snapshot and wait for it to finish
docker compose exec -e REDISCLI_AUTH redis redis-cli --user admin BGSAVE
sleep 2
docker compose exec -e REDISCLI_AUTH redis redis-cli --user admin INFO persistence | grep rdb_bgsave_in_progress
# rdb_bgsave_in_progress:0
# 2. Archive the data volume
docker volume ls | grep redis-data # note the exact volume name
mkdir -p backups
docker run --rm \
-v redis-stack_redis-data:/data:ro \
-v "$PWD/backups":/backup \
alpine tar czf /backup/redis-$(date +%F).tgz -C /data .
Restore:
docker compose down
docker run --rm \
-v redis-stack_redis-data:/data \
-v "$PWD/backups":/backup \
alpine sh -c "rm -rf /data/* && tar xzf /backup/redis-2026-11-02.tgz -C /data"
docker compose up -d
Test a restore on a scratch host before you need it. A backup you have never restored is only a hope.
11. Security hardening checklist
| Check | Why it matters |
|---|---|
default user disabled, per-app ACL users | A leaked app credential cannot run FLUSHALL or CONFIG |
Port published as 127.0.0.1:6379:6379 or not at all | Docker adds its own firewall rules, so published ports can bypass firewalld or ufw rules you wrote for the host |
| Private Compose network for app-to-Redis traffic | No exposure beyond containers that need access |
| Image pinned by digest and scanned | Reproducible builds and known-CVE visibility |
read_only, cap_drop: ALL, no-new-privileges | Smaller attack surface if Redis is compromised |
.env and users.acl not in Git, chmod 400/600 | Credentials stay out of version control |
maxmemory set | Prevents Redis from consuming all host RAM |
| TLS enabled if traffic crosses hosts | ACL passwords are sent in clear text without TLS |
| Regular restore tests | Verifies your backups actually work |
If Redis must be reachable from another machine, do not simply publish 6379 to the world. Put a VPN or private network in front, enable Redis TLS (tls-port, tls-cert-file, tls-key-file, tls-ca-cert-file), and restrict source addresses. You can then run the host audit described in our Docker Bench for Security guide.
On SELinux hosts, bind mounts need the :Z flag, covered in Docker on Rocky Linux 10 with SELinux.
12. Host tuning for Redis
Redis prints warnings on startup if the host is not tuned. These are kernel settings on the host, not inside the container.
# Allow fork() for background saves under memory pressure
echo 'vm.overcommit_memory = 1' | sudo tee /etc/sysctl.d/99-redis.conf
sudo sysctl --system
# Check transparent huge pages (Redis recommends not "always")
cat /sys/kernel/mm/transparent_hugepage/enabled
To change THP persistently, use a tuned profile or a small systemd unit, depending on your distribution.
13. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
DENIED Redis is running in protected mode | No password set and connection is non-loopback | Use the ACL setup above; do not disable protected mode |
NOAUTH Authentication required | Client did not authenticate, and default is off | Pass --user and the password; configure your library with a username |
WRONGPASS invalid username-password pair | Hash mismatch, often from echo adding a newline | Regenerate the hash with printf '%s' |
Error loading ACLs in logs | Syntax error in users.acl | Check each line starts with user, hashes start with # and are 64 hex characters |
Container unhealthy but Redis runs | healthcheck user missing from ACL | Add the healthcheck line and restart |
Can't open the append only file: Permission denied | Volume or bind mount owned by the wrong user, or SELinux denial | Fix ownership of /data; add :Z to bind mounts on SELinux hosts |
MISCONF Redis is configured to save RDB snapshots... | Disk full, permission problem, or fork failure | Check docker compose logs redis and free space |
Can't save in background: fork: Cannot allocate memory | Host overcommit disabled | Set vm.overcommit_memory = 1 on the host |
OOM command not allowed when used memory > 'maxmemory' | noeviction and memory full | Raise maxmemory, delete data, or use an eviction policy for cache workloads |
| App cannot connect | Wrong hostname or different network | Use the service name redis and attach both services to the same network |
| Redis reachable from other machines | Port published on all interfaces | Prefix with 127.0.0.1: or remove ports: |
14. FAQ
Should I expose port 6379 publicly?
No. Keep Redis on a private network. If remote access is required, use a VPN or an SSH tunnel, and enable TLS.
Redis 8 or Valkey?
Both work with this layout. Choose Valkey if your organization avoids AGPL or source-available licenses. Choose Redis 8 if you want the integrated JSON, time series, and query features.
Named volume or bind mount for /data?
A named volume avoids host permission headaches. Use a bind mount if you must place data on a specific disk, and add the correct ownership and the :Z flag on SELinux systems.
Is appendfsync everysec safe enough?
It risks roughly one second of writes in a crash, which suits most applications. Use always only if you can accept the throughput cost.
Can I use this for n8n queue mode?
Yes. n8n’s queue mode needs Redis and PostgreSQL; see n8n Queue Mode: Scale Workflows with Redis, PostgreSQL & Workers.
Do I need Redis on the host instead of Docker?
Not necessarily. For a native installation with systemd and SELinux, see How to Install Redis on Rocky Linux 10.







