From 6d4d575e3a03d9ddeed51ee8395bfc59cb9e9e97 Mon Sep 17 00:00:00 2001 From: Eloi BERLINGER Date: Sun, 22 Mar 2026 10:48:10 +0100 Subject: [PATCH 1/8] Enhance production setup with Nginx and Vite integration - Updated docker-compose.prod.yml to include Nginx as a web server for the Vite build and proxy requests to the backend. - Added Dockerfile for the frontend to build the Vite application and serve it with Nginx. - Introduced nginx/default.conf for Nginx configuration to handle API and admin routes. - Modified backend settings to support proxy headers based on TRUST_PROXY environment variable. - Created .dockerignore for the frontend to exclude unnecessary files from the Docker context. --- backend/setrsoft/settings.py | 5 +++++ docker-compose.prod.yml | 21 +++++++++++++++++---- frontend/.dockerignore | 7 +++++++ frontend/Dockerfile | 21 +++++++++++++++++++++ frontend/nginx/default.conf | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 frontend/.dockerignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/nginx/default.conf diff --git a/backend/setrsoft/settings.py b/backend/setrsoft/settings.py index 5ed1f68..4ef33e8 100644 --- a/backend/setrsoft/settings.py +++ b/backend/setrsoft/settings.py @@ -19,6 +19,11 @@ h.strip() for h in os.environ.get('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',') if h.strip() ] +_TRUST_PROXY = os.environ.get('TRUST_PROXY', '').lower() in ('1', 'true', 'yes') +if _TRUST_PROXY: + USE_X_FORWARDED_HOST = True + SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') + INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 97aa198..df01fb3 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,5 +1,10 @@ -# Production stack: Gunicorn (Dockerfile CMD), no source bind mounts. -# Usage: docker compose -f docker-compose.prod.yml up -d --build +# Production stack: Nginx (web) serves the Vite build and proxies /api/ and /admin/ to Gunicorn. +# Backend is not published on the host; only port 80 (web) is exposed. +# +# Usage: POSTGRES_PASSWORD=... SECRET_KEY=... docker compose -f docker-compose.prod.yml up -d --build +# +# Smoke checks (with stack up): curl -sSf http://localhost/ +# curl -sSf http://localhost/api/health/ curl -sSf http://localhost/editor/ | head services: db: image: postgres:16-alpine @@ -19,8 +24,6 @@ services: build: context: ./backend dockerfile: Dockerfile - ports: - - "8000:8000" environment: POSTGRES_HOST: db POSTGRES_PORT: "5432" @@ -30,9 +33,19 @@ services: SECRET_KEY: ${SECRET_KEY:?Set SECRET_KEY in .env or the environment} DEBUG: ${DEBUG:-False} ALLOWED_HOSTS: ${ALLOWED_HOSTS:-localhost,127.0.0.1} + TRUST_PROXY: ${TRUST_PROXY:-1} depends_on: db: condition: service_healthy + web: + build: + context: ./frontend + dockerfile: Dockerfile + ports: + - "80:80" + depends_on: + - backend + volumes: postgres_data_prod: diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..b2fef1e --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +.git +.gitignore +*.md +.env +.env.* diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..446245d --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,21 @@ +# Production: build Vite SPA, serve with Nginx (see docker-compose.prod.yml service web). +FROM node:22-alpine AS build + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY . . + +ARG VITE_API_BASE= +ENV VITE_API_BASE=$VITE_API_BASE + +RUN npm run build + +FROM nginx:alpine + +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx/default.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 diff --git a/frontend/nginx/default.conf b/frontend/nginx/default.conf new file mode 100644 index 0000000..63bc246 --- /dev/null +++ b/frontend/nginx/default.conf @@ -0,0 +1,36 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Docker embedded DNS; variable proxy_pass defers resolution so nginx -t works without "backend" in /etc/hosts + resolver 127.0.0.11 valid=10s ipv6=off; + + gzip on; + gzip_types text/plain text/css application/javascript application/json image/svg+xml; + + location /api/ { + set $django_upstream http://backend:8000; + proxy_pass $django_upstream; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /admin/ { + set $django_upstream http://backend:8000; + proxy_pass $django_upstream; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + try_files $uri $uri/ /index.html; + } +} From fdabd63f00b3bf9e10625c128a7a270c65c5c53b Mon Sep 17 00:00:00 2001 From: Eloi BERLINGER Date: Sun, 22 Mar 2026 10:54:29 +0100 Subject: [PATCH 2/8] Add .env.example for environment variable management and update Docker Compose files --- .env.example | 32 +++++++++ README.md | 134 +++++++++++++++++++---------------- backend/.env.example | 11 --- backend/README.md | 16 ++--- backend/setrsoft/settings.py | 6 +- database/.env.example | 5 -- docker-compose.prod.yml | 9 ++- docker-compose.yml | 7 ++ 8 files changed, 128 insertions(+), 92 deletions(-) create mode 100644 .env.example delete mode 100644 backend/.env.example delete mode 100644 database/.env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d011f52 --- /dev/null +++ b/.env.example @@ -0,0 +1,32 @@ +# Copy to ".env" at the repository root (Docker Compose and local tooling load it from here). +# Django also loads this file via backend/setrsoft/settings.py (repo root). + +# --- Django --- +# Required in production. Use a long random string. +SECRET_KEY=your-secret-key + +# Development: True. Production: False. +DEBUG=True + +# Comma-separated hostnames Django may serve (no spaces). Include your domain in production. +ALLOWED_HOSTS=localhost,127.0.0.1 + +# Set to 1/true/yes when Django sits behind a reverse proxy (Nginx, load balancer) so +# USE_X_FORWARDED_HOST and X-Forwarded-Proto are honored. Typical in production. +TRUST_PROXY= + +# --- PostgreSQL (Django DATABASES + Docker "db" service) --- +POSTGRES_DB=setrsoft +POSTGRES_USER=setrsoft +POSTGRES_PASSWORD=changeme + +# Host running PostgreSQL: use "localhost" for Django on the host; Docker Compose overrides +# to "db" for the backend container. +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 + +# --- Frontend (Vite) --- +# Base URL for API requests from the browser. Leave empty when the SPA and API share the +# same origin (e.g. production Nginx serves / and proxies /api/ to Django). +# For local Vite (e.g. :5173) talking to Django on :8000, set e.g. http://localhost:8000 +VITE_API_BASE= diff --git a/README.md b/README.md index d2e7761..6cda2c2 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,87 @@ -# React + TypeScript + Vite +# SetterSoft -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +Monorepo: **Django** API (`backend/`), **React + Vite** app (`frontend/`), **PostgreSQL**. -Currently, two official plugins are available: +## Environment variables -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +All variables are listed in **`.env.example`** at the repository root. Before running Docker Compose, copy it once: -## React Compiler +```bash +cp .env.example .env +``` + +Compose loads **`.env`** automatically for `${VAR}` substitution in the YAML files, and each service uses **`env_file: .env`** so containers receive the same values. Django reads the same **`.env`** from the repo root when you run `manage.py` locally (see `backend/setrsoft/settings.py`). -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). +## Development (Docker) -## Expanding the ESLint configuration +From the repository root (after `cp .env.example .env`): + +```bash +docker compose up +``` -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: +This starts: -```js -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... +| Service | Role | URL / port | +| --------- | ---------------------------- | ----------------- | +| `db` | PostgreSQL | `localhost:5432` | +| `backend` | Django `runserver` (reload) | `http://localhost:8000` | +| `frontend`| Vite dev server (`npm run dev` in container) | `http://localhost:5173` | - // Remove tseslint.configs.recommended and replace with this - tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - tseslint.configs.stylisticTypeChecked, +Open the app at **http://localhost:5173**. Set **`VITE_API_BASE=http://localhost:8000`** in `.env` so the browser calls the API on port 8000 when the SPA is not served from the same origin. - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) +First-time backend setup (migrations, superuser) is usually run inside the backend container, for example: + +```bash +docker compose exec backend python manage.py migrate +docker compose exec backend python manage.py createsuperuser ``` -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: - -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) +## Production (Docker) + +Production uses **`docker-compose.prod.yml`**: Nginx serves the built SPA and proxies `/api/` and `/admin/` to Gunicorn. Only **port 80** is published; the database and Django are not exposed on the host. + +Required in **`.env`** at the repository root (or exported in your shell): + +- `POSTGRES_PASSWORD` +- `SECRET_KEY` + +See **`.env.example`** for the full list. Optional values such as `POSTGRES_DB`, `POSTGRES_USER`, `DEBUG`, `ALLOWED_HOSTS`, `TRUST_PROXY`, and **`VITE_API_BASE`** (passed as a Docker **build arg** for the `web` image when you need an absolute API URL in the built SPA) are documented there. + +**Start production stack** (build images, run detached): + +```bash +docker compose -f docker-compose.prod.yml up -d --build ``` + +With inline env (example): + +```bash +POSTGRES_PASSWORD=your-secure-password SECRET_KEY=your-django-secret-key docker compose -f docker-compose.prod.yml up -d --build +``` + +Then open **http://localhost** (or your server’s hostname). Use **`ALLOWED_HOSTS`** (and HTTPS + `TRUST_PROXY` as already set in compose) when deploying under a real domain. + +**Stop:** + +```bash +docker compose -f docker-compose.prod.yml down +``` + +## Local frontend without Docker + +You can still run Vite on the host: + +```bash +cd frontend && npm install && npm run dev +``` + +Use this if you prefer not to use the `frontend` service from `docker compose up`. + +## Project layout + +- `.env.example` — template for all services (Django, PostgreSQL, Vite) +- `backend/` — Django project (`setrsoft`), API under `/api/` +- `frontend/` — Vite + React SPA; production image builds static assets and serves them with Nginx +- `docker-compose.yml` — development +- `docker-compose.prod.yml` — production diff --git a/backend/.env.example b/backend/.env.example deleted file mode 100644 index 266d031..0000000 --- a/backend/.env.example +++ /dev/null @@ -1,11 +0,0 @@ -# Django -SECRET_KEY=your-secret-key -DEBUG=True -ALLOWED_HOSTS=localhost,127.0.0.1 - -# PostgreSQL (used by Django and by the db service in docker-compose) -POSTGRES_DB=setrsoft -POSTGRES_USER=setrsoft -POSTGRES_PASSWORD=changeme -POSTGRES_HOST=localhost -POSTGRES_PORT=5432 diff --git a/backend/README.md b/backend/README.md index 85e24fb..499aa7b 100644 --- a/backend/README.md +++ b/backend/README.md @@ -20,22 +20,14 @@ pip install -r requirements.txt ## Environment variables -Copy the example file and set your values: +Use the **repository root** template and file: ```bash +# From the repository root (not inside backend/) cp .env.example .env ``` -| Variable | Description | -|----------|-------------| -| `SECRET_KEY` | Django secret key (required in production). | -| `DEBUG` | Set to `True` for development, `False` in production. | -| `ALLOWED_HOSTS` | Comma-separated list of allowed hosts (e.g. `localhost,127.0.0.1`). | -| `POSTGRES_DB` | PostgreSQL database name. | -| `POSTGRES_USER` | PostgreSQL user. | -| `POSTGRES_PASSWORD` | PostgreSQL password. | -| `POSTGRES_HOST` | PostgreSQL host (`localhost` when running locally, `db` when using Docker). | -| `POSTGRES_PORT` | PostgreSQL port (default `5432`). | +Variable names and descriptions live in **`/.env.example`**. Django loads **`/.env`** via `setrsoft/settings.py` (`REPO_ROOT / '.env'`). ## Database @@ -57,7 +49,7 @@ The API will be available at `http://localhost:8000/`. Health check: `http://loc ## Docker (optional) -From the repository root, ensure `backend/.env` exists (copy from `backend/.env.example` and set `POSTGRES_HOST=db` for the backend service, or use the defaults which point to the `db` service). +From the repository root, ensure **`.env`** exists (copy from `.env.example`). Docker Compose sets `POSTGRES_HOST=db` inside the backend container; keep `POSTGRES_HOST=localhost` in `.env` for running Django on the host against a local PostgreSQL instance. Start both the database and the backend: diff --git a/backend/setrsoft/settings.py b/backend/setrsoft/settings.py index 4ef33e8..e97cb36 100644 --- a/backend/setrsoft/settings.py +++ b/backend/setrsoft/settings.py @@ -8,10 +8,10 @@ import dotenv -# Load .env from backend directory (when running from repo root or backend/) +# Load .env from repository root (single source of truth; see root .env.example). BASE_DIR = Path(__file__).resolve().parent.parent -env_path = BASE_DIR / '.env' -dotenv.load_dotenv(env_path) +REPO_ROOT = BASE_DIR.parent +dotenv.load_dotenv(REPO_ROOT / '.env') SECRET_KEY = os.environ.get('SECRET_KEY', 'django-insecure-change-me') DEBUG = os.environ.get('DEBUG', 'True').lower() in ('true', '1', 'yes') diff --git a/database/.env.example b/database/.env.example deleted file mode 100644 index a26c189..0000000 --- a/database/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -# PostgreSQL environment for the db service (docker-compose). -# Use the same values as in backend/.env for consistency. -POSTGRES_DB=setrsoft -POSTGRES_USER=setrsoft -POSTGRES_PASSWORD=changeme diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index df01fb3..adcc325 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,13 +1,16 @@ # Production stack: Nginx (web) serves the Vite build and proxies /api/ and /admin/ to Gunicorn. # Backend is not published on the host; only port 80 (web) is exposed. # -# Usage: POSTGRES_PASSWORD=... SECRET_KEY=... docker compose -f docker-compose.prod.yml up -d --build +# Usage: cp .env.example .env, set POSTGRES_PASSWORD and SECRET_KEY, then: +# docker compose -f docker-compose.prod.yml up -d --build # # Smoke checks (with stack up): curl -sSf http://localhost/ # curl -sSf http://localhost/api/health/ curl -sSf http://localhost/editor/ | head services: db: image: postgres:16-alpine + env_file: + - .env environment: POSTGRES_DB: ${POSTGRES_DB:-setrsoft} POSTGRES_USER: ${POSTGRES_USER:-setrsoft} @@ -24,6 +27,8 @@ services: build: context: ./backend dockerfile: Dockerfile + env_file: + - .env environment: POSTGRES_HOST: db POSTGRES_PORT: "5432" @@ -42,6 +47,8 @@ services: build: context: ./frontend dockerfile: Dockerfile + args: + VITE_API_BASE: ${VITE_API_BASE:-} ports: - "80:80" depends_on: diff --git a/docker-compose.yml b/docker-compose.yml index 18ae99e..916a08e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,8 @@ services: db: image: postgres:16-alpine + env_file: + - .env environment: POSTGRES_DB: ${POSTGRES_DB:-setrsoft} POSTGRES_USER: ${POSTGRES_USER:-setrsoft} @@ -21,6 +23,8 @@ services: build: context: ./backend dockerfile: Dockerfile + env_file: + - .env command: ["python", "manage.py", "runserver", "0.0.0.0:8000"] volumes: - ./backend:/app @@ -35,6 +39,7 @@ services: SECRET_KEY: ${SECRET_KEY:-dev-secret-key-change-in-production} DEBUG: ${DEBUG:-True} ALLOWED_HOSTS: ${ALLOWED_HOSTS:-localhost,127.0.0.1} + TRUST_PROXY: ${TRUST_PROXY:-} depends_on: db: condition: service_healthy @@ -42,6 +47,8 @@ services: frontend: image: node:22-alpine working_dir: /app + env_file: + - .env volumes: - ./frontend:/app - frontend_node_modules:/app/node_modules From 0d107ab844012e4519eebece450d6d72213a7096 Mon Sep 17 00:00:00 2001 From: Eloi BERLINGER Date: Sun, 22 Mar 2026 11:00:20 +0100 Subject: [PATCH 3/8] Add WhiteNoise for static file handling in Django --- backend/Dockerfile | 5 +++++ backend/requirements.txt | 1 + backend/setrsoft/settings.py | 9 +++++++++ frontend/nginx/default.conf | 10 ++++++++++ 4 files changed, 25 insertions(+) diff --git a/backend/Dockerfile b/backend/Dockerfile index 7407c6f..8f9c139 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -10,6 +10,11 @@ COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . + +# Collect admin and app static files for WhiteNoise (no DB connection required). +ENV SECRET_KEY=collectstatic-build-placeholder +RUN python manage.py collectstatic --noinput + EXPOSE 8000 CMD ["gunicorn", "setrsoft.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "2"] diff --git a/backend/requirements.txt b/backend/requirements.txt index a295b2c..5451c8b 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -3,3 +3,4 @@ djangorestframework psycopg2-binary python-dotenv gunicorn +whitenoise diff --git a/backend/setrsoft/settings.py b/backend/setrsoft/settings.py index e97cb36..39446ea 100644 --- a/backend/setrsoft/settings.py +++ b/backend/setrsoft/settings.py @@ -37,6 +37,7 @@ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -92,6 +93,14 @@ STATIC_URL = 'static/' STATIC_ROOT = BASE_DIR / 'staticfiles' +STORAGES = { + 'default': { + 'BACKEND': 'django.core.files.storage.FileSystemStorage', + }, + 'staticfiles': { + 'BACKEND': 'whitenoise.storage.CompressedManifestStaticFilesStorage', + }, +} DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' REST_FRAMEWORK = { diff --git a/frontend/nginx/default.conf b/frontend/nginx/default.conf index 63bc246..285ed83 100644 --- a/frontend/nginx/default.conf +++ b/frontend/nginx/default.conf @@ -30,6 +30,16 @@ server { proxy_set_header X-Forwarded-Proto $scheme; } + location /static/ { + set $django_upstream http://backend:8000; + proxy_pass $django_upstream; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location / { try_files $uri $uri/ /index.html; } From e9f6c5a8f3007556607f2118b6fe84a30cda5d43 Mon Sep 17 00:00:00 2001 From: Eloi BERLINGER Date: Sun, 22 Mar 2026 12:09:21 +0100 Subject: [PATCH 4/8] feat: Implement a custom theme with new colors and the Inter font, updating the main layout and base styles. --- frontend/index.html | 3 +++ frontend/src/app/Root.tsx | 48 ++++++++++++++++++++------------------- frontend/src/index.css | 24 ++++++++++++++++++++ 3 files changed, 52 insertions(+), 23 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index e08d524..018651e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,6 +4,9 @@ + + + SetterSoft diff --git a/frontend/src/app/Root.tsx b/frontend/src/app/Root.tsx index ddccdba..e904b32 100644 --- a/frontend/src/app/Root.tsx +++ b/frontend/src/app/Root.tsx @@ -3,36 +3,38 @@ import { ROUTES, APP_TITLE } from '@/core/config'; export function Root() { return ( -
-
-