Day 19: Mastering Docker Compose Secrets for My Flask + Redis App

Usman JapUsman Jap
5 min read

If you’ve been following along, you know I’m building a sweet Flask + Redis app, and today, we’re leveling up by securing sensitive data like passwords and API keys. Think of this as locking your digital diary with a super-secret code—only cooler!

and let’s roll!

Why Secrets? The Spy Game of DevOps

Picture this: my Flask app chats with Redis to track page visits, and Redis needs a password to keep things secure. Hardcoding that password in my code? Yikes! That’s like leaving your house key under the doormat. Environment variables? Better, but they can still leak in logs or docker inspect. Enter Docker Compose secrets—the ninja way to hide sensitive data! Secrets let me store passwords in encrypted files, accessible only to the containers that need them. No leaks, no drama, just pure security vibes.

Today’s mission: secure my app’s redis_password and add a new flask_app_key for Flask’s session management. Plus, I’ll sprinkle in an external TEMP_API_KEY for some 2025 flair. Ready? Let’s dive in!

Step 1: Setting Up the Scene

I’m working in my ~/tmp/flask-redis-app directory on Ubuntu, rocking Docker Compose v2.36.2 (yep, I upgraded it today—more on that later!). My app’s structure is clean and cozy:

  • app/app.py: The Flask app counting page visits via Redis.

  • Dockerfile: Builds the Flask app image.

  • docker-compose.yml: Orchestrates web, redis, nginx-dev, prometheus, grafana, and fluentd services.

  • redis_password.txt: Holds the Redis password (currently supersecretpassword).

First, I check my setup:

ls -l

Output: app/ Dockerfile docker-compose.yml redis_password.txt nginx-dev.conf prometheus.yml fluent.conf ...

Everything’s in place, but I need to add flask_app_key.txt and secure an external API key. Let’s do this!


Step 2: Crafting the Secrets in docker-compose.yml

Secrets in Docker Compose are like passing notes in class—only the intended services get them. I open docker-compose.yml with nano:

nano docker-compose.yml

I add a version for compatibility and define my secrets under the secrets key:

version: "3.8"
secrets:
  redis_password:
    file: ./redis_password.txt
  flask_app_key:
    file: ./flask_app_key.txt

Next, I assign these secrets to my web and test services (and redis for redis_password):

services:
  web:
    build: .
    environment:
      - REDIS_HOST=redis-service
      - APP_TITLE=My Cool App
      - TEMP_API_KEY=${TEMP_API_KEY}  # External API key
    secrets:
      - redis_password
      - flask_app_key
    depends_on:
      redis:
        condition: service_healthy
    profiles:
      - dev
    networks:
      app-net:
        aliases:
          - flask-service

  redis:
    image: redis:latest
    command: redis-server --requirepass ${REDIS_PASSWORD}
    secrets:
      - redis_password
    environment:
      - REDIS_PASSWORD=${REDIS_PASSWORD}
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
    profiles:
      - dev
      - prod
      - test
    networks:
      app-net:
        aliases:
          - redis-service

  test:
    build: .
    command: pytest tests/test_app.py
    environment:
      - REDIS_HOST=redis-service
      - APP_TITLE=Test App
      - TEMP_API_KEY=${TEMP_API_KEY}
    secrets:
      - redis_password
      - flask_app_key
    profiles:
      - test
    networks:
      - app-net

I also add networks, volumes, and other services (nginx-dev, prometheus, etc.) from Day 18. Save and exit—boom, secrets defined!


Step 3: Creating Secret Files

Now, I create the new flask_app_key.txt:

echo "my-flask-secret-key-2025" > flask_app_key.txt

I secure both secret files:

chmod 600 redis_password.txt flask_app_key.txt
ls -l redis_password.txt flask_app_key.txt

Output: -rw------- 1 usman usman 20 redis_password.txt and similar for flask_app_key.txt.

To keep things 2025-fresh, I add an external API key to .env:

echo "TEMP_API_KEY=external-api-key-2025" > .env
echo ".env" >> .gitignore

Step 4: Updating the Flask App to Use Secrets

My Flask app needs to read redis_password and flask_app_key. I tweak app/app.py:

from flask import Flask
import redis
import logging
logging.basicConfig(level=logging.INFO)

app = Flask(__name__)

try:
    with open('/run/secrets/flask_app_key', 'r') as f:
        app.secret_key = f.read().strip()
    logging.info("Flask secret key loaded")
except FileNotFoundError:
    app.secret_key = 'fallback-key'
    logging.error("Flask secret key missing")

try:
    with open('/run/secrets/redis_password', 'r') as f:
        redis_password = f.read().strip()
    redis_client = redis.Redis(host='redis-service', password=redis_password)
    logging.info("Redis connected")
except Exception as e:
    logging.error(f"Redis connection failed: {e}")
    redis_client = None

@app.route('/')
def index():
    if redis_client:
        visits = redis_client.incr('visits')
    else:
        visits = 0
    return f"Default App: Visited {visits} times"

@app.route('/health')
def health():
    return "OK", 200

This logs success or failure without exposing secrets. Cool, right?


Step 5: Testing the Setup

Time to fire it up! I validate my docker-compose.yml:

yamllint -f standard docker-compose.yml
docker compose config --services

Output: web test redis nginx-dev prometheus grafana fluentd (phew, no services: {} drama after recreating the file!).

I start the dev profile:

docker compose --profile dev up -d --build --wait

Check services:

docker compose ps

Output: All services Up (healthy)—Redis is pinging, Flask is serving!

Test the app:

curl http://localhost:8080

Output: “Default App: Visited 1 times”. Click, click, refresh—visits climb!

Verify secrets without exposing them:

docker compose exec web ls /run/secrets

Output: redis_password flask_app_key. They’re there, but I don’t cat them—logging handles that.

Check logs:

docker compose logs web | grep "loaded"

Output: Flask secret key loaded and Redis connected. Secrets are working!


Step 6: Avoiding the Dreaded cat Command

I used to debug with docker compose exec web cat /run/secrets/redis_password, but that’s like shouting my password in a crowded room! Instead, I rely on:

  • Logs: docker compose logs web confirms secret loading.

  • Healthchecks: Redis’ ping ensures redis_password works.

  • Tests: Run the test profile:

      docker compose --profile test up --build --abort-on-container-exit
    

    Output: Tests pass, proving secrets are functional.


Step 7: Reflecting on the Journey

Today was a blast! I secured my app with Docker Compose secrets, learned to avoid risky cat commands, and upgraded Docker to v2.36.2 (pro tip: always sudo apt update first!). Challenges? Oh yeah—my docker-compose.yml once showed services: {} due to a parsing glitch, but recreating it with version: "3.8" fixed it.

Key Takeaways:

  • Secrets > environment variables for sensitive data.

  • Keep secret files secure (chmod 600) and .gitignore’d.

  • Log success, not secrets, for debugging.

  • Healthchecks and tests are your friends!

0
Subscribe to my newsletter

Read articles from Usman Jap directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Usman Jap
Usman Jap