diff --git a/CLAUDE.md b/CLAUDE.md index 4238ea6..6edb4ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co | Layer | Tech | |---|---| | Backend | FastAPI (async), SQLAlchemy 2 (async), Alembic, PostgreSQL | -| Auth | JWT via `python-jose`, bcrypt via `passlib` | +| Auth | JWT via `python-jose`, bcrypt via `bcrypt` (direct, no passlib) | | Frontend | React 18, TypeScript, Vite, React Router v6, TanStack Query, Axios | | Dev DB | PostgreSQL 16 via Docker Compose | @@ -92,6 +92,21 @@ Browser → Vite dev server (:5173) Always run `git push` immediately after every `git commit`. +## Infrastructure change protocol + +After **any** change to Dockerfiles, `docker-compose*.yml`, `nginx.conf`, setup scripts, or installation / usage procedures: + +1. **Update `README.md`** — keep the Containers table, ports, image names, and Current State section accurate. +2. **Spin up the dev stack** and verify that login and registration work end-to-end: + ```bash + docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build + ``` +3. **Spin up the prod stack** and run the same checks: + ```bash + docker compose up --build -d + ``` +4. Confirm each container is running as a non-root user (`docker inspect --format '{{.Config.User}}'`). + ## Security hook A pre-commit hook lives in `.githooks/pre-commit` and runs `scripts/security_check.py` inside a Docker container. It is registered via `git config core.hooksPath .githooks` (already set in this repo). diff --git a/README.md b/README.md index 3caecde..181aaa5 100644 --- a/README.md +++ b/README.md @@ -16,17 +16,18 @@ A fullstack SaaS web application built with FastAPI, React, and PostgreSQL. - Protected dashboard route - `/api/users/me` — authenticated user info - 3 separate Docker containers: `db` (PostgreSQL), `backend` (FastAPI), `frontend` (nginx) +- All containers run as non-root users (UID 1001 for backend and frontend, UID 70 for db) - Dev environment seeds a test user automatically on startup (`test@example.com` / `Test123!`) - Password policy: min 8 chars, upper + lowercase, digit, special character, no common words - Pre-commit security hook (`scripts/security_check.py`) runs inside Docker on every commit ## Containers -| Container | Image | Port | Description | -|---|---|---|---| -| `db` | postgres:16-alpine | 5432 | PostgreSQL database | -| `backend` | custom (python:3.12-slim) | 8000 | FastAPI management API | -| `frontend` | custom (nginx:alpine) | 80 | React UI served by nginx | +| Container | Image | Port | User (UID:GID) | Description | +|---|---|---|---|---| +| `db` | postgres:16-alpine | 5432 | 70:70 (postgres) | PostgreSQL database | +| `backend` | custom (python:3.12-slim) | 8000 | 1001:1001 (appuser) | FastAPI management API | +| `frontend` | custom (nginxinc/nginx-unprivileged:alpine) | 80 | 1001:1001 (appuser) | React UI served by nginx (internal port 8080) | The frontend nginx container proxies `/api/*` to the backend container internally — no CORS headers needed in production. diff --git a/changelog/2026-04-13_rootless-containers.md b/changelog/2026-04-13_rootless-containers.md index 27c7e67..f9c6f59 100644 --- a/changelog/2026-04-13_rootless-containers.md +++ b/changelog/2026-04-13_rootless-containers.md @@ -15,7 +15,24 @@ All containers now run as non-root users with explicit UID:GID assignments enfor | `frontend` (prod) | `nginx` | `101:101` | Switched to `nginxinc/nginx-unprivileged:alpine`; listens on 8080 | | `frontend` (dev) | `appuser` | `1001:1001` | Created via `adduser` in builder stage | -## Files Modified +# 2026-04-13 — Frontend prod UID 1001, infra change protocol, README update + +**Timestamp:** 2026-04-13T01:00:00 + +## Summary + +Aligned frontend prod container to UID 1001 (same as all other app containers), added infrastructure change protocol to CLAUDE.md, updated README with container table and rootless note. Both dev and prod stacks verified working. + +## Files Modified (this entry) + +- `frontend/Dockerfile` — prod stage: added `USER root` + `addgroup`/`adduser` for appuser 1001:1001, `USER appuser`; removed stale 101 reference +- `docker-compose.yml` — frontend `user:` updated from `"101:101"` to `"1001:1001"` +- `CLAUDE.md` — added Infrastructure change protocol section; fixed stale passlib reference in stack table +- `README.md` — updated container table with `nginxinc/nginx-unprivileged:alpine`, UID columns, internal port note; added rootless note to Current State + +--- + +## Files Modified (previous entry) - `backend/Dockerfile` — added `groupadd`/`useradd` for appuser (1001:1001), `--chown` on all `COPY` directives, `USER appuser` - `frontend/Dockerfile` — builder stage: added `addgroup`/`adduser` for appuser (1001:1001), `USER appuser`; prod stage: switched to `nginxinc/nginx-unprivileged:alpine`, `EXPOSE 8080` diff --git a/docker-compose.yml b/docker-compose.yml index 8238a56..aa1867b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,7 +42,7 @@ services: context: ./frontend dockerfile: Dockerfile network: host - user: "101:101" # nginx user UID:GID in nginx-unprivileged:alpine + user: "1001:1001" restart: unless-stopped ports: - "80:8080" diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 298b265..517d67e 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -15,9 +15,16 @@ RUN npm ci COPY --chown=appuser:appuser . . RUN npm run build -# ── Stage 2: serve with nginx (unprivileged, UID 101) ───────────────────────── +# ── Stage 2: serve with nginx (unprivileged, UID 1001) ──────────────────────── FROM nginxinc/nginx-unprivileged:alpine +# nginx-unprivileged already sets USER nginx (101). Step up to root only for +# user creation, then drop back. All nginx writable paths go through /tmp +# (world-writable) so appuser can run the server without extra chowns. +USER root +RUN addgroup -g 1001 appuser && adduser -u 1001 -G appuser -D appuser +USER appuser + COPY --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf