# Self-Hosted Managed Redis in Docker With Backups

> Run Redis in Docker for production: auth, AOF/RDB persistence, memory policy, network isolation and S3 backups — or let Temps manage it on your server.

David Viejo · Published 2026-06-06 · Updated 2026-11-09 · 11 min read

Canonical: https://temps.sh/blog/how-to-set-up-redis-as-a-service

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](https://hub.docker.com/_/redis)).

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](https://temps.sh/docs/managed-redis) do it.

---

## What does a production Redis container need?

Redis' own [security guide](https://redis.io/docs/latest/operate/oss_and_stack/management/security/) 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:

```text
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

```ini
# 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

```ini
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](https://redis.io/docs/latest/operate/oss_and_stack/management/security/acl/)).

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

```yaml
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](https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/) 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](https://redis.io/docs/latest/develop/reference/eviction/)), 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](https://docs.docker.com/engine/network/packet-filtering-firewalls/)). 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](https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/)). The script below takes a fresh snapshot, waits for it, encrypts it and uploads it:

```bash
#!/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`](https://redis.io/docs/latest/commands/info/) 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`](https://redis.io/docs/latest/develop/tools/cli/) 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](https://redis.io/docs/latest/commands/info/) 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](https://temps.sh/docs/managed-redis) runs the same kind of container on your own server and handles the repetitive parts:

```bash
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](https://temps.sh/docs/backups) 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](https://temps.sh/docs/kv-storage) 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](https://temps.sh/docs/managed-redis) 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.

---

Source: https://temps.sh/blog/how-to-set-up-redis-as-a-service · Index of all pages: https://temps.sh/llms.txt
