feat(deploy): production docker compose stack + Gitea CI image build

Adds a complete deployment surface for production:

  Dockerfile               multi-stage 3.12-slim build, collectstatic
                           baked into the image, runs as non-root mnemosyne
                           uid/gid 1000.
  docker/entrypoint.sh     dispatches `web | mcp | worker | beat | migrate
                           | setup | shell` from a single image, so every
                           service in compose runs the same artifact.
  docker-compose.yaml      five services: static-init (one-shot copies
                           statics into the shared volume on every up),
                           web (gunicorn), mcp (uvicorn), worker (celery),
                           nginx. External services (Postgres, Neo4j,
                           RabbitMQ, S3, Memcached, embedder, reranker)
                           reached over the 10.10.0.0/24 internal network
                           and configured via mnemosyne/.env.
  nginx/mnemosyne.conf     reverse proxy: /library/* and /admin/* → web,
                           /mcp/* → mcp, /static/* → volume, /metrics
                           internal-network-only (127/8 + RFC1918), /healthz
                           proxies to /mcp/health for liveness probes.
  .gitea/workflows/        CVE scan + image build, image pushed to
                           git.helu.ca/r/mnemosyne. Trivy scans pyproject
                           extras (dev/test/lint/docs) and the built image.
  pyproject.toml           adds [test], [lint], [docs] extras so the CI
                           pip-compile step has something to resolve.

README documents the bring-up flow (`docker compose run --rm web migrate`,
then `setup`, then `up -d`), day-to-day commands, and the env-var values
that need adjusting for production (DEBUG=False, KVDB_LOCATION pointing
at the external memcached, AWS keys filled in, etc.).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-29 12:05:23 -04:00
parent 1cd556c3f6
commit 236d9e2e74
7 changed files with 547 additions and 0 deletions

93
Dockerfile Normal file
View File

@@ -0,0 +1,93 @@
# =============================================================================
# Mnemosyne — production image
# =============================================================================
# Multi-stage:
# builder installs Python deps and runs `collectstatic` once.
# runtime copies only the artifacts the running process needs.
#
# The same image runs three different processes (Django web, MCP server,
# Celery worker) — the compose file picks the command per service.
# =============================================================================
# ── Stage 1: builder ────────────────────────────────────────────────────────
FROM python:3.12-slim AS builder
# Build deps for psycopg, PyMuPDF, Pillow, cryptography, etc.
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
libffi-dev \
libssl-dev \
libjpeg-dev \
zlib1g-dev \
&& rm -rf /var/lib/apt/lists/*
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /build
# Install dependencies first (better layer caching).
COPY pyproject.toml README.md ./
COPY mnemosyne/ ./mnemosyne/
RUN pip install --upgrade pip \
&& pip install .
# Bake static files into the image. The env vars below are build-time-only
# stubs needed for settings.py to import without real infrastructure — they
# never reach the runtime image because this is the builder stage.
# Inlined into the RUN command (rather than ENV/ARG) so static analysis
# tools (Trivy) don't flag them as baked-in secrets.
ENV DJANGO_SETTINGS_MODULE=mnemosyne.settings \
DEBUG=False \
USE_LOCAL_STORAGE=True \
APP_DB_NAME=collectstatic \
APP_DB_USER=collectstatic
WORKDIR /build/mnemosyne
RUN SECRET_KEY=collectstatic-stub \
APP_DB_PASSWORD=collectstatic-stub \
python manage.py collectstatic --noinput --clear
# ── Stage 2: runtime ────────────────────────────────────────────────────────
FROM python:3.12-slim AS runtime
# Runtime libs for psycopg + PyMuPDF + Pillow + cryptography.
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
libjpeg62-turbo \
zlib1g \
libssl3 \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
DJANGO_SETTINGS_MODULE=mnemosyne.settings \
PATH=/usr/local/bin:$PATH
# Copy installed packages from the builder.
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
# Application code + collected statics.
WORKDIR /app
COPY --from=builder /build/mnemosyne /app
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
# Non-root user for everything that runs in this image. uid:gid 1000:1000
# matches the convention for a single-application container.
RUN groupadd --gid 1000 mnemosyne \
&& useradd --uid 1000 --gid mnemosyne --home /app --no-create-home --shell /sbin/nologin mnemosyne \
&& mkdir -p /app/media /app/logs \
&& chown -R mnemosyne:mnemosyne /app
USER mnemosyne
# The compose file overrides this per service. Default = Django web.
EXPOSE 8000 22091
ENTRYPOINT ["entrypoint.sh"]
CMD ["web"]