register and sign in pages
This commit is contained in:
+38
-47
@@ -1,79 +1,70 @@
|
||||
# Production deployment
|
||||
|
||||
Unified build for the SAR Link portal monorepo. One `compose.yml` builds all
|
||||
services from the repo root, wiring together the two submodules:
|
||||
One `compose.yml` builds every service from the repo root:
|
||||
|
||||
- **`backend/`** — Django API (gunicorn on `:5000`, static via WhiteNoise)
|
||||
- **`frontend/`** — Next.js portal (standalone server on `:3000`)
|
||||
|
||||
A single **nginx** container is the only published entrypoint. The browser only
|
||||
ever talks to Next.js; Next.js reaches Django **server-side** over the compose
|
||||
network (`SARLINK_API_BASE_URL=http://backend:5000`). nginx only exposes
|
||||
Django's browser-facing surface — the admin, its static assets, and media.
|
||||
- **`backend/`** — Django API on gunicorn `:5000`, static via WhiteNoise
|
||||
- **`frontend/`** — Vite SPA, built to static files at image build time
|
||||
- **`web`** — the only published container: nginx serving the SPA and reverse
|
||||
proxying the API. No node or bun at runtime.
|
||||
|
||||
```
|
||||
host.com/ -> frontend (Next.js) # incl. its own /api/* route handlers
|
||||
host.com/admin/ -> backend (Django admin)
|
||||
host.com/static/ -> backend (WhiteNoise)
|
||||
host.com/media/ -> nginx (shared `media` volume)
|
||||
host.com/ -> nginx (SPA, history fallback to index.html)
|
||||
host.com/api/ -> backend (Django REST API)
|
||||
host.com/admin/ -> backend (Django admin)
|
||||
host.com/static/ -> backend (WhiteNoise: admin/DRF assets)
|
||||
host.com/media/ -> nginx (shared `media` volume)
|
||||
```
|
||||
|
||||
> `/api/` is **not** proxied to Django — it belongs to Next.js (NextAuth etc.).
|
||||
> Django's own `/api/...` is reached only internally via `SARLINK_API_BASE_URL`.
|
||||
Because nginx fronts both, the browser sees one origin and there is no CORS in
|
||||
production.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
| -------------------- | --------------------------------------------------- |
|
||||
| `compose.yml` | postgres + backend + frontend + nginx |
|
||||
| `api.Dockerfile` | Django image (collectstatic at build) |
|
||||
| `frontend.Dockerfile`| Next.js standalone image |
|
||||
| `nginx.Dockerfile` | nginx + `nginx.conf` |
|
||||
| `entrypoint.sh` | backend: wait for postgres, `migrate`, then gunicorn|
|
||||
| `nginx.conf` | front reverse proxy |
|
||||
| File | Purpose |
|
||||
| ----------------- | -------------------------------------------------------- |
|
||||
| `compose.yml` | database + backend + worker + web |
|
||||
| `api.Dockerfile` | Django image (collectstatic at build) |
|
||||
| `web.Dockerfile` | node builds `frontend/dist`, nginx serves it |
|
||||
| `nginx.conf` | SPA + API/admin/static/media routing, asset cache headers |
|
||||
| `entrypoint.sh` | backend: wait for postgres, `migrate`, then gunicorn |
|
||||
|
||||
## Configure
|
||||
|
||||
Fill each submodule's `.env` (copy from its `.env.example`); compose reads
|
||||
`backend/.env` and `frontend/.env`. For the compose network set:
|
||||
Fill `backend/.env` (copy from `backend/.env.example`). For the compose network:
|
||||
|
||||
**`backend/.env`**
|
||||
```
|
||||
DJANGO_DEBUG=False
|
||||
SECRET_KEY=<generate one>
|
||||
POSTGRES_HOST=database
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_DATABASE=sarlink
|
||||
POSTGRES_USER=sarlink
|
||||
POSTGRES_PASSWORD=changeme
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1,backend # + your public host
|
||||
CSRF_TRUSTED_ORIGINS=https://portal.example.com
|
||||
POSTGRES_PASSWORD=<strong>
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1,backend,portal.sarlink.net
|
||||
CSRF_TRUSTED_ORIGINS=https://portal.sarlink.net
|
||||
FRONTEND_URL=https://portal.sarlink.net
|
||||
SMS_API_URL=...
|
||||
SMS_API_KEY=...
|
||||
```
|
||||
|
||||
**`frontend/.env`**
|
||||
```
|
||||
SARLINK_API_BASE_URL=http://backend:5000
|
||||
NEXTAUTH_URL=https://portal.example.com
|
||||
NEXTAUTH_SECRET=...
|
||||
```
|
||||
The `POSTGRES_*` values also feed the `database` service through compose
|
||||
defaults, so keep them in sync or export them before `up`.
|
||||
|
||||
The `POSTGRES_*` values also feed the `database` service (via compose defaults),
|
||||
so keep them in sync — or export them in the shell before `up`.
|
||||
The frontend needs no runtime configuration: it calls a relative `/api/...`
|
||||
which nginx routes to the backend.
|
||||
|
||||
## Build & run
|
||||
|
||||
```sh
|
||||
docker compose -f .build/prod/compose.yml up -d --build
|
||||
```
|
||||
|
||||
The published site is on `http://localhost:8080` (remap the `nginx` port in
|
||||
`compose.yml` behind your TLS terminator). The backend runs migrations on
|
||||
startup; create an admin user once with:
|
||||
|
||||
```sh
|
||||
docker compose -f .build/prod/compose.yml exec backend python manage.py createsuperuser
|
||||
```
|
||||
|
||||
The site is on `http://localhost:8080`; remap the `web` port behind your TLS
|
||||
terminator. Migrations run on backend startup.
|
||||
|
||||
## Running from published images
|
||||
|
||||
The build pushes to `git.shihaam.dev/sarlink/sarlinkportal/{backend,frontend,nginx}`.
|
||||
To deploy without building, replace each service's `build:` block with its
|
||||
`image:` and keep the `database`, volumes, `env_file`, and `nginx` port mapping.
|
||||
The build pushes `git.shihaam.dev/sarlink/sarlinkportal/{backend,web}`. To deploy
|
||||
without building, replace each service's `build:` block with its `image:` and
|
||||
keep `database`, the volumes, `env_file` and the `web` port mapping.
|
||||
|
||||
+23
-13
@@ -2,12 +2,18 @@ services:
|
||||
database:
|
||||
image: postgres:16
|
||||
hostname: database
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DATABASE:-sarlink}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-sarlink}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme}
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-sarlink}"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
backend:
|
||||
build:
|
||||
@@ -15,36 +21,40 @@ services:
|
||||
dockerfile: .build/prod/api.Dockerfile
|
||||
hostname: backend
|
||||
image: git.shihaam.dev/sarlink/sarlinkportal/backend
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ../../backend/.env
|
||||
volumes:
|
||||
- media:/app/media
|
||||
depends_on:
|
||||
- database
|
||||
database:
|
||||
condition: service_healthy
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ../../
|
||||
dockerfile: .build/prod/frontend.Dockerfile
|
||||
hostname: frontend
|
||||
image: git.shihaam.dev/sarlink/sarlinkportal/frontend
|
||||
# Background tasks: same image, procrastinate worker instead of gunicorn.
|
||||
worker:
|
||||
image: git.shihaam.dev/sarlink/sarlinkportal/backend
|
||||
restart: unless-stopped
|
||||
command: python manage.py procrastinate worker
|
||||
env_file:
|
||||
- ../../frontend/.env
|
||||
- ../../backend/.env
|
||||
volumes:
|
||||
- media:/app/media
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
nginx:
|
||||
# The only published container: static SPA + reverse proxy to the API.
|
||||
web:
|
||||
build:
|
||||
context: ../../
|
||||
dockerfile: .build/prod/nginx.Dockerfile
|
||||
hostname: nginx
|
||||
image: git.shihaam.dev/sarlink/sarlinkportal/nginx
|
||||
dockerfile: .build/prod/web.Dockerfile
|
||||
hostname: web
|
||||
image: git.shihaam.dev/sarlink/sarlinkportal/web
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- media:/app/media:ro
|
||||
depends_on:
|
||||
- frontend
|
||||
- backend
|
||||
|
||||
volumes:
|
||||
|
||||
Regular → Executable
@@ -1,27 +0,0 @@
|
||||
FROM node:22-slim AS builder
|
||||
|
||||
WORKDIR /var/www/html
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
RUN npm ci --legacy-peer-deps
|
||||
|
||||
COPY frontend/ .
|
||||
RUN npm run build
|
||||
|
||||
# ---- runtime ----
|
||||
# next.config.ts sets `output: "standalone"`, so we ship only the traced server.
|
||||
FROM node:22-slim AS runner
|
||||
|
||||
WORKDIR /var/www/html
|
||||
ENV NODE_ENV=production \
|
||||
NEXT_TELEMETRY_DISABLED=1 \
|
||||
HOSTNAME=0.0.0.0 \
|
||||
PORT=3000
|
||||
|
||||
COPY --from=builder /var/www/html/public ./public
|
||||
COPY --from=builder /var/www/html/.next/standalone ./
|
||||
COPY --from=builder /var/www/html/.next/static ./.next/static
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["node", "server.js"]
|
||||
@@ -1,3 +0,0 @@
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY .build/prod/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
+27
-18
@@ -1,17 +1,27 @@
|
||||
upstream frontend { server frontend:3000; }
|
||||
upstream backend { server backend:5000; }
|
||||
upstream backend { server backend:5000; }
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
access_log /dev/stdout;
|
||||
error_log /dev/stderr;
|
||||
|
||||
# Matches the frontend's serverActions bodySizeLimit (20mb).
|
||||
client_max_body_size 20M;
|
||||
|
||||
# --- Django admin + its static assets (served by WhiteNoise from gunicorn) ---
|
||||
# --- Django: API, admin, its static assets ---
|
||||
location /api/ {
|
||||
proxy_pass http://backend;
|
||||
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;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
location /admin/ {
|
||||
proxy_pass http://backend;
|
||||
proxy_set_header Host $host;
|
||||
@@ -19,31 +29,30 @@ server {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Django's own static files (admin, DRF, swagger) via WhiteNoise.
|
||||
location /static/ {
|
||||
proxy_pass http://backend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# --- Django-uploaded media (written to the shared `media` volume) ---
|
||||
# --- user uploads, from the shared `media` volume ---
|
||||
location /media/ {
|
||||
alias /app/media/;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# --- Next.js app (everything else, including its own /api/* route handlers) ---
|
||||
# The browser only ever talks to Next.js; Next.js reaches Django server-side
|
||||
# over the compose network via SARLINK_API_BASE_URL=http://backend:5000.
|
||||
# --- the SPA ---
|
||||
# Hashed bundles are immutable; the entry document must never be cached,
|
||||
# or a deploy leaves browsers asking for chunks that no longer exist.
|
||||
location /assets/ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://frontend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
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;
|
||||
# Honour the X-Accel-Buffering: no header the app sets for streamed responses.
|
||||
proxy_buffering off;
|
||||
try_files $uri $uri/ /index.html;
|
||||
add_header Cache-Control "no-cache";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# The SPA is built with node here and shipped as static files.
|
||||
# The runtime image is nginx only - no node, no bun.
|
||||
FROM node:22-slim AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY frontend/ .
|
||||
RUN npm run build
|
||||
|
||||
# ---- runtime ----
|
||||
FROM nginx:1.27-alpine
|
||||
|
||||
COPY .build/prod/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
EXPOSE 80
|
||||
Reference in New Issue
Block a user