← Blog
Terracotta ceramic tiles connect to a cream storage jar inside a sage tray, with a small gold key outside.
Published on
2026-11-10

Redis with Docker Compose: a private service on a CPU VM

Redis Docker Compose configuration can describe the server, its network, credentials, and persistent storage in one project. This guide builds a small private lab with one Redis service and an optional command-line client. It gives you checks for authenticated access, restricted commands, and a marker surviving container recreation.

Use disposable data on a Linux host you control. If you are still choosing who should operate the service, start with managed Redis hosting versus a self-managed CPU VM. This example is a development exercise, not a production availability or recovery design.

Validation, November 10, 2026: local checks passed for both Compose profiles with Docker Compose 5.1.4 and for the credential helper with Python 3.12.14. The checks covered configuration parsing, credential/hash consistency, refusal to overwrite credentials, network and volume declarations, and image pins. The Docker Linux engine was unavailable. Image pull, container startup, authentication, Linux file permissions, network isolation, persistence, and cloud deployment were not run. Expected responses below are instructions for your own checks, not captured results.

Prepare a separate Linux project

You need Docker Engine with Linux containers, the Compose plugin, Python 3, and permission to use the Docker daemon. Check that the engine responds, not just that the client is installed. The Docker on a Compute VM guide covers the surrounding host setup. Run the commands below in a Linux shell, from a new project directory.

docker version
docker compose version
python3 --version
mkdir redis-private-lab
cd redis-private-lab

Stop if that directory or a Compose project named redis-private-lab already exists. Choose a distinct directory and project name rather than attaching this exercise to an existing Redis volume. Save the four files below together. Run each command only after the previous one succeeds.

Define the Redis Docker Compose services

Save this as compose.yaml. Both services use redis:8.10.3-alpine, listed in the official Redis image manifest at this review. This pins a version tag, not immutable image bytes. Record the pulled digest when validating a deployment. The Redis Docker instructions cover the image workflow.

name: redis-private-lab

services:
  redis:
    image: redis:8.10.3-alpine
    command: [redis-server, /usr/local/etc/redis/redis.conf]
    volumes:
      - ./redis.conf:/usr/local/etc/redis/redis.conf:ro
      - redis_data:/data
    secrets: [redis_users, redis_password]
    networks: [private]
    mem_limit: 256m
    restart: unless-stopped
    healthcheck:
      test: [CMD-SHELL, 'REDISCLI_AUTH="$$(cat /run/secrets/redis_password)" redis-cli --user app ping | grep -qx PONG']
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 5s
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
  client:
    image: redis:8.10.3-alpine
    profiles: [tools]
    secrets: [redis_password]
    networks: [private]
    entrypoint:
      - /bin/sh
      - -c
      - 'export REDISCLI_AUTH="$$(cat /run/secrets/redis_password)"; exec redis-cli --user app -h redis "$$@"'
      - --
    command: [PING]

networks:
  private:
    internal: true
volumes:
  redis_data:
secrets:
  redis_users:
    file: ./secrets/users.acl
  redis_password:
    file: ./secrets/redis_password

There is no ports entry. The client reaches redis:6379 through the service name on an internal Docker network. Your laptop does not get a published Redis endpoint. Inside the client container, localhost would mean the client itself. The Compose networking guide explains service discovery and network boundaries.

The network declaration is intended to restrict external connectivity; it is not protection from an administrator who controls Docker or the host. Do not add a public 6379:6379 mapping to solve a connection problem. This example has no TLS and is limited to a trusted single-host lab. A remote client route needs its own authentication, encryption, and firewall design.

The client runs on demand through the tools profile. Its entrypoint reads the mounted password file and passes it through REDISCLI_AUTH, which the Redis CLI supports. The doubled dollar signs preserve shell expansion inside the container instead of letting Compose substitute values on the host. Do not put passwords in command arguments or share diagnostic output containing them.

