Self-Hosted Managed Redis in Docker With Backups

David Viejo
David Viejo · Updated · Published

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.x image, load a redis.conf that sets appendonly yes, appendfsync everysec, a few save points, maxmemory below the container's memory limit and a maxmemory-policy. Disable the default user and define ACL users in an aclfile. Put Redis on an internal Docker network with no ports: entry. Back up by running BGSAVE, copying dump.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:

ConcernDefault redis containerProduction setting
Authenticationdefault user, no passworddefault user off; named ACL users with long generated passwords
ExposureProtected mode off in the imageNo published port; a private Docker network
DurabilityRDB snapshots onlyAOF (fsync every second) plus RDB snapshots
MemoryNo limit on 64-bit systemsmaxmemory below the container limit, an explicit policy
BackupsNoneDaily RDB copied off the server, encrypted
UpgradesWhatever latest resolves toA 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 off means a connection that hasn't sent AUTH can't run anything. Without it, every new connection is logged in as default.
  • app gets every command except the @dangerous category, which includes FLUSHALL, KEYS, CONFIG, DEBUG, SAVE and INFO. Some client libraries call INFO on connect; if yours does, add +info to the app line.
  • ops is for you and the backup script, which needs BGSAVE and LASTSAVE.
  • healthcheck can only PING.

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. always is safer but very slow; no leaves 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 by appenddirname.
  • RDB writes a compact point-in-time snapshot (dump.rdb). save 3600 1 300 100 60 10000 snapshots 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:

WorkloadPolicyWhy
Cache onlyallkeys-lruRedis' suggested default when some keys are read far more than others
Cache with a stable hot setallkeys-lfuKeeps frequently used keys even if they weren't touched recently
Sessions, queues, rate-limit counters, locksnoevictionWrites fail with an error at the limit instead of silently dropping data
Mixed cache and persistent keysvolatile-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:

  1. Stop the app containers, then docker compose stop redis.
  2. Decrypt the backup and copy it into the volume as dump.rdb; move the existing appendonlydir aside.
  3. Start Redis once with AOF off: temporarily set appendonly no in redis.conf, then docker compose start redis, and check the key count with DBSIZE.
  4. Turn AOF back on while it runs: issue CONFIG SET appendonly yes as the ops user and require an OK response. Poll INFO persistence until aof_enabled:1, aof_rewrite_in_progress:0 and aof_rewrite_scheduled:0, and require both aof_last_bgrewrite_status:ok and aof_last_write_status:ok. Only then set appendonly yes in redis.conf again 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:

  1. Run the backup script and confirm the upload.
  2. Read the release notes for every version you skip, especially ACL changes: Redis 8 added command categories that can break existing ACL rules.
  3. 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.
  4. Check INFO server for the new version and DBSIZE for 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_PASSWORD and REDIS_DATABASE on 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.

#redis#docker#self-hosted#backups#managed service