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:
93
Dockerfile
Normal file
93
Dockerfile
Normal 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"]
|
||||
Reference in New Issue
Block a user