From 02ad08b85aa91280765232ee73c61158f16bf29f Mon Sep 17 00:00:00 2001 From: Hermes Date: Fri, 9 Oct 2026 08:08:25 +0000 Subject: [PATCH] fix(containers): production frontend image, non-root, no dev-mode masking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First time the container path could be exercised (docker now installed) — it was badly broken. - frontend/Dockerfile ran 'npm run dev': a hot-reloading DEVELOPMENT server shipped as the production artifact, in an image that ran as root. Replaced with a three-stage Next.js standalone build (deps -> builder -> runner) that runs as the unprivileged 'node' user (trivy DS-0002 HIGH cleared). - docker-compose.yml masked the image three ways: 'command: [npm run dev]' overrode the image CMD, './frontend:/app' hid the built app, and an anonymous '/app/node_modules' volume shadowed the installed modules. In practice the frontend container crash-looped with 'sh: next: not found'. All three removed; the backend had the same './backend:/app' masking. - backend/Dockerfile: dropped apt entirely. Installing libssl-dev failed outright ('libssl3t64 u2' pinned by the index vs u3 offered). No compiler is needed — every dependency ships a manylinux wheel. Now python:3.12-slim matching the repo target, plus the missing HEALTHCHECK (DS-0026) that uses the bundled interpreter. - Added .dockerignore to both contexts: without them the 539MB node_modules, the .git dir and any local .env would be copied into the image. - compose ports bound to 127.0.0.1 instead of 0.0.0.0. - Removed the 'nethermind' username/naming from the built images in favour of netcore. Verified with real containers: - docker compose up -d -> backend healthy and frontend healthy within 10s - curl 127.0.0.1:8000/health 200; curl 127.0.0.1:3000/ 200 - docker exec nethermind-backend id -> uid=1000(netcore) - docker exec nethermind-frontend id -> uid=1000(node) - trivy config on both Dockerfiles -> 0 findings (was 1 HIGH + 2 LOW) - pytest 11 passed; gitleaks clean; compose down -v afterwards, 0 containers left --- backend/.dockerignore | 23 +++++++++++++++++++ backend/Dockerfile | 28 ++++++++++++++--------- docker-compose.yml | 9 ++------ frontend/.dockerignore | 30 ++++++++++++++++++++++++ frontend/Dockerfile | 52 +++++++++++++++++++++++++++++++++--------- 5 files changed, 113 insertions(+), 29 deletions(-) create mode 100644 backend/.dockerignore create mode 100644 frontend/.dockerignore diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..f2c55f5 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,23 @@ +# Never bake local state, secrets or VCS data into the backend image. +.env +.env.* +!.env.example + +.git +.gitignore +.github +*.md +Dockerfile* +docker-compose*.yml +.dockerignore + +# Local database + logs + caches +*.db +*.db-shm +*.db-wal +*.log +__pycache__ +*.pyc +.venv +venv +.pytest_cache diff --git a/backend/Dockerfile b/backend/Dockerfile index e6dc5b3..0f9ff98 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,15 +1,17 @@ -FROM python:3.11-slim +# ============================================================================= +# NetCore Backend — FastAPI + Uvicorn +# +# No apt-get at all: every dependency in requirements.txt ships a prebuilt +# manylinux wheel, so no compiler is needed. This keeps the image small, makes +# builds hermetic, and avoids depending on the base image's apt mirror (which is +# inconsistent on this host: the index pins libssl3t64 u2 while u3 is offered, +# so installing libssl-dev fails outright). +# ============================================================================= +FROM python:3.12-slim WORKDIR /app -# Install system dependencies -RUN apt-get update && apt-get install -y --no-install-recommends \ - gcc \ - libffi-dev \ - libssl-dev \ - && rm -rf /var/lib/apt/lists/* - -# Python dependencies +# Python dependencies (wheels only — no build toolchain required) COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt @@ -17,9 +19,13 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . # Create non-root user -RUN useradd -m -u 1000 nethermind && chown -R nethermind:nethermind /app -USER nethermind +RUN useradd -m -u 1000 netcore && chown -R netcore:netcore /app +USER netcore EXPOSE 8000 +# trivy DS-0026: healthcheck. Uses the bundled python so no curl/apt is needed. +HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=15s \ + CMD python -c "import urllib.request;urllib.request.urlopen('http://127.0.0.1:8000/health',timeout=5)" || exit 1 + CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/docker-compose.yml b/docker-compose.yml index 4a8accc..78812b1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: dockerfile: Dockerfile container_name: nethermind-backend ports: - - "8000:8000" + - "127.0.0.1:8000:8000" environment: - DATABASE_URL=${DATABASE_URL:-sqlite:///./switches.db} - OPENAI_API_KEY=${OPENAI_API_KEY} @@ -17,7 +17,6 @@ services: - CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:3000,http://localhost:5173} volumes: - nethermind-data:/app/data - - ./backend:/app restart: unless-stopped networks: - nethermind-network @@ -34,18 +33,14 @@ services: dockerfile: Dockerfile container_name: nethermind-frontend ports: - - "3000:3000" + - "127.0.0.1:3000:3000" environment: - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:8000} - volumes: - - ./frontend:/app - - /app/node_modules depends_on: - backend restart: unless-stopped networks: - nethermind-network - command: ["npm", "run", "dev"] # PostgreSQL (optional — uncomment for production) # postgres: diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..930f816 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,30 @@ +# Keep the build context small and prevent local state/secrets from reaching the image. +node_modules +.next +out +build +dist + +# Local env files — never bake these into an image +.env +.env.* +!.env.example + +# VCS + tooling +.git +.gitignore +.github +*.md +Dockerfile* +docker-compose*.yml +.dockerignore + +# Local databases and logs +*.db +*.db-shm +*.db-wal +*.log +__pycache__ +*.pyc +.venv +venv diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 6bd3c0d..0d87171 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,20 +1,50 @@ -# Development server (production build requires more memory) -FROM node:20-alpine AS runner +# ============================================================================= +# NetCore Frontend — Next.js production image (standalone output) +# +# Previously this image ran `npm run dev`: a hot-reloading development server +# shipped as the production artifact. It now does a real production build and +# runs as a non-root user (trivy DS-0002). +# +# `next.config.js` sets `output: 'standalone'`, so the runtime image only needs +# the traced server bundle — not node_modules. +# ============================================================================= +# ── Stage 1: dependencies ─────────────────────────────────────────────────── +FROM node:20-alpine AS deps WORKDIR /app - -ENV NODE_ENV=development -ENV NEXT_TELEMETRY_DISABLED=1 - -# Install dependencies COPY package.json package-lock.json* ./ RUN npm ci --prefer-offline -# Copy source +# ── Stage 2: build ────────────────────────────────────────────────────────── +FROM node:20-alpine AS builder +WORKDIR /app +ENV NEXT_TELEMETRY_DISABLED=1 +COPY --from=deps /app/node_modules ./node_modules COPY . . +# Next.js expects a public/ dir; this repo has none, so create an empty one to +# keep the standalone COPY below valid. +RUN mkdir -p public +RUN npm run build + +# ── Stage 3: runtime ──────────────────────────────────────────────────────── +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 + +# Non-root: the node:alpine image already ships an unprivileged `node` user (uid 1000). +COPY --from=builder --chown=node:node /app/public ./public +COPY --from=builder --chown=node:node /app/.next/standalone ./ +COPY --from=builder --chown=node:node /app/.next/static ./.next/static + +USER node -# Expose port EXPOSE 3000 -# Run dev server -CMD ["npm", "run", "dev"] +HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=20s \ + CMD wget --spider -q http://127.0.0.1:3000/ || exit 1 + +CMD ["node", "server.js"]