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-cli is included in the container, so you can run commands directly.
  • SET ... EX 60 makes the key expire after 60 seconds. Use TTL to 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:

  1. If you don’t need external connections, don’t use -p. If you do, bind only to 127.0.0.1.
  2. 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-warning only hides the warning.
  • A better approach is to pass the password through the REDISCLI_AUTH environment variable. redis-cli uses 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:

ConditionMeaning
3600 1Save if at least 1 key changes within 1 hour
300 100Save if at least 100 keys change within 5 minutes
60 10000Save 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.

MethodAdvantagesDisadvantages
RDB (default)Small files, fast loading after restartAn unexpected shutdown can lose data written since the last snapshot
AOFWrites to disk once per second by default, so at most about 1 second of data may be lostLarger files, and more disk load when there are many writes
No persistenceFastestAll 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:

PolicyBehavior
noevictionDefault; reject new writes and return an error
allkeys-lruEvict the least recently used keys from all keys; most common for caches
volatile-lruEvict only keys with an expiration set
allkeys-lfuEvict 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: 67108864 bytes is 64 MB, and the policy is allkeys-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:
SettingDescription
@redis:6379 in REDIS_URLConnect using the service name
${REDIS_PASSWORD} in commandCompose replaces this with the actual password from .env
REDISCLI_AUTHMakes redis-cli inside the container (including health checks) use the password automatically
redis has no portsOnly 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. Use SCAN instead (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 --requirepass and pass it with REDISCLI_AUTH.
  • RDB doesn’t save data immediately. Enable --appendonly yes when you need reliable persistence.
  • Set maxmemory and allkeys-lru when 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