Production deployment hardening: rootless Docker, single-origin Apache proxy, CSP - #1
Open
mantkiew wants to merge 8 commits into
Open
Production deployment hardening: rootless Docker, single-origin Apache proxy, CSP#1mantkiew wants to merge 8 commits into
mantkiew wants to merge 8 commits into
Conversation
Deployment improvements: - Remove CORSMiddleware from FastAPI: CORS is unnecessary when Apache collapses both frontend (:3000) and API (:8000) into a single origin. - Use Next.js dev proxy for API calls: frontend dev now routes through Next.js server instead of direct cross-origin requests, eliminating CORS in all environments. - Switch NEXT_PUBLIC_API_BASE_URL to path-only prefix (/DriverQ): build is now host-independent and portable across deployment domains. - Remove ALLOWED_ORIGINS env var and configuration. Apache hardening: - Add ProxyRequests Off to prevent forward-proxy abuse. - Add ServerTokens Prod and ServerSignature Off to reduce information leakage in Server header and error pages. - Add Content-Security-Policy header with strict defaults: - default-src 'none' baseline - script-src 'self' 'unsafe-inline' (Next.js RSC streaming limitation) - style-src 'self' 'unsafe-inline' (React inline styles) - connect-src 'self' (strict: no exfiltration to external origins) - All other resources (img, font, worker, object) locked to 'self' or 'none'. Documentation: - Clarify that loopback binding (127.0.0.1) is the real defense, not UFW rules. - Explain that CORS is a browser check, not relevant when behind a reverse proxy.
Apache was proxying / as a catch-all to the Next.js container, which would silently swallow requests for other unrelated sites hosted on the same Apache instance. Replace it with an explicit prefix for each service, matching the existing /DriverQ pattern already used for the API. - Add Next.js support (frontend/next.config.ts) via a new BASE_PATH build arg (frontend/Dockerfile, docker-compose.yml), defaulting to /DriverQ for the frontend. - Move the API to /DriverQServer (was /DriverQ) so the two prefixes are visually distinct; NEXT_PUBLIC_API_BASE_URL now defaults to /DriverQServer. - Update Apache config: /DriverQServer strips its prefix before forwarding to FastAPI (routes are prefix-unaware, JSON has no self-referencing URLs). /DriverQ preserves its prefix when forwarding to Next.js, since basePath makes the app itself prefix-aware (HTML/JS/RSC payloads embed /DriverQ/... URLs that Apache cannot rewrite in response bodies). Document why this asymmetry is intentional, and fix a stale comment that incorrectly said the frontend prefix was stripped. - Update architecture diagram to show both explicit prefixes plus other co-hosted sites passing through untouched. - docker-compose.yml / docker_setup.md: give both env vars sensible defaults so works without manual configuration; document how to override either prefix.
There was a problem hiding this comment.
Pull request overview
Hardens the production deployment to run behind a single Apache origin with explicit per-service URL prefixes, reducing cross-site interference and tightening container/runtime security.
Changes:
- Removed CORS middleware and shifted dev API access to same-origin proxying via Next.js rewrites.
- Added explicit URL prefixes (
/DriverQfrontend viabasePath,/DriverQServerAPI) and updated Compose build args accordingly. - Added deployment hardening docs/config guidance (Apache proxy rules, CSP/security headers, loopback-only port binding, and container privilege reductions).
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
server/Dockerfile |
Drops root by introducing an unprivileged runtime user for the API container. |
server/app.py |
Removes CORS middleware now that production is same-origin behind Apache. |
frontend/next.config.ts |
Adds basePath support and keeps dev rewrites for local proxying. |
frontend/Dockerfile |
Adds BASE_PATH build arg and switches the runtime to a non-root user. |
frontend/app/lib/api.ts |
Uses relative API URLs by default to support same-origin routing/proxying. |
docker-compose.yml |
Binds services to 127.0.0.1 and adds container hardening options; updates build args for prefixes. |
docker_setup.md |
Updates local setup docs for new/removed env vars and prefix defaults. |
apache-setup.md |
New production Apache configuration guidance: explicit ProxyPass prefixes, CSP, and hardening notes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- next.config.ts: dev API proxy target now falls back to localhost:8000 when NEXT_PUBLIC_API_BASE_URL is not an absolute URL (it's path-only in production), instead of producing an invalid rewrite destination. - frontend/Dockerfile: set BASE_PATH in the runner stage too, since next.config.ts is reloaded at runtime and would otherwise serve at "/" instead of the built-in prefix. Replace RUN chown -R with COPY --chown on each layer to avoid a redundant, size-doubling copy. - server/Dockerfile: chown /app to appuser before switching users, so runtime writes under the working directory don't fail. - docker_setup.md: document that the app is served at http://localhost:3000/DriverQ/ (not /) given the BASE_PATH default.
Switch ObjectInspector from a hardcoded /api/... fetch to the shared apiUrl(...) helper so production requests are correctly prefixed (for example /DriverQServer/api/...) when served behind Apache path-based proxying. Prevents object detail fetch failures caused by bypassing the configured API base path.
Connect to scene_data.db via a URI with mode=ro&immutable=1 instead of a plain path. WAL mode is persisted in the database file header, so a normal connection attempts locking/WAL reconciliation on open — which fails with 'attempt to write a readonly database' when the file is mounted read-only in Docker. immutable=1 tells SQLite the file will not change, skipping all locking and change detection, so read queries succeed against the read-only mount.
Collaborator
Author
|
@bluebarryz I have the deployment working: https://wiselab.uwaterloo.ca/DriverQ/ |
- next.config.ts: validate BASE_PATH eagerly (must be empty or start with '/' and not end with '/'), matching Next.js's own basePath constraints, so misconfiguration fails fast with a clear error instead of a confusing downstream routing issue. - docker_setup.md: document that 'docker compose up' alone does not route /DriverQServer/* to the api container — that's Apache's job in production (apache-setup.md). Without a reverse proxy in front, API calls will 404 against the frontend container. Add guidance for testing locally without Apache (local reverse proxy, or `npm run dev` + 'docker compose up api'). Addresses the two still-open threads from the Copilot review on PR #1; the other four (dev proxy fallback, BASE_PATH in Dockerfile runner stage, COPY --chown, server/Dockerfile ownership) were already fixed in earlier commits.
Collaborator
Author
|
@bluebarryz I noticed that there are camera images missing for the cut-in scenarios (all others have images). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Hardens the production deployment for a single-host Apache + rootless Docker setup, and gives the frontend its own explicit URL prefix so it coexists safely with other sites on the same Apache instance.
Changes
Remove CORS (no longer needed)
https://wiselab.uwaterloo.ca), so cross-origin requests never occur in production.CORSMiddlewareandALLOWED_ORIGINSfromserver/app.py,docker-compose.yml, anddocker_setup.md.frontend/next.config.tsrewrites) instead of making direct cross-origin requests tolocalhost:8000.Explicit path prefixes (avoid catch-all proxy)
/(catch-all) to the Next.js container, which would have silently intercepted requests to other unrelated sites hosted on the same Apache instance (e.g./t3-necsis/)./DriverQ, via Next.js'sbasePathconfig (newBASE_PATHbuild arg)./DriverQServer(was/DriverQ) so the two prefixes are visually distinct./DriverQServerprefix before forwarding to FastAPI (its routes are prefix-unaware and JSON responses have no self-referencing URLs), but preserves the/DriverQprefix when forwarding to Next.js, sincebasePathmakes the app itself prefix-aware (its HTML/JS/RSC payloads embed/DriverQ/...URLs that Apache cannot rewrite in response bodies). This asymmetry is documented inapache-setup.md.NEXT_PUBLIC_API_BASE_URLandBASE_PATHare now path-only (no hostname baked into the build), making the Docker image portable across deployment hosts.Apache hardening
ProxyRequests Off— disables forward-proxy behavior.ServerTokens Prod/ServerSignature Off— suppress Apache version disclosure in headers and error pages (documented as global directives, not per-vhost).Content-Security-Policyheader with adefault-src 'none'baseline.connect-src 'self'is fully strict (blocks exfiltration to external origins from any injected script).script-src/style-srcrequire'unsafe-inline'due to Next.js RSC streaming hydration and React inlinestyle={{}}props — documented as a known limitation with a follow-up path (nonce-based CSP via Next.js middleware).Firewall / network exposure clarification
ufw deny 8000/tcp/ufw deny 3000/tcprules fromapache-setup.md. The real protection is the existing127.0.0.1loopback binding indocker-compose.yml, which makes those ports unreachable from any external network interface — there's nothing for UFW to block. (Also noted: on root Docker, UFW rules for published ports are ineffective anyway since Docker inserts its owniptablesrules ahead of UFW's chains; this deployment uses rootless Docker where UFW rules would apply, but they're redundant given the loopback binding.)Files changed
server/app.py— remove CORS middlewarefrontend/app/lib/api.ts— use relative URLs in dev to route through Next.js proxyfrontend/next.config.ts— addbasePathsupportfrontend/Dockerfile— addBASE_PATHbuild argdocker-compose.yml— dropALLOWED_ORIGINS, addBASE_PATHdefault, rename API prefix defaultdocker_setup.md— update setup instructions for new/removed env varsapache-setup.md— single-origin architecture diagram, explicit per-serviceProxyPassprefixes, CSP header,ServerTokens/ServerSignature/ProxyRequestshardening, corrected firewall guidanceDeployment notes
When deploying this branch, the Apache vhost config in
apache-setup.mdmust be updated to match (new/DriverQServerAPI prefix, new/DriverQfrontend prefix with preserved path, new security headers). NoALLOWED_ORIGINSenv var needs to be set anymore.