Set Redis persistence and a small memory boundary

Save this as redis.conf:

bind 0.0.0.0
protected-mode yes
port 6379
aclfile /run/secrets/redis_users
dir /data
appendonly yes
appendfsync everysec
save ""
maxmemory 64mb
maxmemory-policy noeviction

Redis listens on its container interfaces so the separate client can connect. The named volume mounts at /data, matching the configured data directory. Append-only persistence is enabled; automatic RDB snapshots are disabled for this exercise. With appendfsync everysec, a failure can still lose recent writes. See the Redis persistence documentation before changing the durability tradeoff.

The 64 MiB Redis limit and 256 MiB container cap are deliberately small lab values, not VM sizing advice. Redis needs memory beyond the dataset, including operating overhead and persistence work. The noeviction policy rejects memory-growing writes at the configured limit instead of automatically evicting keys. Your application must handle that error. A queue and a disposable cache can need different policies; review the eviction documentation before reusing these settings.

To review the memory settings for a different workload, use the Redis memory and eviction guide. It distinguishes Redis maxmemory from the container cap and uses a separate disposable copy for configuration checks. Its budget is hypothetical, and cache eviction policies should not be applied to a queue without evaluating delivery failures.

Create a restricted Redis user without printing its password

Save this as prepare_secrets.py. It creates the secrets directory once and refuses to overwrite an existing one. The server ACL contains a SHA-256 hash; the client password stays in a separate local file.

"""Create disposable lab credentials once; never print or overwrite them."""
import hashlib
import secrets
from pathlib import Path


def main():
    directory = Path(__file__).resolve().parent / "secrets"
    directory.mkdir(mode=0o700, exist_ok=False)
    password = secrets.token_hex(32)
    password_hash = hashlib.sha256(password.encode("ascii")).hexdigest()
    files = {
        "redis_password": password + "\n",
        "users.acl": (
            "user default off\n"
            f"user app on #{password_hash} ~lab:* -@all "
            "+ping +get +set +del +exists +expire +ttl +incr\n"
        ),
    }
    for name, content in files.items():
        path = directory / name
        with path.open("x", encoding="ascii", newline="\n") as handle:
            handle.write(content)
        path.chmod(0o444)
    print("Created lab credentials in secrets/. Keep this directory private.")


if __name__ == "__main__":
    main()

Save this as .gitignore, then run the helper:

secrets/
__pycache__/
*.pyc
python3 prepare_secrets.py

The helper requests a private parent directory on Linux. Its files are readable inside the containers, including by the Redis process after it drops privileges. Verify the actual permissions and ownership on your Linux host before starting; the local Windows check did not validate Linux permission enforcement. Keep the entire secrets/ directory out of shared archives and Git. If setup fails partway, inspect the directory rather than blindly rerunning or deleting credentials.

Local Compose secrets are file mounts, not an encrypted secret store. Anyone with sufficient host or Docker access can read them. Our Compose secrets guide covers that boundary. Setting a variable named REDIS_PASSWORD alone does not configure this server: the explicit ACL file does.

The default user is disabled. The app user can run a short command allowlist against keys beginning with lab:. It cannot administer Redis or operate on arbitrary keys. See the Redis ACL reference when designing permissions for an actual client. This lab supplies no administrator login; configuration changes use the protected host files and a deliberate restart.

Validate configuration, then start Redis

These first two commands only parse the project. They do not prove that Redis accepts its configuration or that the network and credentials work:

docker compose config --quiet
docker compose --profile tools config --quiet

The remaining container commands have not been executed for this article. Pull the selected image, start Redis, and wait for its authenticated health check:

docker compose pull redis
docker compose up -d --wait --wait-timeout 120 redis
docker compose ps
docker compose logs --tail=50 redis

