DEV Community

Remdore
Remdore

Posted on AI-assisted

What happens to your Redis data in Docker

I wrote 10,000 keys into Redis running in Docker, then ran one ordinary Docker command and counted what was left. I did that for every command I could think of that people run on a Redis container: restart, stop, kill, rm, Compose down and up, an image upgrade. Then I filled Redis up inside a memory-limited container to see what maxmemory actually protects you from.

Most of it went the way the docs say. Three things didn't: the current redis:8 image no longer declares a volume, so moving from redis:7 to redis:8 in Compose can start you on an empty database. maxmemory set at three quarters of the container limit still got Redis OOM-killed. And a restart policy hides that kill completely.

The setup

  • Docker Engine 29.8.2 in Docker Desktop on an ARM Mac, Docker Compose v5.5.1
  • redis:8 (Redis 8.10.2, built 6 October) and redis:7-alpine (Redis 7.4.8)
  • default configuration unless I say otherwise: RDB snapshots on the stock save rules 3600 1 300 100 60 10000, no AOF, no maxmemory
  • 10,000 small keys written with redis-cli --pipe, then one Docker action, then DBSIZE

I ran the plain Docker scenarios and the redis:8 Compose scenarios twice and got identical results both times. The redis:7 Compose column ran once, and the 7-to-8 upgrade twice.

What survives which command

Command after writing 10,000 keys Keys left
docker restart 10,000
docker stop, docker start 10,000
docker kill, docker start, right after writing 0
docker kill, docker start, 65 s after writing 10,000
docker rm -f, docker run, no volume 0
docker stop, docker rm, docker run, no volume 0
named volume: stop, rm, run 10,000
named volume: rm -f, run 0
appendonly yes: kill, start 10,000

docker stop sends SIGTERM, and Redis writes a final snapshot when it shuts down. That's why stop and restart lose nothing even though nothing had been saved yet. docker kill sends SIGKILL, so there's no final snapshot. You keep whatever the last scheduled snapshot wrote. Ten thousand changes trigger the 60 10000 rule, so after 65 seconds the snapshot existed and everything survived. Killed right after the writes, the database came back empty.

The named-volume rm -f row is the same thing. rm -f kills the container, so the volume kept whatever was there before the writes, which was nothing.

AOF and kill

With appendonly yes, I set one more key and killed the container in the same shell line. It came back with all 10,001 keys.

appendfsync everysec sounds like "you can lose up to a second", but that second only applies when the machine itself dies. Redis had already handed the write to the kernel. Killing the process doesn't throw away what the kernel holds, so a container kill isn't a power cut. I didn't test a host crash.

The volume that isn't there any more

The redis:8 image I pulled has no VOLUME in its config:

redis:7-alpine     Volumes: {'/data': {}}
redis:alpine       Volumes: {'/data': {}}
redis:8.10-alpine  Volumes: None
redis:8            Volumes: None
Enter fullscreen mode Exit fullscreen mode

The working directory is still /data and the snapshot still goes to /data/dump.rdb. But without a declared volume, Docker gives the container no anonymous volume, so the file lives in the container's own writable layer. When the container goes, the data goes with it.

With redis:7, Docker Compose quietly made up for a missing volume in your compose file. It reuses a container's anonymous volumes when it recreates it, so data survived things you might expect to lose it:

Compose, no volume in the file redis:7-alpine redis:8
up -d --force-recreate kept gone
image tag changed, up -d kept gone
down, up -d gone gone

restart and stop/start kept the data on redis:8. With a named volume, down/up and an image change kept it too, and down -v deleted it, as documented.

The case that matters is the upgrade. I started a Compose service on redis:7-alpine with no volume in the file, wrote 10,000 keys, stopped it, changed the image to redis:8 and ran docker compose up -d. Redis came up with 0 keys and no mounts. I did it twice, with the same result.

The data wasn't deleted. The old anonymous volume was still there, dangling, with dump.rdb in it. Mounting it into a redis:8 container brought back all 10,000 keys:

docker volume ls -qf dangling=true        # find it; check with: docker run --rm -v <id>:/d redis:8 ls /d
docker run -d --name recovered -v <id>:/data redis:8
Enter fullscreen mode Exit fullscreen mode

So if this happens to you, don't run docker volume prune.

maxmemory inside a memory-limited container

Next I ran Redis with --memory 64m and wrote 1 KB values in batches of 2,000 until something happened. The writer ran in a separate container on the same Docker network, so its memory didn't count against Redis's limit (more on that below).

