Self-Hosted Managed Redis in Docker With Backups
To run Redis in Docker for production, add what the default container lacks: ACL users instead of an open default user, AOF plus RDB persistence, a maxmemory limit with an eviction policy that fits the workload, a private network with no published port, and encrypted backups copied off the server.
The official redis image starts with protected mode disabled and no password, so a container published with -p 6379:6379 is open to anyone who can reach the port (Docker Hub).
This guide builds that setup with Docker Compose, one file at a time, using only settings documented by Redis. If you'd rather not run it yourself, the last section shows how Temps provisions the same container on your server.
TL;DR: Pin a
redis:8.ximage, load aredis.confthat setsappendonly yes,appendfsync everysec, a fewsavepoints,maxmemorybelow the container's memory limit and amaxmemory-policy. Disable thedefaultuser and define ACL users in anaclfile. Put Redis on an internal Docker network with noports:entry. Back up by runningBGSAVE, copyingdump.rdb(safe while Redis runs), encrypting it and uploading it to S3. Or let Temps' managed Redis do it.
What does a production Redis container need?
Redis' own security guide is blunt: Redis "is designed to be accessed by trusted clients inside trusted environments", and a single FLUSHALL from an outsider deletes the whole dataset. A container that is fine on a laptop needs these changes before it holds production data:
| Concern | Default redis container | Production setting |
|---|---|---|
| Authentication | default user, no password | default user off; named ACL users with long generated passwords |
| Exposure | Protected mode off in the image | No published port; a private Docker network |
| Durability | RDB snapshots only | AOF (fsync every second) plus RDB snapshots |
| Memory | No limit on 64-bit systems | maxmemory below the container limit, an explicit policy |
| Backups | None | Daily RDB copied off the server, encrypted |
| Upgrades | Whatever latest resolves to | A pinned tag, upgraded deliberately |
How do I lay out the files?
Keep the configuration next to the Compose file so it is versioned with your app:
redis/
redis.conf # persistence, memory, ACL file location
users.acl # ACL users (keep out of git; contains secrets)
docker-compose.yml
backup-redis.sh
redis.conf
# Network: listen inside the container. The Docker network, not bind,
# keeps Redis private (see the Compose file: no published port).
bind 0.0.0.0
port 6379
protected-mode yes
# Users come from the ACL file. Redis won't accept users defined both
# here and in an aclfile, so define them only there.
aclfile /usr/local/etc/redis/users.acl
# Persistence: AOF for durability, RDB for compact backups.
dir /data
appendonly yes
appendfsync everysec
save 3600 1 300 100 60 10000
# Memory: stay below the container limit (1 GiB in the Compose file).
maxmemory 768mb
maxmemory-policy noeviction
users.acl
user default off resetpass -@all
user app on >REPLACE_WITH_APP_PASSWORD ~* &* +@all -@dangerous
user ops on >REPLACE_WITH_OPS_PASSWORD ~* &* +@all
user healthcheck on >REPLACE_WITH_HEALTHCHECK_PASSWORD +ping
Generate each password with redis-cli ACL GENPASS (64 hex characters from the system's secure random generator) or openssl rand -hex 32. Redis stores ACL passwords hashed with SHA-256, and the file also accepts #<sha256-hex> instead of >password if you'd rather not keep plaintext on disk (ACL docs).
What the rules do:
user default offmeans a connection that hasn't sentAUTHcan't run anything. Without it, every new connection is logged in asdefault.appgets every command except the@dangerouscategory, which includesFLUSHALL,KEYS,CONFIG,DEBUG,SAVEandINFO. Some client libraries callINFOon connect; if yours does, add+infoto theappline.opsis for you and the backup script, which needsBGSAVEandLASTSAVE.healthcheckcan onlyPING.
docker-compose.yml
services:
redis:
image: redis:8.10-bookworm
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
volumes:
- ./redis:/usr/local/etc/redis
- redis-data:/data
networks: [backend]
mem_limit: 1g
restart: unless-stopped
environment:
HEALTHCHECK_PASSWORD: ${REDIS_HEALTHCHECK_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "redis-cli --user healthcheck --pass \"$$HEALTHCHECK_PASSWORD\" --no-auth-warning ping | grep -q PONG"]
interval: 10s
timeout: 3s
retries: 5
# No "ports:" entry. Apps on the backend network reach redis:6379.
app:
image: your-app
networks: [backend]
environment:
REDIS_URL: redis://app:${REDIS_APP_PASSWORD}@redis:6379/0
volumes:
redis-data:
networks:
backend:
The image runs your config with redis-server /usr/local/etc/redis/redis.conf, as its Docker Hub page documents, and keeps data in /data. The config directory is mounted read-write because Redis rewrites files there if you use CONFIG REWRITE or ACL SAVE.
Should I use AOF, RDB or both?
Use both. Redis' persistence guide says you should run both "if you want a degree of data safety comparable to what PostgreSQL can provide you", and discourages AOF alone because an occasional RDB snapshot helps with backups, faster restarts and AOF bugs.
- AOF logs every write. With
appendfsync everysec, the default and the recommended policy, you can lose about one second of writes in a crash.alwaysis safer but very slow;noleaves flushing to the kernel, typically every 30 seconds on Linux. Since Redis 7.0 the AOF is a set of files (a base file plus incremental files and a manifest) in the directory named byappenddirname. - RDB writes a compact point-in-time snapshot (
dump.rdb).save 3600 1 300 100 60 10000snapshots after an hour if at least one key changed, after 5 minutes if 100 changed, and after a minute if 10,000 changed. On its own, RDB can lose the minutes since the last snapshot.
When both are on, Redis rebuilds the dataset from the AOF at startup because it is the most complete. That matters when you restore, below.
If the instance is a pure cache you can rebuild from the database, you can switch persistence off (appendonly no, save "") and skip backups entirely.
Which maxmemory-policy should I use?
Set maxmemory explicitly: on 64-bit systems the default is no limit, so Redis grows until the container's memory limit stops it. Leave headroom between maxmemory and the container limit, because the AOF and replication buffers aren't counted against maxmemory (eviction docs), and BGSAVE and AOF rewrites fork the process, so pages that change during a snapshot get copied.
Then pick the policy by what the data is:
| Workload | Policy | Why |
|---|---|---|
| Cache only | allkeys-lru | Redis' suggested default when some keys are read far more than others |
| Cache with a stable hot set | allkeys-lfu | Keeps frequently used keys even if they weren't touched recently |
| Sessions, queues, rate-limit counters, locks | noeviction | Writes fail with an error at the limit instead of silently dropping data |
| Mixed cache and persistent keys | volatile-lru (TTL on the cache keys) | Only keys with an expiry are evicted; Redis recommends two instances instead |
Redis 8.6 added allkeys-lrm and volatile-lrm, which evict the least recently modified keys, for read-heavy data that should stay while it is still being read. Watch evicted_keys, keyspace_hits and keyspace_misses in INFO stats to check the choice.
Job queues such as BullMQ and Sidekiq store jobs in Redis, so give them noeviction. If the same app also needs a cache, run a second Redis container for it.
How do I keep Redis off the internet?
Don't publish the port. In the Compose file above, Redis has no ports: entry, so only containers on the backend network can reach it, by the service name redis.
A host firewall is not a substitute. Docker routes published-port traffic before it reaches ufw's chains, so a port published with -p 6379:6379 is reachable "effectively ignoring your firewall configuration" (Docker docs). If you need to reach Redis from the host for maintenance, use docker compose exec redis redis-cli --user ops --pass ..., or publish it on the loopback address only (127.0.0.1:6379:6379).
AUTH travels in plaintext, so if a client connects across hosts rather than over a local Docker network, enable Redis' TLS support or put the link inside a private network or VPN.
How do I back up Redis to S3 without stopping it?
Redis writes each RDB snapshot to a temporary file and renames it into place only when it is complete, so copying dump.rdb while the server runs is safe (persistence guide). The script below takes a fresh snapshot, waits for it, encrypts it and uploads it:
#!/usr/bin/env bash
# backup-redis.sh — run from the directory with docker-compose.yml
set -euo pipefail
cli() {
docker compose exec -T redis redis-cli -e --raw --user ops --pass "$REDIS_OPS_PASSWORD" --no-auth-warning "$@"
}
cli BGSAVE >/dev/null
# Poll completion and outcome, not LASTSAVE (a seconds-resolution timestamp).
# Allow up to 5 minutes of polling; choose a budget for your dataset.
for ((attempt = 0; attempt < 150; attempt++)); do
persistence=$(cli INFO persistence | tr -d '\r')
if grep -qx 'rdb_bgsave_in_progress:0' <<< "$persistence"; then
if ! grep -qx 'rdb_last_bgsave_status:ok' <<< "$persistence"; then
echo "Redis snapshot failed; no backup uploaded" >&2
exit 1
fi
break
fi
if ! grep -qx 'rdb_bgsave_in_progress:1' <<< "$persistence"; then
echo "Cannot determine Redis snapshot state; no backup uploaded" >&2
exit 1
fi
sleep 2
done
if ((attempt == 150)); then
echo "Redis snapshot wait timed out; no backup uploaded" >&2
exit 1
fi
stamp=$(date -u +%Y%m%dT%H%M%SZ)
docker compose cp redis:/data/dump.rdb "/tmp/redis-$stamp.rdb"
gpg --batch --symmetric --passphrase-file /root/.redis-backup-passphrase \
-o "/tmp/redis-$stamp.rdb.gpg" "/tmp/redis-$stamp.rdb"
# Any S3-compatible store: add --endpoint-url for Hetzner, R2, MinIO, etc.
aws s3 cp "/tmp/redis-$stamp.rdb.gpg" "s3://my-backups/redis/redis-$stamp.rdb.gpg"
rm -f "/tmp/redis-$stamp.rdb" "/tmp/redis-$stamp.rdb.gpg"
The INFO persistence fields distinguish an in-progress snapshot from a completed, failed one. LASTSAVE changes only after success and has seconds resolution, so waiting only for its value to change can hang after a failed save or miss a save that completes in the same second. redis-cli -e also makes a rejected BGSAVE or another Redis command error exit nonzero. The polling budget above is five minutes plus command time; it does not impose a network timeout on each CLI call. A failure exits before copying or uploading the snapshot; investigate it and rerun the job.
Schedule it with cron (15 3 * * * cd /srv/app && ./backup-redis.sh) and let an S3 lifecycle rule expire old copies. Redis suggests hourly snapshots kept for 48 hours and daily ones kept for a month or two, with at least one copy a day leaving the machine. Add an alert when an upload fails, and check that the uploaded file's size matches the local one.
If you only run AOF, you have to copy the whole appenddirname directory with automatic rewrites paused (CONFIG SET auto-aof-rewrite-percentage 0, then restore the old value). Redis 8.10 also added a BACKUP command family (BACKUP START, SEAL, LIST, CLEANUP) that produces a restorable snapshot plus incremental files without pausing rewrites.
How do I restore it?
Because Redis loads the AOF instead of dump.rdb when AOF is on, copying an old dump.rdb into /data and restarting does nothing useful. Restore like this:
- Stop the app containers, then
docker compose stop redis. - Decrypt the backup and copy it into the volume as
dump.rdb; move the existingappendonlydiraside. - Start Redis once with AOF off: temporarily set
appendonly noinredis.conf, thendocker compose start redis, and check the key count withDBSIZE. - Turn AOF back on while it runs: issue
CONFIG SET appendonly yesas theopsuser and require anOKresponse. PollINFO persistenceuntilaof_enabled:1,aof_rewrite_in_progress:0andaof_rewrite_scheduled:0, and require bothaof_last_bgrewrite_status:okandaof_last_write_status:ok. Only then setappendonly yesinredis.confagain and resume the app containers.
A rewrite that is not running can still be queued or have failed; aof_rewrite_in_progress:0 alone does not establish success. Choose a finite wait budget for your dataset. If enabling AOF fails, the rewrite or writes fail, or the wait expires, keep the apps stopped, retain the source backup and investigate before restarting Redis. These checks establish completion of the RDB-to-AOF conversion, not application-level restore correctness; verify the restored data before reopening traffic. See the Redis INFO persistence fields for the status meanings.
Redis' guide warns that switching to AOF by editing the config and restarting, without the live CONFIG SET, can lose data, which is why step 4 does it on the running server. On Redis 8.10 and later you can instead set preload-file rdb:/data/restore.rdb for one start.
How do I upgrade Redis safely?
Pin a minor version (redis:8.10-bookworm) rather than latest, so a restart never changes versions behind your back. To upgrade:
- Run the backup script and confirm the upload.
- Read the release notes for every version you skip, especially ACL changes: Redis 8 added command categories that can break existing ACL rules.
- Change the tag, then
docker compose pull redis && docker compose up -d redis. The container restarts and reloads its data from the volume, so expect a short outage. - Check
INFO serverfor the new version andDBSIZEfor the key count.
Keep the pre-upgrade backup until you're sure you won't roll back: a newer Redis can write files an older version can't load.
Or let Temps run Redis for you
Everything above is a few hours of setup plus ongoing care. Temps runs the same kind of container on your own server and handles the repetitive parts:
bunx @temps-sdk/cli services create -t redis -n my-cache -y
bunx @temps-sdk/cli services link --id <id> -p my-app
- It generates the password and puts the container on Temps' internal Docker network.
- Linking a project injects
REDIS_URL,REDIS_HOST,REDIS_PORT,REDIS_PASSWORDandREDIS_DATABASEon the next deploy, with a separate logical database number per project and environment. - On the backup schedule you set, it takes RDB snapshots and uploads them to your S3 bucket, and restores them from the dashboard or CLI.
- You can cap the container's memory and CPU, and upgrade the image while keeping the data volume.
If all you need is a cache or counters over HTTP, the built-in KV store may be enough without a Redis container of your own. Temps is free to self-host under MIT / Apache 2.0; the managed Redis docs cover every option.
Frequently asked questions
Is it safe to expose the Redis port to the internet?
No. Redis is designed for trusted clients on a trusted network, the official Docker image has protected mode off, and Docker's published ports bypass host firewalls such as ufw. Keep Redis on a private Docker network, and if you must reach it from outside, use a VPN or TLS plus ACL users, never a bare published port.
Do I need both AOF and RDB?
For data you can't rebuild, yes: AOF with appendfsync everysec limits loss to about a second, and RDB gives you compact files to back up. For a pure cache, you can turn both off.
What is the best maxmemory-policy for a cache?
allkeys-lru is Redis' suggested default for caches. Use noeviction for anything that must not disappear, such as sessions, queues and locks, and run caches and queues in separate instances.
Can I use requirepass instead of ACL users?
You can: requirepass still works and sets the password of the default user. ACL users let the app run without FLUSHALL, CONFIG or KEYS and give backups and health checks their own credentials, so a leaked app password does less damage.
How do I run Redis with high availability in Docker?
A single container with backups covers most small apps: a crash costs the restart time plus up to a second of writes. For automatic failover you need replicas plus Redis Sentinel (three Sentinels on separate hosts), or Redis Cluster for sharding. Temps' managed Redis runs a single instance per service.
Get weekly updates