docs(env): expand .env.example into full compose interpolation template
Replace the minimal placeholder .env.example with a comprehensive template documenting every variable consumed by docker-compose.yaml, organized by service (Django core, HTTP, Postgres, Neo4j, Memcached, S3/MinIO, Daedalus, Celery/RabbitMQ, etc.). Clarifies that this file is rendered from an Ansible Jinja2 template with vaulted secrets in production, and distinguishes it from the in-tree mnemosyne/.env used for bare-Python development.
This commit is contained in:
@@ -12,6 +12,16 @@
|
||||
# and reranker on Nyx, smtp4dev on Oberon. All reached over the internal
|
||||
# 10.10.0.0/24 network.
|
||||
#
|
||||
# Environment scoping
|
||||
# -------------------
|
||||
# Every service lists ONLY the environment variables it actually needs, with
|
||||
# values interpolated from the shell (typically `.env` at the project root,
|
||||
# which an Ansible role generates from a j2 template + vault secrets). No
|
||||
# `env_file:` sharing — a compromised MCP container should not see the Celery
|
||||
# broker creds or the LLM API encryption key, and the Celery worker has no
|
||||
# business knowing `ALLOWED_HOSTS`. If you add a new Django setting, decide
|
||||
# which services need it and add it only to those `environment:` blocks.
|
||||
#
|
||||
# Run:
|
||||
# docker compose up -d
|
||||
# docker compose run --rm app migrate # one-shot DB migrate
|
||||
@@ -32,11 +42,63 @@ services:
|
||||
restart: "no"
|
||||
|
||||
# ── App: Django REST API + admin ──────────────────────────────────────────
|
||||
# Serves /library/api/*, /admin/, /live/, /ready/, /metrics. Enqueues
|
||||
# Celery tasks (hence CELERY_BROKER_URL is required here too — Django is
|
||||
# the producer, the worker is the consumer).
|
||||
app:
|
||||
image: git.helu.ca/r/mnemosyne:latest
|
||||
pull_policy: always
|
||||
command: ["web"]
|
||||
env_file: mnemosyne/.env
|
||||
environment:
|
||||
# Django core
|
||||
- DJANGO_SETTINGS_MODULE=mnemosyne.settings
|
||||
- SECRET_KEY=${SECRET_KEY}
|
||||
- DEBUG=${DEBUG}
|
||||
- ALLOWED_HOSTS=${ALLOWED_HOSTS}
|
||||
- CSRF_TRUSTED_ORIGINS=${CSRF_TRUSTED_ORIGINS}
|
||||
- TIME_ZONE=${TIME_ZONE}
|
||||
- LANGUAGE_CODE=${LANGUAGE_CODE}
|
||||
# Postgres (Django ORM)
|
||||
- APP_DB_NAME=${APP_DB_NAME}
|
||||
- APP_DB_USER=${APP_DB_USER}
|
||||
- APP_DB_PASSWORD=${APP_DB_PASSWORD}
|
||||
- DB_HOST=${DB_HOST}
|
||||
- DB_PORT=${DB_PORT}
|
||||
# Neo4j (knowledge graph + vectors)
|
||||
- NEOMODEL_NEO4J_BOLT_URL=${NEOMODEL_NEO4J_BOLT_URL}
|
||||
# Memcached (readiness probe, theme/notification cache)
|
||||
- KVDB_LOCATION=${KVDB_LOCATION}
|
||||
- KVDB_PREFIX=${KVDB_PREFIX}
|
||||
# S3 (Django storage backend — chunk text, item files)
|
||||
- USE_LOCAL_STORAGE=${USE_LOCAL_STORAGE}
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
|
||||
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
|
||||
- AWS_STORAGE_BUCKET_NAME=${AWS_STORAGE_BUCKET_NAME}
|
||||
- AWS_S3_ENDPOINT_URL=${AWS_S3_ENDPOINT_URL}
|
||||
- AWS_S3_USE_SSL=${AWS_S3_USE_SSL}
|
||||
- AWS_S3_VERIFY=${AWS_S3_VERIFY}
|
||||
- AWS_S3_REGION_NAME=${AWS_S3_REGION_NAME}
|
||||
# Celery (Django enqueues tasks; does NOT consume)
|
||||
- CELERY_BROKER_URL=${CELERY_BROKER_URL}
|
||||
- CELERY_RESULT_BACKEND=${CELERY_RESULT_BACKEND}
|
||||
- CELERY_TASK_ALWAYS_EAGER=${CELERY_TASK_ALWAYS_EAGER}
|
||||
# LLM API secrets (admin + DRF pages decrypt stored provider API keys)
|
||||
- LLM_API_SECRETS_ENCRYPTION_KEY=${LLM_API_SECRETS_ENCRYPTION_KEY}
|
||||
# Email
|
||||
- EMAIL_HOST=${EMAIL_HOST}
|
||||
- EMAIL_PORT=${EMAIL_PORT}
|
||||
- EMAIL_USE_TLS=${EMAIL_USE_TLS}
|
||||
# Search & re-ranker (serves /library/api/search)
|
||||
- SEARCH_VECTOR_TOP_K=${SEARCH_VECTOR_TOP_K}
|
||||
- SEARCH_FULLTEXT_TOP_K=${SEARCH_FULLTEXT_TOP_K}
|
||||
- SEARCH_GRAPH_MAX_DEPTH=${SEARCH_GRAPH_MAX_DEPTH}
|
||||
- SEARCH_RRF_K=${SEARCH_RRF_K}
|
||||
- SEARCH_DEFAULT_LIMIT=${SEARCH_DEFAULT_LIMIT}
|
||||
- RERANKER_MAX_CANDIDATES=${RERANKER_MAX_CANDIDATES}
|
||||
- RERANKER_TIMEOUT=${RERANKER_TIMEOUT}
|
||||
# Logging
|
||||
- LOGGING_LEVEL=${LOGGING_LEVEL}
|
||||
- DJANGO_LOGGING_LEVEL=${DJANGO_LOGGING_LEVEL}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
static-init:
|
||||
@@ -51,11 +113,63 @@ services:
|
||||
start_period: 30s
|
||||
|
||||
# ── MCP server: FastMCP Streamable HTTP at /mcp/ ───────────────────────────
|
||||
# Read-only LLM-facing surface. Intentionally excluded:
|
||||
# CELERY_BROKER_URL — MCP must not enqueue tasks
|
||||
# LLM_API_SECRETS_ENCRYPTION_KEY — MCP must not decrypt stored provider keys
|
||||
# DAEDALUS_S3_* — MCP does not ingest
|
||||
# CSRF_TRUSTED_ORIGINS — MCP does not accept browser forms
|
||||
# EMAIL_* — MCP does not send mail
|
||||
# EMBEDDING_* (batch/timeout) — MCP does not embed
|
||||
# S3 vars ARE passed so STORAGES initialises identically to the app container
|
||||
# (simpler to reason about than having mcp use FileSystemStorage while the
|
||||
# rest of the stack uses S3). MCP is read-only at the application layer so
|
||||
# the S3 key here only matters if someone exploits a write path in the
|
||||
# future — keep the credential scoped to read-only in your secret manager.
|
||||
mcp:
|
||||
image: git.helu.ca/r/mnemosyne:latest
|
||||
pull_policy: always
|
||||
command: ["mcp"]
|
||||
env_file: mnemosyne/.env
|
||||
environment:
|
||||
# Django core (ASGI still imports settings)
|
||||
- DJANGO_SETTINGS_MODULE=mnemosyne.settings
|
||||
- SECRET_KEY=${SECRET_KEY}
|
||||
- DEBUG=${DEBUG}
|
||||
- ALLOWED_HOSTS=${ALLOWED_HOSTS}
|
||||
- TIME_ZONE=${TIME_ZONE}
|
||||
- LANGUAGE_CODE=${LANGUAGE_CODE}
|
||||
# Postgres (McpToken lookup lives in Django ORM)
|
||||
- APP_DB_NAME=${APP_DB_NAME}
|
||||
- APP_DB_USER=${APP_DB_USER}
|
||||
- APP_DB_PASSWORD=${APP_DB_PASSWORD}
|
||||
- DB_HOST=${DB_HOST}
|
||||
- DB_PORT=${DB_PORT}
|
||||
# Neo4j (search + get_chunk)
|
||||
- NEOMODEL_NEO4J_BOLT_URL=${NEOMODEL_NEO4J_BOLT_URL}
|
||||
# Memcached
|
||||
- KVDB_LOCATION=${KVDB_LOCATION}
|
||||
- KVDB_PREFIX=${KVDB_PREFIX}
|
||||
# S3 (same block as app — STORAGES must initialise identically)
|
||||
- USE_LOCAL_STORAGE=${USE_LOCAL_STORAGE}
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
|
||||
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
|
||||
- AWS_STORAGE_BUCKET_NAME=${AWS_STORAGE_BUCKET_NAME}
|
||||
- AWS_S3_ENDPOINT_URL=${AWS_S3_ENDPOINT_URL}
|
||||
- AWS_S3_USE_SSL=${AWS_S3_USE_SSL}
|
||||
- AWS_S3_VERIFY=${AWS_S3_VERIFY}
|
||||
- AWS_S3_REGION_NAME=${AWS_S3_REGION_NAME}
|
||||
# MCP-specific
|
||||
- MCP_REQUIRE_AUTH=${MCP_REQUIRE_AUTH}
|
||||
# Search & re-ranker (the `search` MCP tool uses these)
|
||||
- SEARCH_VECTOR_TOP_K=${SEARCH_VECTOR_TOP_K}
|
||||
- SEARCH_FULLTEXT_TOP_K=${SEARCH_FULLTEXT_TOP_K}
|
||||
- SEARCH_GRAPH_MAX_DEPTH=${SEARCH_GRAPH_MAX_DEPTH}
|
||||
- SEARCH_RRF_K=${SEARCH_RRF_K}
|
||||
- SEARCH_DEFAULT_LIMIT=${SEARCH_DEFAULT_LIMIT}
|
||||
- RERANKER_MAX_CANDIDATES=${RERANKER_MAX_CANDIDATES}
|
||||
- RERANKER_TIMEOUT=${RERANKER_TIMEOUT}
|
||||
# Logging
|
||||
- LOGGING_LEVEL=${LOGGING_LEVEL}
|
||||
- DJANGO_LOGGING_LEVEL=${DJANGO_LOGGING_LEVEL}
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- mnemosyne-media:/app/media
|
||||
@@ -67,11 +181,66 @@ services:
|
||||
start_period: 30s
|
||||
|
||||
# ── Celery worker: embedding + ingest + batch queues ───────────────────────
|
||||
# Consumer side of the queue. Needs the full S3 block (reads Daedalus's
|
||||
# bucket, writes to Mnemosyne's), the LLM API encryption key (ingest calls
|
||||
# vision models via stored provider keys), and both broker URL + result
|
||||
# backend. Does NOT need HTTP-layer settings (ALLOWED_HOSTS, CSRF, MCP auth)
|
||||
# or search tuning (the worker never serves queries).
|
||||
worker:
|
||||
image: git.helu.ca/r/mnemosyne:latest
|
||||
pull_policy: always
|
||||
command: ["worker"]
|
||||
env_file: mnemosyne/.env
|
||||
environment:
|
||||
# Django core (Celery imports settings)
|
||||
- DJANGO_SETTINGS_MODULE=mnemosyne.settings
|
||||
- SECRET_KEY=${SECRET_KEY}
|
||||
- DEBUG=${DEBUG}
|
||||
- TIME_ZONE=${TIME_ZONE}
|
||||
- LANGUAGE_CODE=${LANGUAGE_CODE}
|
||||
# Postgres
|
||||
- APP_DB_NAME=${APP_DB_NAME}
|
||||
- APP_DB_USER=${APP_DB_USER}
|
||||
- APP_DB_PASSWORD=${APP_DB_PASSWORD}
|
||||
- DB_HOST=${DB_HOST}
|
||||
- DB_PORT=${DB_PORT}
|
||||
# Neo4j (graph writes during embed/ingest)
|
||||
- NEOMODEL_NEO4J_BOLT_URL=${NEOMODEL_NEO4J_BOLT_URL}
|
||||
# Memcached (task progress cache)
|
||||
- KVDB_LOCATION=${KVDB_LOCATION}
|
||||
- KVDB_PREFIX=${KVDB_PREFIX}
|
||||
# S3 — Mnemosyne's own bucket (chunk text writes, item file storage)
|
||||
- USE_LOCAL_STORAGE=${USE_LOCAL_STORAGE}
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
|
||||
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
|
||||
- AWS_STORAGE_BUCKET_NAME=${AWS_STORAGE_BUCKET_NAME}
|
||||
- AWS_S3_ENDPOINT_URL=${AWS_S3_ENDPOINT_URL}
|
||||
- AWS_S3_USE_SSL=${AWS_S3_USE_SSL}
|
||||
- AWS_S3_VERIFY=${AWS_S3_VERIFY}
|
||||
- AWS_S3_REGION_NAME=${AWS_S3_REGION_NAME}
|
||||
# Daedalus S3 — cross-bucket reads for ingest (worker-only)
|
||||
- DAEDALUS_S3_ENDPOINT_URL=${DAEDALUS_S3_ENDPOINT_URL}
|
||||
- DAEDALUS_S3_ACCESS_KEY_ID=${DAEDALUS_S3_ACCESS_KEY_ID}
|
||||
- DAEDALUS_S3_SECRET_ACCESS_KEY=${DAEDALUS_S3_SECRET_ACCESS_KEY}
|
||||
- DAEDALUS_S3_BUCKET_NAME=${DAEDALUS_S3_BUCKET_NAME}
|
||||
- DAEDALUS_S3_REGION_NAME=${DAEDALUS_S3_REGION_NAME}
|
||||
- DAEDALUS_S3_USE_SSL=${DAEDALUS_S3_USE_SSL}
|
||||
- DAEDALUS_S3_VERIFY=${DAEDALUS_S3_VERIFY}
|
||||
# Celery / RabbitMQ
|
||||
- CELERY_BROKER_URL=${CELERY_BROKER_URL}
|
||||
- CELERY_RESULT_BACKEND=${CELERY_RESULT_BACKEND}
|
||||
- CELERY_TASK_ALWAYS_EAGER=${CELERY_TASK_ALWAYS_EAGER}
|
||||
# Worker tuning (entrypoint.sh reads these)
|
||||
- CELERY_QUEUES=${CELERY_QUEUES}
|
||||
- CELERY_CONCURRENCY=${CELERY_CONCURRENCY}
|
||||
- CELERY_LOG_LEVEL=${CELERY_LOGGING_LEVEL}
|
||||
# LLM API secrets (ingest vision pass decrypts stored provider keys)
|
||||
- LLM_API_SECRETS_ENCRYPTION_KEY=${LLM_API_SECRETS_ENCRYPTION_KEY}
|
||||
# Embedding pipeline
|
||||
- EMBEDDING_BATCH_SIZE=${EMBEDDING_BATCH_SIZE}
|
||||
- EMBEDDING_TIMEOUT=${EMBEDDING_TIMEOUT}
|
||||
# Logging
|
||||
- LOGGING_LEVEL=${LOGGING_LEVEL}
|
||||
- CELERY_LOGGING_LEVEL=${CELERY_LOGGING_LEVEL}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
app:
|
||||
@@ -85,7 +254,9 @@ services:
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
|
||||
# ── Web: nginx reverse proxy, public port 23090 ───────────────────────────
|
||||
# ── Web: nginx reverse proxy, public port 23181 ────────────────────────────
|
||||
# No Django env — nginx only knows how to route. Public listener is
|
||||
# templated into the conf file by Ansible if the port ever needs to change.
|
||||
web:
|
||||
image: nginx:alpine
|
||||
restart: unless-stopped
|
||||
@@ -106,8 +277,9 @@ services:
|
||||
retries: 3
|
||||
|
||||
volumes:
|
||||
# Static files baked into the image at /app/staticfiles. The app service
|
||||
# mounts this volume, populating it on first start; nginx reads from it.
|
||||
# Static files baked into the image at /app/staticfiles. The static-init
|
||||
# service seeds this volume on every `up`, so nginx always serves the
|
||||
# current image's static bundle.
|
||||
mnemosyne-static:
|
||||
# Local FileSystemStorage fallback. Production uses USE_LOCAL_STORAGE=False
|
||||
# so this is mostly empty — kept for parity with dev and for any path
|
||||
|
||||
Reference in New Issue
Block a user