# syntax=docker/dockerfile:1.7 # # neuronetz-gateway — multi-stage image. # # builder stage : installs dependencies into a self-contained virtualenv using uv. # runtime stage : copies the venv + source, drops to a NON-ROOT user, contains # no build tools, and runs `python -m neuronetz_gateway`. # # uv is pulled from the official distroless image so we don't need network access # to `pip install uv`. Dependencies come from pyproject.toml (+ uv.lock if present). # ---------------------------------------------------------------------------- # Stage 1 — builder # ---------------------------------------------------------------------------- FROM python:3.12-slim AS builder # Bring in the `uv` binary from its official image. COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv ENV UV_LINK_MODE=copy \ UV_COMPILE_BYTECODE=1 \ UV_PYTHON_DOWNLOADS=never \ # Create the project venv at a stable, copyable location. VIRTUAL_ENV=/opt/venv \ PATH=/opt/venv/bin:$PATH WORKDIR /app # Create the target virtualenv up front so uv installs into it. RUN uv venv /opt/venv # Dependency layer: copy only the manifest(s) first for better caching. # uv.lock is optional in Phase 1 — the wildcard makes COPY succeed either way. COPY pyproject.toml ./ COPY uv.loc[k] ./ # Install dependencies. If a lockfile is present `uv sync` honours it; otherwise # we fall back to resolving straight from pyproject.toml. Either way the build # does NOT fail when the lock is absent. RUN --mount=type=cache,target=/root/.cache/uv \ if [ -f uv.lock ]; then \ uv sync --frozen --no-install-project --no-dev ; \ else \ uv pip install --python /opt/venv/bin/python -r pyproject.toml ; \ fi # Now copy the application source and install the project itself into the venv. # README.md + LICENSE are required by the build backend (pyproject `readme`/license). COPY README.md LICENSE ./ COPY src ./src COPY alembi[c] ./alembic COPY alembic.in[i] ./ RUN --mount=type=cache,target=/root/.cache/uv \ uv pip install --python /opt/venv/bin/python --no-deps . # ---------------------------------------------------------------------------- # Stage 2 — runtime # ---------------------------------------------------------------------------- FROM python:3.12-slim AS runtime # Runtime-only OS packages: curl is used by the compose healthcheck. RUN apt-get update \ && apt-get install -y --no-install-recommends curl \ && rm -rf /var/lib/apt/lists/* # Non-root user. RUN groupadd --system --gid 10001 gateway \ && useradd --system --uid 10001 --gid gateway --home-dir /app --shell /usr/sbin/nologin gateway ENV VIRTUAL_ENV=/opt/venv \ PATH=/opt/venv/bin:$PATH \ PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ GATEWAY_BIND_HOST=0.0.0.0 \ GATEWAY_BIND_PORT=8080 WORKDIR /app # Copy the fully-populated virtualenv and the application from the builder. COPY --from=builder /opt/venv /opt/venv COPY --from=builder /app/src ./src # alembic assets are optional during early scaffolding; copy if present. COPY --from=builder /app/alembi[c] ./alembic COPY --from=builder /app/alembic.in[i] ./ # Drop privileges. No build tools are present in this stage. USER gateway EXPOSE 8080 # Liveness probe target lives at /healthz (see SPEC §6.4). HEALTHCHECK --interval=15s --timeout=3s --start-period=20s --retries=5 \ CMD curl -fsS "http://127.0.0.1:${GATEWAY_BIND_PORT}/healthz" || exit 1 # Default command: run the server. Compose overrides this in dev to run # `alembic upgrade head` first (see docker-compose.dev.yml). CMD ["python", "-m", "neuronetz_gateway"]