The health check expects an authenticated PONG from Redis inside the server container. It does not test the client network path or recovery. A restart policy applies when a container exits; an unhealthy status alone does not automatically restart it. If startup fails, inspect the logs, mounted file paths, permissions, and configuration before continuing. Do not weaken authentication to turn the health indicator green.

Check access and the command boundary

From the same directory, use the optional client service. A successful run should return PONG, OK, and then the exact marker text:

docker compose run --rm client PING
docker compose run --rm client SET lab:marker compose-check
docker compose run --rm client GET lab:marker

Now check rejection paths. The first command bypasses the client wrapper, so it sends no password. The second uses the authenticated user but asks for a key outside its permitted prefix. The third requests an administrative command that was not granted:

docker compose run --rm --entrypoint redis-cli client -h redis PING
docker compose run --rm client GET outside:marker
docker compose run --rm client CONFIG GET dir

Expect NOAUTH for the unauthenticated request and NOPERM for the two authorization checks. Record the actual error text. Redis CLI output matters here; a shell command finishing is not proof that Redis accepted or rejected the operation correctly. Stop if an unauthorized operation succeeds. These checks do not establish isolation from a privileged host user.

Check persistence across container recreation

After writing the marker successfully, replace the Redis container while retaining the named volume. The expected GET response is still compose-check:

docker compose up -d --force-recreate --wait --wait-timeout 120 redis
docker compose run --rm client GET lab:marker

If the marker is missing, check the project name, volume mount, Redis data directory, and startup logs. Do not write the marker again and count that as persistence. This procedure has not been run locally; even a successful result would show only survival of this container recreation, not crash safety, an independent backup, or recovery after losing the VM.

The Docker volumes guide explains what remains outside the container. Keep a separate recovery copy when the data matters and rehearse restoration into an isolated environment. A volume on the same failed or deleted machine cannot provide that protection.

For the next recovery check, follow the Redis persistence guide to RDB, AOF, and isolated restores. It separates a container restart from a copied backup and a restore into a fresh volume. Its RDB exercise uses a separate disposable project; do not replace this lab’s AOF configuration or count its proposed steps as tested recovery.

Connect an application without widening the lab by accident

An application service attached to this network would use hostname redis, port 6379, and the app username with a securely delivered password. Check its exact command needs before expanding the ACL. Connection pools, timeouts, retry behavior, and unavailable-service handling belong in the application design; a passing PING establishes none of them.

For queued background work, continue with the Celery with Redis guide. That is a separate project with its own broker configuration and validation limits. The restricted user here is not a drop-in Celery broker user: Celery needs additional Redis commands and key/channel access. Do not replace the Celery configuration with this lab or expand permissions to everything merely to silence an error.

For an API read path, follow the FastAPI Redis caching guide. Its example uses keys under this lab’s lab:* prefix and explains TTLs, invalidation, and failure fallback. The proposed client uses the app user; connecting it to this Compose network and checking the real ACL handshake remain untested integration steps.

Stop the lab and keep its limits visible

Use this command from the project directory to remove the containers and network while retaining the named data volume:

docker compose down

Do not add --volumes or -v unless you intend to delete the lab’s stored data. Keep the credentials and configuration available if you plan to resume. Changing a password file is not a coordinated rotation: server ACLs and clients must agree, and this example does not provide an automated rotation procedure.

For Hivenet, keep this within the documented CPU VM development and testing scope. Confirm the actual OS, storage, Docker support, and network access before evaluating it. No Hivenet instance, Redis benchmark, or cloud recovery process was tested for this guide.

The VM lifecycle still applies. Hivenet’s stop and termination documentation states that a stopped instance expires after 10 days and its local data is deleted. A named Docker volume does not override that rule. Keep any needed recovery copy outside the instance, and treat container checks, cloud access, and recovery evidence as separate acceptance steps.

Your next workload belongs on Hivenet.

Pick one AI, compute, or storage workload and see the difference for yourself. Spin it up in minutes, or let our team map your fastest path to production.