Open this post in EchoOS →

Chapter 1.7 - Redis in Docker: Persistence, Memory Limits and Safe Upgrades

Redis is the Swiss-army knife next to most applications: a cache, a session store, a rate limiter, a job queue. It’s also deceptively easy to run badly. The defaults are tuned for a developer laptop, and a Redis instance exposed to the internet without a password is still one of the most commonly exploited services.

This chapter sets it up properly: a config file you understand, data on a volume, memory that can’t run away, and an upgrade path.

First, decide what this Redis is for

Two settings follow from that one decision: persistence and what happens when memory fills up.

Use Persistence When memory is full
Cache (data can be rebuilt from the database) none, or snapshots only evict old keys: allkeys-lru
Sessions, rate limits (losing them is annoying, not fatal) snapshots (RDB) volatile-lru, evicting keys that have a TTL
Queues, source-of-truth data append-only file (AOF), ideally plus RDB noeviction: reject writes rather than silently lose data

The two persistence mechanisms:

Quick start for development

docker run -d --name redis-dev -p 127.0.0.1:6379:6379 redis:8.0
docker exec -it redis-dev redis-cli ping      # PONG

Fine on a laptop, and deliberately bound to 127.0.0.1. Everything below is about running it for real.

A production configuration

The official image ships without a config file (Redis runs on built-in defaults), so download the annotated example for your version and keep it next to your deployment:

mkdir -p ~/redis/conf
curl -fsSL -o ~/redis/conf/redis.conf \
  https://raw.githubusercontent.com/redis/redis/8.0/redis.conf

It’s long and heavily commented, which makes it good documentation. These are the lines to change (they already exist in the file; edit them in place rather than appending duplicates):

# --- Network ---------------------------------------------------------------
bind 0.0.0.0 -::
protected-mode yes
port 6379

# --- Auth ------------------------------------------------------------------
requirepass use-a-long-random-password

# --- Persistence -----------------------------------------------------------
dir /data
appendonly yes
appendfsync everysec
save 3600 1 300 100 60 10000

# --- Memory ----------------------------------------------------------------
maxmemory 512mb
maxmemory-policy noeviction

Section by section:

Running it

docker network create backend

docker run -d --name redis \
  --network backend \
  --restart unless-stopped \
  --memory 768m \
  -p 127.0.0.1:6379:6379 \
  -v redis_data:/data \
  -v ~/redis/conf/redis.conf:/usr/local/etc/redis/redis.conf:ro \
  redis:8.0 redis-server /usr/local/etc/redis/redis.conf

Check it (the examples from here on assume the password is in a shell variable, for example read -rs REDIS_PASSWORD, which reads it without echoing):

docker exec -it -e REDISCLI_AUTH="$REDIS_PASSWORD" redis redis-cli
127.0.0.1:6379> INFO persistence
127.0.0.1:6379> CONFIG GET maxmemory*

REDISCLI_AUTH passes the password through the environment instead of -a, which would show it in the process list and prints a warning.

Most settings can be changed live (CONFIG SET maxmemory 1gb) and written back to the file with CONFIG REWRITE. With the file mounted read-only, that write fails on purpose: edit the host file and docker restart redis instead, so the file on the host stays the source of truth.

Backups

Everything Redis persists is in the volume: dump.rdb and, with AOF on, the appendonlydir/ directory (Redis 7+ splits the AOF into several files plus a manifest). To take a consistent copy:

docker exec -e REDISCLI_AUTH="$REDIS_PASSWORD" redis redis-cli BGSAVE
docker exec -e REDISCLI_AUTH="$REDIS_PASSWORD" redis redis-cli LASTSAVE      # repeat until the timestamp changes
docker run --rm -v redis_data:/data -v "$PWD":/backup alpine \
  tar czf "/backup/redis-$(date +%F).tgz" -C /data .

Upgrading

Newer Redis versions read older RDB and AOF files, but not the other way round. So the plan is: keep the old container around until the new one has proven itself, and have a backup for the moment you can’t go back.

flowchart LR
  subgraph prep["Prepare"]
    direction TB
    A[Back up /data] --> B[docker pull redis:8.0.3]
    B --> C[docker stop redis]
    C --> D[docker rename redis redis-old]
    D --> E[Compare redis.conf with the<br/>new version's example file]
  end
  subgraph swap["Swap and verify"]
    direction TB
    F[docker run new container<br/>same volume and config] --> G{Version, DBSIZE<br/>and app checks OK?}
    G -- yes --> H[docker rm redis-old]
    G -- no --> I[Stop the new container, restore<br/>the backup into the volume]
    I --> J[docker start redis-old]
  end
  prep --> swap

Checks after the switch:

docker exec -e REDISCLI_AUTH="$REDIS_PASSWORD" redis redis-cli INFO server | grep redis_version
docker exec -e REDISCLI_AUTH="$REDIS_PASSWORD" redis redis-cli DBSIZE

Compare DBSIZE with the number before the upgrade. Resist KEYS * on a real instance: it walks the entire keyspace in one blocking call and freezes every client until it finishes. SCAN gives you a sample without blocking.

Before a major version jump, diff your redis.conf against the new version’s example file: directives do get renamed or removed, and Redis refuses to start on an unknown directive, which is at least an easy failure to spot.

A note on licensing

Redis changed its license in 2024 (to RSALv2/SSPL) and added AGPLv3 as an option with Redis 8. For most teams running it internally this changes nothing, but if you offer Redis as a service or redistribute it, check the terms. Valkey, a Linux Foundation fork of Redis 7.2, remains BSD-licensed, is a drop-in replacement for the commands above (valkey/valkey image, valkey-cli), and is what several cloud providers now offer.

Checklist

Chapter 1.8 looks at what has been connecting all these containers: Docker’s network modes.