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) andredis: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, nomaxmemory - 10,000 small keys written with
redis-cli --pipe, then one Docker action, thenDBSIZE
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
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
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
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
/datain every Compose file, even for a "cache". Without one,redis:8loses its data on any recreate, including an image upgrade.
services:
redis:
image: redis:8
volumes: [redis-data:/data]
volumes:
redis-data: {}
-
docker stopis safe anddocker killisn't. If you can't afford to lose the last minute, turn onappendonly 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
maxmemorywith real headroom. Onredis: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)
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.
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?
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.