# ============================================================================= # Stage 0: Ofelia binary # ============================================================================= FROM golang:1.25-bookworm@sha256:2c7c65601b020ee79db4c1a32ebee0bf3d6b298969ec683e24fcbea29305f10e AS ofelia-builder # Ofelia version (built from source for multi-arch support) ARG OFELIA_VERSION=0.3.12 ARG TARGETARCH ARG TARGETVARIANT RUN apt-get update && apt-get install -y --no-install-recommends \ git \ ca-certificates \ && rm -rf /var/lib/apt/lists/* WORKDIR /src/ofelia RUN git clone --depth 1 --branch "v${OFELIA_VERSION}" https://github.com/mcuadros/ofelia.git /src/ofelia RUN set -ex; \ if [ "$TARGETARCH" = "amd64" ]; then \ GOARCH="amd64"; \ elif [ "$TARGETARCH" = "arm64" ]; then \ GOARCH="arm64"; \ elif [ "$TARGETARCH" = "arm" ] && [ "$TARGETVARIANT" = "v7" ]; then \ GOARCH="arm"; \ GOARM="7"; \ else \ echo "Unsupported architecture: $TARGETARCH${TARGETVARIANT:+/$TARGETVARIANT}" && exit 1; \ fi; \ if [ -n "${GOARM:-}" ]; then \ export GOARM; \ fi; \ CGO_ENABLED=0 GOOS=linux GOARCH="$GOARCH" go build -o /usr/local/bin/ofelia . # ============================================================================= # Stage 1: Build dependencies # ============================================================================= FROM python:3.14-slim-bookworm@sha256:3be2c910db2dacfb3e576f94c7ffc07c10b115cbcd3de99d49bfb0b4ccfd75e7 AS builder # uv version and checksums (verified from GitHub releases) ARG UV_VERSION=0.9.24 ARG UV_SHA256_AMD64=fb13ad85106da6b21dd16613afca910994446fe94a78ee0b5bed9c75cd066078 ARG UV_SHA256_ARM64=9b291a1a4f2fefc430e4fc49c00cb93eb448d41c5c79edf45211ceffedde3334 ARG UV_SHA256_ARMV7=8d05b55fe2108ecab3995c2b656679a72c543fd9dc72eeb3a525106a709cfdcb ARG TARGETARCH ARG TARGETVARIANT # Install build dependencies for Python packages RUN apt-get update && apt-get install -y --no-install-recommends \ gcc \ libfreetype6-dev \ libpng-dev \ curl \ && rm -rf /var/lib/apt/lists/* # Download and verify uv binary in builder stage RUN set -ex; \ if [ "$TARGETARCH" = "amd64" ]; then \ UV_ARCH="x86_64"; \ UV_SHA256="$UV_SHA256_AMD64"; \ elif [ "$TARGETARCH" = "arm64" ]; then \ UV_ARCH="aarch64"; \ UV_SHA256="$UV_SHA256_ARM64"; \ elif [ "$TARGETARCH" = "arm" ] && [ "$TARGETVARIANT" = "v7" ]; then \ UV_ARCH="armv7"; \ UV_SHA256="$UV_SHA256_ARMV7"; \ else \ echo "Unsupported architecture: $TARGETARCH${TARGETVARIANT:+/$TARGETVARIANT}" && exit 1; \ fi; \ curl -fsSL "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${UV_ARCH}-unknown-linux-gnu.tar.gz" \ -o /tmp/uv.tar.gz \ && echo "${UV_SHA256} /tmp/uv.tar.gz" | sha256sum -c - \ && tar -xzf /tmp/uv.tar.gz -C /usr/local/bin --strip-components=1 --wildcards "*/uv" \ && rm /tmp/uv.tar.gz \ && chmod +x /usr/local/bin/uv # Create virtual environment RUN python -m venv /opt/venv ENV PATH="/opt/venv/bin:$PATH" \ UV_PROJECT_ENVIRONMENT=/opt/venv # Install Python dependencies COPY pyproject.toml uv.lock ./ RUN pip install --no-cache-dir --upgrade pip && \ uv sync --frozen --no-dev --no-install-project # ============================================================================= # Stage 2: Runtime # ============================================================================= FROM python:3.14-slim-bookworm@sha256:3be2c910db2dacfb3e576f94c7ffc07c10b115cbcd3de99d49bfb0b4ccfd75e7 # OCI Labels LABEL org.opencontainers.image.source="https://github.com/jorijn/meshcore-stats" LABEL org.opencontainers.image.description="MeshCore Stats - LoRa mesh network monitoring" LABEL org.opencontainers.image.licenses="MIT" # Install runtime dependencies # - tini: init system for proper signal handling # - libfreetype6, libpng16-16: matplotlib runtime libraries # - fontconfig, fonts-dejavu-core: fonts for chart text rendering RUN apt-get update && apt-get install -y --no-install-recommends \ tini \ libfreetype6 \ libpng16-16 \ fontconfig \ fonts-dejavu-core \ && rm -rf /var/lib/apt/lists/* \ # Build font cache for matplotlib && fc-cache -f \ # Remove setuid/setgid binaries for security && find / -perm /6000 -type f -exec chmod a-s {} \; 2>/dev/null || true # Create non-root user with dialout group for serial access RUN groupadd -g 1000 meshmon \ && useradd -u 1000 -g meshmon -G dialout -s /sbin/nologin meshmon \ && mkdir -p /data/state /out /tmp/matplotlib \ && chown -R meshmon:meshmon /data /out /tmp/matplotlib # Copy Ofelia binary from builder (keeps curl out of runtime image) COPY --from=ofelia-builder /usr/local/bin/ofelia /usr/local/bin/ofelia # Copy virtual environment from builder COPY --from=builder /opt/venv /opt/venv # Copy application code COPY --chown=meshmon:meshmon src/ /app/src/ COPY --chown=meshmon:meshmon scripts/ /app/scripts/ COPY --chown=meshmon:meshmon docker/ofelia.ini /app/ofelia.ini # Environment configuration # - PATH: Include venv so Ofelia can run Python # - PYTHONPATH: Allow imports from src/meshmon # - PYTHONUNBUFFERED: Ensure logs are output immediately # - PYTHONDONTWRITEBYTECODE: Don't create .pyc files # - MPLCONFIGDIR: Matplotlib font cache directory ENV PATH="/opt/venv/bin:$PATH" \ PYTHONPATH=/app/src \ PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ MPLCONFIGDIR=/tmp/matplotlib WORKDIR /app # Run as non-root user USER meshmon # Use tini as init system for proper signal handling ENTRYPOINT ["/usr/bin/tini", "--"] # Run Ofelia scheduler CMD ["ofelia", "daemon", "--config=/app/ofelia.ini"] # Health check - verify database is accessible HEALTHCHECK --interval=5m --timeout=30s --start-period=60s --retries=3 \ CMD python -c "import sqlite3; sqlite3.connect('/data/state/metrics.db').execute('SELECT 1')" || exit 1