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

  1. What you are deploying (Redis 8, licensing, Valkey)
  2. Architecture overview
  3. Project layout
  4. Step 1: Create ACL users
  5. Step 2: Write redis.conf
  6. Step 3: Write docker-compose.yml
  7. Step 4: Start and verify
  8. Step 5: Connect an application container
  9. Persistence: RDB vs AOF
  10. Backup and restore
  11. Security hardening checklist
  12. Host tuning for Redis
  13. Troubleshooting
  14. FAQ
  15. 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).

OptionImage exampleLicenseNotes
Redis 8redis:8-alpineRSALv2 / SSPLv1 / AGPLv3Includes JSON, time series, vector sets (beta)
Valkeyvalkey/valkey:8-alpineBSD-3-ClauseDrop-in for most Redis workloads

Pin your image. 8-alpine moves 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:

UserPurposePermissions
adminYou, for maintenanceEverything
appuserYour applicationKeys prefixed app:, no dangerous commands
healthcheckDocker healthcheckPING only
defaultBuilt-inDisabled

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? requirepass sets one shared password on the default user. ACLs give you per-client credentials, key-pattern limits, and the ability to revoke one client without rotating everything. Do not mix user lines in redis.conf with an aclfile; 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:

PolicyUse when
noevictionRedis holds data you cannot lose (queues, sessions, job state). Writes fail when full instead of silently dropping keys.
allkeys-lruRedis is a pure cache. Old keys are evicted automatically.
volatile-lruMixed 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:

SettingEffect
no-new-privilegesBlocks privilege escalation through setuid binaries inside the container
read_only: trueRoot filesystem is immutable; only the /data volume is writable
cap_drop: ALL + minimal cap_addRemoves Linux capabilities Redis never needs
mem_limit / pids_limitLimits blast radius of a runaway process
127.0.0.1: port prefixPrevents 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 snapshotsAOF (append-only file)
How it worksPoint-in-time dump at intervalsLogs every write command
Data loss on crashUp to minutes of writesAbout 1 second with appendfsync everysec
Restart speedFastSlower on large datasets
File sizeCompactLarger, rewritten in the background
Best forBackups, fast restoreDurability

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

CheckWhy it matters
default user disabled, per-app ACL usersA leaked app credential cannot run FLUSHALL or CONFIG
Port published as 127.0.0.1:6379:6379 or not at allDocker 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 trafficNo exposure beyond containers that need access
Image pinned by digest and scannedReproducible builds and known-CVE visibility
read_only, cap_drop: ALL, no-new-privilegesSmaller attack surface if Redis is compromised
.env and users.acl not in Git, chmod 400/600Credentials stay out of version control
maxmemory setPrevents Redis from consuming all host RAM
TLS enabled if traffic crosses hostsACL passwords are sent in clear text without TLS
Regular restore testsVerifies 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

SymptomLikely causeFix
DENIED Redis is running in protected modeNo password set and connection is non-loopbackUse the ACL setup above; do not disable protected mode
NOAUTH Authentication requiredClient did not authenticate, and default is offPass --user and the password; configure your library with a username
WRONGPASS invalid username-password pairHash mismatch, often from echo adding a newlineRegenerate the hash with printf '%s'
Error loading ACLs in logsSyntax error in users.aclCheck each line starts with user, hashes start with # and are 64 hex characters
Container unhealthy but Redis runshealthcheck user missing from ACLAdd the healthcheck line and restart
Can't open the append only file: Permission deniedVolume or bind mount owned by the wrong user, or SELinux denialFix 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 failureCheck docker compose logs redis and free space
Can't save in background: fork: Cannot allocate memoryHost overcommit disabledSet vm.overcommit_memory = 1 on the host
OOM command not allowed when used memory > 'maxmemory'noeviction and memory fullRaise maxmemory, delete data, or use an eviction policy for cache workloads
App cannot connectWrong hostname or different networkUse the service name redis and attach both services to the same network
Redis reachable from other machinesPort published on all interfacesPrefix 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.

(Visited 1 times, 1 visits today)

You may also like