Contents
Redis with Docker Tutorial: Set Passwords, Persist Data, and Connect
Redis is an in-memory key-value database commonly used for caching, session storage, counters, and queues. You can run it with Docker in seconds.
Run it
$ docker run -d --name redis -p 127.0.0.1:6379:6379 redis:8-alpine
Unable to find image 'redis:8-alpine' locally
8-alpine: Pulling from library/redis
Digest: sha256:3811787313eba226a2ef38658c6ccb91cd5e110edc89c37767de373120a0e5a0
Status: Downloaded newer image for redis:8-alpine
6fbb2effcf1bc874eda1064575153984b6223fd6de15ac2452eae0bbf71a74f1
$ docker exec redis redis-cli PING
PONG
$ docker exec redis redis-cli SET greeting hello
OK
$ docker exec redis redis-cli GET greeting
hello
$ docker exec redis redis-cli SET session:abc "user42" EX 60
OK
$ docker exec redis redis-cli TTL session:abc
60
$ docker exec redis redis-server --version
Redis server v=8.10.2 sha=00000000:1 malloc=jemalloc-5.3.0 bits=64 build=6583f6419e33bdeb
$ docker rm -f redis
redis
-p 127.0.0.1:6379:6379: expose the port only on your local machine.redis-cliis included in the container, so you can run commands directly.SET ... EX 60makes the key expire after 60 seconds. UseTTLto check the remaining time. This is commonly used for sessions and caches.
For interactive use, run docker exec -it redis redis-cli. You’ll see the 127.0.0.1:6379> prompt. Enter commands and press Enter; type exit to quit.
Set a password
Redis has no password by default. If its port is exposed, anyone can read and write data, and may even use it to break into the host. So:
- If you don’t need external connections, don’t use
-p. If you do, bind only to127.0.0.1. - Set a password:
$ docker run -d --name redis redis:8-alpine redis-server --requirepass "change-me"
8dbd1f84d2541f264d93555142cb65f6497e9635111ff4f9544258516e06a0e2
$ docker exec redis redis-cli GET greeting
NOAUTH Authentication required.
$ docker exec redis redis-cli -a "change-me" PING
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
PONG
$ docker exec redis redis-cli -a "change-me" --no-auth-warning PING
PONG
$ docker exec -e REDISCLI_AUTH=change-me redis redis-cli PING
PONG
$ docker rm -f redis
redis
- Without a password, you get
NOAUTH Authentication required.. - You can pass a password with
-a, but you’ll see a warning because the password appears in the process list.--no-auth-warningonly hides the warning. - A better approach is to pass the password through the
REDISCLI_AUTHenvironment variable.redis-cliuses it automatically, without a warning.
Application connection string (leave the username blank):
redis://:change-me@redis:6379/0
The final /0 is the database number (0–15).
Persist data
Redis stores data in memory. You can persist it to disk in two ways: RDB snapshots and AOF logs. The official image enables only RDB by default, and stores its files in the container’s /data, so mount a Volume there.
Test: RDB doesn’t save data immediately
$ docker run -d --name redis -v redis_data:/data redis:8-alpine
882983e53f8e000fa65faaf8d71bbcbeb57192401b1854621172ce2df95c29a6
$ docker exec redis redis-cli SET visits 100
OK
$ docker rm -f redis
redis
$ docker run -d --name redis -v redis_data:/data redis:8-alpine
5ad1473152a6eafdfdb33df64f68355c73e454f10e6bc135688f7c51686bb059
$ docker exec redis redis-cli GET visits
$ docker exec redis redis-cli CONFIG GET save
save
3600 1 300 100 60 10000
Even though you mounted a Volume, visits is empty after you run docker rm -f and recreate the container. CONFIG GET save shows the default save conditions, 3600 1 300 100 60 10000, which mean:
| Condition | Meaning |
|---|---|
3600 1 | Save if at least 1 key changes within 1 hour |
300 100 | Save if at least 100 keys change within 5 minutes |
60 10000 | Save if at least 10,000 keys change within 1 minute |
If you change just 1 key, Redis waits an hour before saving. docker rm -f terminates Redis with SIGKILL, so it doesn’t have time to save.
Try a clean shutdown with docker stop:
$ docker exec redis redis-cli SET visits 100
OK
$ docker stop redis
redis
$ docker logs redis 2>&1 | tail -n 5
1:M 13 Oct 2026 00:57:09.620 * User requested shutdown...
1:M 13 Oct 2026 00:57:09.620 * Saving the final RDB snapshot before exiting.
1:M 13 Oct 2026 00:57:09.621 * BGSAVE done, 1 keys saved, 0 keys skipped, 104 bytes written.
1:M 13 Oct 2026 00:57:09.623 * DB saved on disk
1:M 13 Oct 2026 00:57:09.623 # Redis is now ready to exit, bye bye...
$ docker start redis
redis
$ docker exec redis redis-cli GET visits
100
$ docker exec redis ls -l /data
total 4
-rw------- 1 redis redis 104 Oct 13 00:57 dump.rdb
$ docker rm -f redis
redis
$ docker volume rm redis_data
redis_data
docker stop sends SIGTERM. Before exiting, Redis runs Saving the final RDB snapshot before exiting, which saves the data. So:
- Clean shutdowns (
docker stop,docker compose down, or shutting down the host normally) don’t lose data. - Crashes, power outages,
docker rm -f, or termination by the system due to low memory will lose data written since the last snapshot.
AOF: record every write
For more reliable persistence, enable AOF (Append Only File). It records every write command:
$ docker run -d --name redis -v redis_data:/data redis:8-alpine redis-server --appendonly yes
aaac5c67a222018db886e0f3a3306841ec4bba7e8ae0b4b4eca71dc5cede91c9
$ docker exec redis redis-cli SET visits 41
OK
$ docker exec redis redis-cli INCR visits
42
$ docker rm -f redis
redis
$ docker run -d --name redis -v redis_data:/data redis:8-alpine redis-server --appendonly yes
1c880b5c2e8ef54b65baaa1fd99b1b9c976eb42f2d9c52f1eb9188eaeda955e4
$ docker exec redis redis-cli GET visits
42
$ docker exec redis ls /data/appendonlydir
appendonly.aof.1.base.rdb
appendonly.aof.1.incr.aof
appendonly.aof.manifest
$ docker rm -f redis
redis
$ docker volume rm redis_data
redis_data
Even after force-removing the container with docker rm -f, visits is still 42 after recreation. The AOF files are in /data/appendonlydir: base.rdb is the base snapshot, incr.aof records each subsequent write, and manifest lists the files.
| Method | Advantages | Disadvantages |
|---|---|---|
| RDB (default) | Small files, fast loading after restart | An unexpected shutdown can lose data written since the last snapshot |
| AOF | Writes to disk once per second by default, so at most about 1 second of data may be lost | Larger files, and more disk load when there are many writes |
| No persistence | Fastest | All data disappears after restart; suitable only for pure caching |
For a pure cache, you can disable persistence entirely: redis-server --save "" --appendonly no.
Use a config file and set a memory limit
Use a config file when you have many parameters. redis-demo/redis.conf:
requirepass change-me
appendonly yes
maxmemory 64mb
maxmemory-policy allkeys-lru
When using Redis as a cache, always set maxmemory. Otherwise, Redis keeps using memory until the host runs out. When it reaches the limit, Redis follows the maxmemory-policy:
| Policy | Behavior |
|---|---|
noeviction | Default; reject new writes and return an error |
allkeys-lru | Evict the least recently used keys from all keys; most common for caches |
volatile-lru | Evict only keys with an expiration set |
allkeys-lfu | Evict the least frequently used keys |
Test: write more data than the limit
Use Redis’s built-in redis-benchmark to write 300,000 entries, each 500 bytes (about 150 MB), far beyond the 64 MB limit:
$ docker run -d --name redis -v "$(pwd)/redis.conf:/usr/local/etc/redis/redis.conf:ro" -v redis_data:/data redis:8-alpine redis-server /usr/local/etc/redis/redis.conf
af187c21e0d6ecb0e9a0299200a6d866dc605d92c715e49df8a7abd850107040
$ docker exec -e REDISCLI_AUTH=change-me redis redis-cli CONFIG GET maxmemory*
maxmemory
67108864
maxmemory-clients
0
maxmemory-eviction-tenacity
10
maxmemory-samples
5
maxmemory-policy
allkeys-lru
$ docker exec redis redis-benchmark -a change-me -t set -n 300000 -r 300000 -d 500 -q
SET: 31043.05 requests per second, p50=0.903 msec
$ docker exec -e REDISCLI_AUTH=change-me redis redis-cli INFO memory | grep -E "^(used_memory_human|maxmemory_human|maxmemory_policy)"
used_memory_human:63.89M
maxmemory_human:64.00M
maxmemory_policy:allkeys-lru
$ docker exec -e REDISCLI_AUTH=change-me redis redis-cli INFO stats | grep evicted_keys
evicted_keys:95511
$ docker exec -e REDISCLI_AUTH=change-me redis redis-cli DBSIZE
115038
Windows equivalent (select a tab to switch):
docker run -d --name redis -v "$(pwd)/redis.conf:/usr/local/etc/redis/redis.conf:ro" -v redis_data:/data redis:8-alpine redis-server /usr/local/etc/redis/redis.conf
docker run -d --name redis -v "${PWD}/redis.conf:/usr/local/etc/redis/redis.conf:ro" -v redis_data:/data redis:8-alpine redis-server /usr/local/etc/redis/redis.conf
docker run -d --name redis -v "%cd%/redis.conf:/usr/local/etc/redis/redis.conf:ro" -v redis_data:/data redis:8-alpine redis-server /usr/local/etc/redis/redis.conf
CONFIG GET maxmemory*confirms the config file took effect:67108864bytes is 64 MB, and the policy isallkeys-lru.- After the write, memory stayed at
63.89M, below the limit. evicted_keys:95511: more than 90,000 keys were automatically evicted, leaving about 115,000.
Now try the same scenario with noeviction:
$ docker exec -e REDISCLI_AUTH=change-me redis redis-cli CONFIG SET maxmemory-policy noeviction
OK
$ docker exec -e REDISCLI_AUTH=change-me redis sh -c 'head -c 2000000 /dev/zero | tr "\\0" x | redis-cli -x SET big-value'
OOM command not allowed when used memory > 'maxmemory'.
$ docker exec -e REDISCLI_AUTH=change-me redis redis-cli GET greeting
Memory is full. Writing a 2 MB value returns OOM command not allowed when used memory > 'maxmemory', so the application will fail. Use allkeys-lru for pure caches. If Redis stores data you can’t lose, such as a queue, use noeviction and monitor memory usage.
Use an application with Compose
Build a Flask counter that increments visits in Redis with every request:
redis-demo/
├── compose.yaml
├── .env
└── app/
├── app.py
├── requirements.txt
└── Dockerfile
app/app.py:
import os
import redis
from flask import Flask
app = Flask(__name__)
r = redis.Redis.from_url(os.environ["REDIS_URL"], decode_responses=True)
@app.get("/")
def index():
visits = r.incr("visits")
return {"message": "Hello from Flask + Redis", "visits": visits}
app/requirements.txt:
flask==3.1.*
gunicorn==23.*
redis==6.*
app/Dockerfile (see Python application for an explanation):
FROM python:3.14-slim
ENV PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
USER nobody
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
.env:
REDIS_PASSWORD=change-this-password
compose.yaml:
services:
app:
build: ./app
ports:
- "8000:8000"
environment:
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
depends_on:
redis:
condition: service_healthy
redis:
image: redis:8-alpine
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}", "--appendonly", "yes"]
environment:
REDISCLI_AUTH: ${REDIS_PASSWORD}
volumes:
- redis_data:/data
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep -q PONG"]
interval: 5s
retries: 10
restart: unless-stopped
volumes:
redis_data:
| Setting | Description |
|---|---|
@redis:6379 in REDIS_URL | Connect using the service name |
${REDIS_PASSWORD} in command | Compose replaces this with the actual password from .env |
REDISCLI_AUTH | Makes redis-cli inside the container (including health checks) use the password automatically |
redis has no ports | Only app needs to connect, so the port isn’t exposed externally |
$ docker compose up -d --build
[+] Building 0.3s (12/12) FINISHED
=> [internal] load local bake definitions 0.0s
=> => reading from stdin 486B 0.0s
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 263B 0.0s
=> [internal] load metadata for docker.io/library/python:3.14-slim 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/5] FROM docker.io/library/python:3.14-slim@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170 0.0s
=> => resolve docker.io/library/python:3.14-slim@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 93B 0.0s
=> CACHED [2/5] WORKDIR /app 0.0s
=> CACHED [3/5] COPY requirements.txt . 0.0s
=> CACHED [4/5] RUN pip install -r requirements.txt 0.0s
=> CACHED [5/5] COPY . . 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => exporting manifest sha256:2cb1f769137117a44dde49bfb2f8551a8ce989fe8bd0a72696ba37987c29fd13 0.0s
=> => exporting config sha256:88373d2af6030b965f8b4e878d871e906738cd9194ec77bd007c6bbbace7c1b9 0.0s
=> => exporting attestation manifest sha256:c8f570c26323c3e08476544296d44ed0e159d27d9f25b86bf879cd08c6b6e186 0.0s
=> => exporting manifest list sha256:46bd2450b425a935481fb6ef8567c915f5543996f4e35018bdb955ecf858b361 0.0s
=> => naming to docker.io/library/redis-demo-app:latest 0.0s
=> => unpacking to docker.io/library/redis-demo-app:latest 0.0s
=> resolving provenance for metadata file 0.0s
[+] up 5/5
✔ Image redis-demo-app Built 0.4s
✔ Network redis-demo_default Created 0.0s
✔ Volume redis-demo_redis_data Created 0.0s
✔ Container redis-demo-redis-1 Healthy 5.8s
✔ Container redis-demo-app-1 Started 5.9s
$ curl -s http://localhost:8000/
{"message":"Hello from Flask + Redis","visits":1}
$ curl -s http://localhost:8000/
{"message":"Hello from Flask + Redis","visits":2}
$ curl -s http://localhost:8000/
{"message":"Hello from Flask + Redis","visits":3}
The app starts only after Redis becomes Healthy. Each request increments visits by 1. Because AOF is enabled and a Volume is mounted, the counter continues after recreating all containers:
$ docker compose up -d --force-recreate
[+] up 2/2
✔ Container redis-demo-redis-1 Healthy 6.1s
✔ Container redis-demo-app-1 Started 6.2s
$ curl -s http://localhost:8000/
{"message":"Hello from Flask + Redis","visits":4}
Why health checks should grep PONG
A common example online is test: ["CMD", "redis-cli", "ping"]. Let’s check its exit code:
$ docker compose exec redis redis-cli ping; echo "exit code: $?"
PONG
exit code: 0
$ docker compose exec -e REDISCLI_AUTH=wrong redis redis-cli ping; echo "exit code: $?"
AUTH failed: WRONGPASS invalid username-password pair or user is disabled.
NOAUTH Authentication required.
exit code: 0
$ docker compose exec -e REDISCLI_AUTH=wrong redis sh -c "redis-cli ping | grep -q PONG"; echo "exit code: $?"
AUTH failed: WRONGPASS invalid username-password pair or user is disabled.
exit code: 1
With the wrong password, redis-cli prints NOAUTH Authentication required., but the exit code is still 0. Docker uses only the exit code to determine health, so a health check passes even with the wrong password. The app then fails to connect after it starts. Add | grep -q PONG so the exit code is 0 only when the response actually contains PONG.
Common redis-cli commands
docker exec redis redis-cli INFO memory # Memory usage
docker exec redis redis-cli INFO stats # Statistics, including evicted_keys
docker exec redis redis-cli DBSIZE # Number of keys
docker exec redis redis-cli --scan --pattern 'session:*' | head
docker exec -it redis redis-cli MONITOR # Show all commands in real time (for debugging; affects performance; press Ctrl+C to stop)
If you set a password, add -e REDISCLI_AUTH=password to docker exec.
Avoid using
KEYS *in production. It scans every key and blocks Redis, which can stall it for several seconds when there are many keys. UseSCANinstead (redis-cli --scan).
To browse data with a graphical interface, use the official Redis Insight (image redis/redisinsight, default port 5540). The first time you open it, read and accept its license terms. Then add a connection using the Compose service name redis as the host, port 6379, and your password.
Redis licensing and Valkey
In 2024, Redis changed its license to non-open-source licenses such as RSALv2 and SSPL. Starting with Redis 8, AGPLv3 was added as an option. In 2024, the Linux Foundation introduced Valkey, a compatible open-source fork under the BSD license. Its commands and usage are nearly identical:
$ docker run -d --name valkey valkey/valkey:8-alpine
Unable to find image 'valkey/valkey:8-alpine' locally
8-alpine: Pulling from valkey/valkey
Digest: sha256:081c2f5cb575efc901aa80ff9cdbd1ec6a301682fd35e1ebb4b0990a4a4a8507
Status: Downloaded newer image for valkey/valkey:8-alpine
3e7385b8f0d22a999ccf9306938e178c8f82bb4bc1ef9d304bd307917a890087
$ docker exec valkey valkey-cli PING
PONG
$ docker exec valkey redis-cli SET greeting hello
OK
$ docker exec valkey valkey-server --version
Valkey server v=8.1.10 sha=00000000:1 malloc=jemalloc-5.3.0 bits=64 build=fd3b186b1408478b
$ docker rm -f valkey
valkey
The Valkey image includes both valkey-cli and the compatible redis-cli, and Redis libraries in applications can connect to it directly. Using Redis yourself is generally unaffected by the license change. If you provide Redis as a service to others, check the license terms.
Recap
- Redis has no password by default. Set one with
--requirepassand pass it withREDISCLI_AUTH. - RDB doesn’t save data immediately. Enable
--appendonly yeswhen you need reliable persistence. - Set
maxmemoryandallkeys-lruwhen using Redis as a cache. - Write health checks as
redis-cli ping | grep -q PONG.
Exercises
Exercise 1: Use docker exec redis redis-cli to set a key that expires after 30 seconds, then check the remaining time.
Answer
docker exec redis redis-cli SET temp hello EX 30
docker exec redis redis-cli TTL temp
If you set a password, add -e REDISCLI_AUTH=password.
FAQ
Does Redis always need a password?
Always set one if anyone else might be able to connect. The best approach is to keep the port private and set a password.
Why is my data gone after Redis restarts?
By default, RDB writes to disk only when a save condition is met or Redis shuts down cleanly. docker rm -f and crashes can cause data loss. Enable AOF when you need reliable persistence.
Can I use Valkey and Redis interchangeably?
Valkey is an open-source fork of Redis. Its commands and protocol are compatible, so applications usually need no changes.
Updated October 14, 2026