Implement rootless containers for all services
- backend: appuser UID/GID 1001 via useradd, USER directive, --chown on COPY - frontend builder: appuser UID/GID 1001 via adduser, USER directive - frontend prod: switch to nginxinc/nginx-unprivileged:alpine (nginx UID 101), listen on 8080 - docker-compose: explicit user: for all services (70:70 db, 1001:1001 backend/frontend-dev, 101:101 frontend-prod) - nginx.conf: listen 8080 to match unprivileged image Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
## Infrastructure
|
## Infrastructure
|
||||||
|
|
||||||
- [ ] **Rootless containers** — run backend and frontend containers as non-root users (add `USER` directive to Dockerfiles, map UID/GID appropriately)
|
- [x] **Rootless containers** — run backend and frontend containers as non-root users (add `USER` directive to Dockerfiles, map UID/GID appropriately)
|
||||||
- [ ] **Persistent storage** — ensure database data, config files, and any uploaded assets survive container restarts and rebuilds (named volumes, bind mounts for config)
|
- [ ] **Persistent storage** — ensure database data, config files, and any uploaded assets survive container restarts and rebuilds (named volumes, bind mounts for config)
|
||||||
- [ ] **Docker development workflow** — document and streamline the full dev loop: hot reload, one-command startup, migration handling, seed data, and how to attach a debugger
|
- [ ] **Docker development workflow** — document and streamline the full dev loop: hot reload, one-command startup, migration handling, seed data, and how to attach a debugger
|
||||||
|
|||||||
+10
-4
@@ -11,15 +11,21 @@ RUN pip install --prefix=/install .
|
|||||||
# ── Stage 2: runtime ──────────────────────────────────────────────────────────
|
# ── Stage 2: runtime ──────────────────────────────────────────────────────────
|
||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
# Create non-root user (UID/GID 1001)
|
||||||
|
RUN groupadd --gid 1001 appuser && \
|
||||||
|
useradd --uid 1001 --gid 1001 --no-create-home --shell /bin/sh appuser
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy installed packages from builder
|
# Copy installed packages from builder
|
||||||
COPY --from=builder /install /usr/local
|
COPY --from=builder /install /usr/local
|
||||||
|
|
||||||
# Copy application source
|
# Copy application source with correct ownership
|
||||||
COPY app ./app
|
COPY --chown=appuser:appuser app ./app
|
||||||
COPY alembic ./alembic
|
COPY --chown=appuser:appuser alembic ./alembic
|
||||||
COPY alembic.ini .
|
COPY --chown=appuser:appuser alembic.ini .
|
||||||
|
|
||||||
|
USER appuser
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# 2026-04-13 — Rootless containers
|
||||||
|
|
||||||
|
**Timestamp:** 2026-04-13T00:00:00
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
All containers now run as non-root users with explicit UID:GID assignments enforced in both Dockerfiles and docker-compose files.
|
||||||
|
|
||||||
|
## User mapping
|
||||||
|
|
||||||
|
| Service | User | UID:GID | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `db` | `postgres` | `70:70` | Fixed by `postgres:16-alpine`; image owns PGDATA as 70:70 so named-volume seeding works |
|
||||||
|
| `backend` | `appuser` | `1001:1001` | Created via `useradd` in runtime stage |
|
||||||
|
| `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
|
||||||
|
|
||||||
|
- `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`
|
||||||
|
- `frontend/nginx.conf` — changed `listen 80` → `listen 8080` to match unprivileged image default
|
||||||
|
- `docker-compose.yml` — added `user: "70:70"` to `db`, `user: "1001:1001"` to `backend`, `user: "101:101"` to `frontend`; updated frontend port mapping to `"80:8080"`
|
||||||
|
- `docker-compose.dev.yml` — added `user: "1001:1001"` to `backend` and `frontend` overrides
|
||||||
|
- `TODO.md` — marked rootless containers item as completed
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
|
user: "1001:1001"
|
||||||
command: sh scripts/start_dev.sh
|
command: sh scripts/start_dev.sh
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend:/app
|
- ./backend:/app
|
||||||
@@ -12,6 +13,7 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
target: builder # stop at the Node stage, skip nginx
|
target: builder # stop at the Node stage, skip nginx
|
||||||
|
user: "1001:1001"
|
||||||
command: npm run dev -- --host 0.0.0.0
|
command: npm run dev -- --host 0.0.0.0
|
||||||
ports:
|
ports:
|
||||||
- "5173:5173"
|
- "5173:5173"
|
||||||
|
|||||||
+4
-1
@@ -3,6 +3,7 @@ services:
|
|||||||
# ── Database ────────────────────────────────────────────────────────────────
|
# ── Database ────────────────────────────────────────────────────────────────
|
||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
|
user: "70:70" # postgres user UID:GID in alpine image (fixed by image)
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||||
@@ -24,6 +25,7 @@ services:
|
|||||||
context: ./backend
|
context: ./backend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
network: host
|
network: host
|
||||||
|
user: "1001:1001"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file: ./backend/.env
|
env_file: ./backend/.env
|
||||||
environment:
|
environment:
|
||||||
@@ -40,9 +42,10 @@ services:
|
|||||||
context: ./frontend
|
context: ./frontend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
network: host
|
network: host
|
||||||
|
user: "101:101" # nginx user UID:GID in nginx-unprivileged:alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:8080"
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
|
|
||||||
|
|||||||
+12
-6
@@ -1,18 +1,24 @@
|
|||||||
# ── Stage 1: build ────────────────────────────────────────────────────────────
|
# ── Stage 1: build ────────────────────────────────────────────────────────────
|
||||||
FROM node:20-alpine AS builder
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
# Create non-root user (UID/GID 1001)
|
||||||
|
RUN addgroup -g 1001 appuser && adduser -u 1001 -G appuser -s /bin/sh -D appuser
|
||||||
|
|
||||||
COPY package.json package-lock.json* ./
|
WORKDIR /app
|
||||||
|
RUN chown appuser:appuser /app
|
||||||
|
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
COPY --chown=appuser:appuser package.json package-lock.json* ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
COPY . .
|
COPY --chown=appuser:appuser . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# ── Stage 2: serve with nginx ─────────────────────────────────────────────────
|
# ── Stage 2: serve with nginx (unprivileged, UID 101) ─────────────────────────
|
||||||
FROM nginx:alpine
|
FROM nginxinc/nginx-unprivileged:alpine
|
||||||
|
|
||||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 8080
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 8080;
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user