9cb0311544
The backend Dockerfile only installed the [sqlcipher] extra, so paramiko wasn't on the path inside the container. SP25 + SP26's real-mode SFTP client needs paramiko to connect to the Gainwell MFT — the scheduler tick surfaced this as 'No module named paramiko' on the very first /api/admin/scheduler/tick against the dzinesco clearhouse with stub=false. Install both extras ([sqlcipher,sftp]) in builder and runtime stages.
84 lines
3.2 KiB
Docker
84 lines
3.2 KiB
Docker
# syntax=docker/dockerfile:1.7
|
|
#
|
|
# Cyclone backend — FastAPI on python:3.11-slim-bookworm with sqlcipher.
|
|
#
|
|
# Two-stage build:
|
|
# 1. builder — wheels the package with [sqlcipher] extra into /wheels.
|
|
# 2. runtime — slim base, tini PID 1, curl-based healthcheck.
|
|
#
|
|
# `sqlcipher` is preferred but the engine falls back to plain SQLite at
|
|
# runtime if the package isn't actually installed (see cyclone.db) — so a
|
|
# missing libsqlcipher-dev during build will fail loudly here rather than
|
|
# silently downgrading encryption in production.
|
|
|
|
# ---------- builder ----------
|
|
FROM python:3.11-slim-bookworm AS builder
|
|
|
|
ENV PIP_NO_CACHE_DIR=1 \
|
|
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
|
PYTHONDONTWRITEBYTECODE=1
|
|
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
build-essential \
|
|
libffi-dev \
|
|
libsqlcipher-dev \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
WORKDIR /build
|
|
|
|
# Copy the build manifest first so this layer caches across source edits.
|
|
COPY pyproject.toml ./
|
|
|
|
# Copy the full source tree, then build the wheel once. We deliberately
|
|
# avoid the "stub __init__.py, build wheel, then rebuild" pattern — it
|
|
# left stale `__init__.py` content in the wheel because pip wheel reuses
|
|
# the cached wheel metadata when the name+version matches. See git
|
|
# history on this file for the long version.
|
|
COPY src/ ./src/
|
|
# Install the sftp extra alongside sqlcipher so the real-mode SFTP
|
|
# client (paramiko) is available inside the container — required by
|
|
# SP25 + SP26 for live Gainwell MFT polling.
|
|
RUN pip wheel --no-cache-dir --wheel-dir /wheels '.[sqlcipher,sftp]'
|
|
|
|
# ---------- runtime ----------
|
|
FROM python:3.11-slim-bookworm
|
|
|
|
ENV PYTHONUNBUFFERED=1 \
|
|
PYTHONDONTWRITEBYTECODE=1
|
|
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
libsqlcipher-dev \
|
|
curl \
|
|
tini \
|
|
&& rm -rf /var/lib/apt/lists/* \
|
|
&& useradd --create-home --uid 1000 --shell /bin/bash cyclone
|
|
|
|
WORKDIR /app
|
|
|
|
COPY --from=builder /wheels /wheels
|
|
RUN pip install --no-cache-dir --no-index --find-links /wheels 'cyclone[sqlcipher,sftp]' \
|
|
&& rm -rf /wheels
|
|
|
|
# NOTE: we deliberately do NOT drop privileges to the `cyclone` user.
|
|
# Named volumes mount as root inside the container, and chown-ing them
|
|
# requires CAP_CHOWN (root). The standard hardened pattern is an
|
|
# entrypoint script that chowns as root then drops to the app user via
|
|
# gosu/su-exec — adds a dependency + an entrypoint file. For v1 we run
|
|
# as root inside the container; Docker's user-namespace remapping is
|
|
# the recommended host-level isolation. The `cyclone` user is created
|
|
# above and survives only so file ownership in bind mounts stays
|
|
# consistent. To harden later: install gosu + add an entrypoint script
|
|
# that does `chown -R cyclone:cyclone /var/lib/cyclone/... && exec gosu
|
|
# cyclone "$@"`.
|
|
|
|
EXPOSE 8000
|
|
|
|
# Container-level healthcheck — the compose service healthcheck is
|
|
# effectively a duplicate but the Docker `HEALTHCHECK` directive keeps
|
|
# `docker ps` honest without needing compose to be running.
|
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
|
|
CMD curl -fs http://localhost:8000/api/health || exit 1
|
|
|
|
ENTRYPOINT ["tini", "--"]
|
|
CMD ["python", "-m", "cyclone", "serve"]
|