add prod and dev compose
This commit is contained in:
@@ -0,0 +1,79 @@
|
|||||||
|
# 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:
|
||||||
|
|
||||||
|
- **`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.
|
||||||
|
|
||||||
|
```
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
> `/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`.
|
||||||
|
|
||||||
|
## 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 |
|
||||||
|
|
||||||
|
## Configure
|
||||||
|
|
||||||
|
Fill each submodule's `.env` (copy from its `.env.example`); compose reads
|
||||||
|
`backend/.env` and `frontend/.env`. For the compose network set:
|
||||||
|
|
||||||
|
**`backend/.env`**
|
||||||
|
```
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
**`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 (via compose defaults),
|
||||||
|
so keep them in sync — or export them in the shell before `up`.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# netcat is used by entrypoint.sh to wait for postgres
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends netcat-openbsd \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY backend/requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY backend/ .
|
||||||
|
|
||||||
|
# Build the WhiteNoise manifest (staticfiles/staticfiles.json) so /static/ works.
|
||||||
|
RUN python manage.py collectstatic --noinput
|
||||||
|
|
||||||
|
COPY .build/prod/entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
CMD ["gunicorn", "apibase.wsgi:application", "--bind", "0.0.0.0:5000", "--workers", "4"]
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
services:
|
||||||
|
database:
|
||||||
|
image: postgres:16
|
||||||
|
hostname: database
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DATABASE:-sarlink}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-sarlink}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme}
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ../../
|
||||||
|
dockerfile: .build/prod/api.Dockerfile
|
||||||
|
hostname: backend
|
||||||
|
image: git.shihaam.dev/sarlink/sarlinkportal/backend
|
||||||
|
env_file:
|
||||||
|
- ../../backend/.env
|
||||||
|
volumes:
|
||||||
|
- media:/app/media
|
||||||
|
depends_on:
|
||||||
|
- database
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ../../
|
||||||
|
dockerfile: .build/prod/frontend.Dockerfile
|
||||||
|
hostname: frontend
|
||||||
|
image: git.shihaam.dev/sarlink/sarlinkportal/frontend
|
||||||
|
env_file:
|
||||||
|
- ../../frontend/.env
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
build:
|
||||||
|
context: ../../
|
||||||
|
dockerfile: .build/prod/nginx.Dockerfile
|
||||||
|
hostname: nginx
|
||||||
|
image: git.shihaam.dev/sarlink/sarlinkportal/nginx
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
- media:/app/media:ro
|
||||||
|
depends_on:
|
||||||
|
- frontend
|
||||||
|
- backend
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
|
media:
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -n "$POSTGRES_HOST" ]; then
|
||||||
|
echo "Waiting for postgres at $POSTGRES_HOST:${POSTGRES_PORT:-5432}..."
|
||||||
|
while ! nc -z "$POSTGRES_HOST" "${POSTGRES_PORT:-5432}"; do
|
||||||
|
sleep 0.2
|
||||||
|
done
|
||||||
|
echo "PostgreSQL is up"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Applying database migrations..."
|
||||||
|
python manage.py migrate --noinput
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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"]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
COPY .build/prod/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
upstream frontend { server frontend:3000; }
|
||||||
|
upstream backend { server backend:5000; }
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
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) ---
|
||||||
|
location /admin/ {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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) ---
|
||||||
|
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.
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
include:
|
||||||
|
- frontend/compose.yml
|
||||||
|
- backend/compose.yml
|
||||||
Reference in New Issue
Block a user