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"]