Image and settings, 64 MB container Result
redis:8, no maxmemory OOM-killed, exit 137, about 48,000 keys in
redis:8, maxmemory 48mb refused writes at the limit, then OOM-killed seconds later
redis:8, maxmemory 48mb, snapshots off same, OOM-killed
redis:7-alpine, maxmemory 48mb refused writes, stayed up with 45,156 keys
redis:8, maxmemory 32mb refused writes, stayed up with 29,929 keys
redis:8, maxmemory 32mb, allkeys-lru evicted, stayed up with 29,810 keys

maxmemory limits Redis's dataset, used_memory. The container limit counts the whole process. On an empty server those two numbers are already far apart:

Image used_memory, empty process RSS, empty modules loaded
redis:8 1.39 MB 27.9 MB 5
redis:7-alpine 0.97 MB 13.7 MB 0

The Redis 8 image starts with five modules loaded: search, ReJSON, timeseries, bf and vectorset. An empty redis:8 uses about 14 MB more RSS than an empty redis:7-alpine. I'd guess most of that is the modules, but I didn't load them one at a time to check. With a 48 MB dataset on top, the process ran past 64 MB. The same setting that worked on Redis 7 got Redis 8 killed. At 32 MB there was enough room.

I first guessed the background snapshot fork was the cause, since copy-on-write can double memory. The run with snapshots turned off died the same way, so it wasn't.

A restart policy hides the kill

The same 64 MB container with no maxmemory, plus --restart unless-stopped:

running oom=false exit=0 restarts=1 dbsize=0
Enter fullscreen mode Exit fullscreen mode

Docker restarted it after the OOM kill, so docker ps showed it up and healthy. It came back with an empty database, because nothing had been snapshotted yet. The only clue was RestartCount. If something watches your cache, have it watch that number, or uptime_in_seconds from INFO.

What I got wrong

I assumed the official image still declared VOLUME /data. It had for years. I found out only because a scenario I expected to keep its data didn't.

My first memory runs used docker exec ... redis-cli --pipe to write. That runs the client inside the Redis container, so its buffers counted against the 64 MB too. With that setup, redis:7-alpine with maxmemory 48mb was OOM-killed. With the client moved to its own container it survived. Every number in the memory table comes from the second setup.

I blamed the snapshot fork for the OOM kills before testing it, as above.

docker stats and Redis's own numbers disagreed badly. For an idle container it showed 12 MiB for redis:8 and 42 MiB for redis:7-alpine, the reverse of what RSS says. I used Redis's own INFO memory and the container's OOMKilled flag instead.

Caveats

One machine, Docker Desktop's Linux VM on an ARM Mac. The exact OOM points depend on the allocator, the kernel and the image build, so treat the 48 and 32 MB thresholds as this setup's numbers, not rules. Tags move: redis:8 today is 8.10.2, and a later build could declare the volume again. I tested keys of a few bytes and values of 1 KB, not large values or big data structures. I didn't test a host crash, Docker Swarm or Kubernetes.

What I would take from it

  • Put a named volume on /data in every Compose file, even for a "cache". Without one, redis:8 loses its data on any recreate, including an image upgrade.
services:
  redis:
    image: redis:8
    volumes: [redis-data:/data]
volumes:
  redis-data: {}
Enter fullscreen mode Exit fullscreen mode
  • docker stop is safe and docker kill isn't. If you can't afford to lose the last minute, turn on appendonly yes. It survived a kill here.
  • If data vanished after an upgrade, look for a dangling volume before you prune anything.
  • Under a container memory limit, set maxmemory with real headroom. On redis:8, half the limit worked and three quarters didn't.
  • Alert on restarts, not just on "is it running". A Redis that was OOM-killed and restarted looks healthy and empty.

Top comments (3)

Collapse
 
kashif_manzer profile image
Kashif Manzer •

The maxmemory finding deserves a sticky note on every monitor. Redis's maxmemory only caps the dataset, but the container limit caps the whole process, so once the bundled modules and fork buffers pile on, three quarters can already be over the edge. And the missing VOLUME declaration is a strong argument for pinning image digests in production instead of floating tags like redis:8, because an upstream rebuild can silently change where your data lives. Great piece of testing, the 7 to 8 upgrade scenario alone will save someone a very bad day.

Collapse
 
mno_tao_236ab4649edf4cf9f profile image
alex.toolkit •

The redis:7 to redis:8 Compose row is the one that would slip through review: the only change in the diff is the image tag, so nothing looks risky. Good call adding the dangling-volume recovery step and the warning about volume prune.

Did Compose print anything during that up -d, like a note about the old anonymous volume, or was it completely silent?

Collapse
 
rushdeploy profile image
Rishabh Mishra •

The row that earned this post its clicks is the 7 to 8 upgrade, since an image bump should never be the thing that loses data. One addition worth making: docker events --filter event=oom exposes the kill directly, so you can alert on the OOM itself instead of noticing the empty database later. Solid empirical work.