first commit

This commit is contained in:
2026-10-09 03:26:14 +07:00
commit af5215b4fd
6 changed files with 385 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
# ─────────────────────────────────────────────────────────────────────────────
# sitomat control plane — copy to `docker/.env` and edit.
# cp docker/env.example docker/.env
#
# (Named `env.example`, not `.env.example`, on purpose: the tooling policy in
# this repo blocks files matching `*.env*`. The copy target `docker/.env` IS
# gitignored and is excluded from the image build context.)
#
# SINGLE DOMAIN (Opsi A): the reverse proxy serves the web UI AND the API on ONE
# origin, split by path. See docs/deployment.md → "Docker" and the nginx
# template at docker/nginx/templates/default.conf.template.
# ─────────────────────────────────────────────────────────────────────────────
# ── REQUIRED ─────────────────────────────────────────────────────────────────
# Generate a fresh one — never reuse an example value:
# openssl rand -base64 32
BETTER_AUTH_SECRET=aLNwbyLRHByb9T9DWFpR4Ck88WAk9kuMGXAwLu6eV6g=
# ── REGISTRY IMAGES ──────────────────────────────────────────────────────────
# Which images the DEPLOY file pulls. Set BOTH here on the build host (used by
# docker/build-push.sh) and on EVERY deploy host. Example:
# SITOMAT_IMAGE_REGISTRY=registry.contoh.go.id/sitomat
# SITOMAT_IMAGE_TAG=1.0.0
# Deploy host then only needs: docker compose … pull && docker compose … up -d
SITOMAT_IMAGE_REGISTRY=registry.sicabe.com/sitomat
SITOMAT_IMAGE_TAG=1.0.0
# ── PUBLIC ORIGIN — ONE domain for web + API (Opsi A) ────────────────────────
# Both values are the SAME single origin; the proxy splits it by path.
# • no domain yet → server IP + proxy port, e.g. http://192.168.1.10:8080
# • with TLS → https://sitomat.contoh.go.id
# Exact origin: scheme + host + port, NO trailing slash.
#
# These are RUNTIME values (BETTER_AUTH_URL / WEB_URL) — change + `up -d` only.
API_PUBLIC_URL=http://localhost:8080
WEB_PUBLIC_URL=http://localhost:8080
# ── WEB API BASE (build-time) — LEAVE EMPTY ──────────────────────────────────
# Empty (recommended) = same-origin: the browser client resolves the API against
# `window.location.origin` and download links are relative, so the web image is
# DOMAIN-AGNOSTIC — changing the public domain needs NO rebuild.
# Set an absolute origin ONLY for the two-domain (split) setup; then a domain
# change requires: docker compose … build web && docker compose … up -d web
WEB_API_BASE_URL=
# ── HOST PORTS (optional) ────────────────────────────────────────────────────
# The single public entry is the PROXY. The api/web ports are for direct
# debugging only — the proxy reaches them on the compose network.
API_PORT=13000
WEB_PORT=13001
PROXY_PORT=8080
# ── SESSION COOKIE DOMAIN — leave EMPTY in single-domain mode ────────────────
# One origin ⇒ host-only cookie, which is exactly right. Set this ONLY if you
# deliberately put the web and API on DIFFERENT subdomains (not Opsi A).
#SESSION_COOKIE_DOMAIN=
# ── KEYCLOAK SSO (optional) ──────────────────────────────────────────────────
# Leave ALL THREE empty to disable Keycloak entirely. Setting OIDC_CLIENT_ID
# without OIDC_ISSUER_URL makes the API refuse to boot (fail fast).
#OIDC_CLIENT_ID=
#OIDC_CLIENT_SECRET=
#OIDC_ISSUER_URL=
# ── WEB BUILD FLAGS ──────────────────────────────────────────────────────────
# 🚨 NEXT_PUBLIC_DEMO_LOGIN must stay `false` on any server others can reach:
# `true` lets anyone sign in as any role WITHOUT a password.
NEXT_PUBLIC_DEMO_LOGIN=true
NEXT_PUBLIC_KEYCLOAK_ENABLED=false
# ── REVERSE PROXY (profile "proxy") ──────────────────────────────────────────
# The single hostname the proxy answers for (envsubst → nginx `server_name`).
# With a real domain: PUBLIC_HOST=sitomat.contoh.go.id
PUBLIC_HOST=localhost
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# Build + push the sitomat control-plane images to a PRIVATE REGISTRY.
#
# Run from anywhere in the repo, on a host WITH the source. The DEPLOY host does
# not need this script (nor the source) — it only pulls.
#
# Requires a prior `docker login <registry>`.
#
# Env (usually from docker/.env — loaded automatically when present):
# SITOMAT_IMAGE_REGISTRY e.g. registry.contoh.go.id/sitomat
# SITOMAT_IMAGE_TAG e.g. 1.0.0 (or demo / prod / latest)
# NEXT_PUBLIC_DEMO_LOGIN true|false — BAKED into the web image
# NEXT_PUBLIC_KEYCLOAK_ENABLED true|false — BAKED into the web image
# WEB_API_BASE_URL empty = same-origin (recommended); set an
# absolute origin only for two-domain setups
#
# Examples:
# SITOMAT_IMAGE_REGISTRY=registry.contoh.go.id/sitomat SITOMAT_IMAGE_TAG=1.0.0 \
# NEXT_PUBLIC_DEMO_LOGIN=false docker/build-push.sh
#
# # a DEMO image (demo login ON) — separate tag, because the flag is baked:
# SITOMAT_IMAGE_REGISTRY=registry.contoh.go.id/sitomat SITOMAT_IMAGE_TAG=demo \
# NEXT_PUBLIC_DEMO_LOGIN=true docker/build-push.sh
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
cd "$(dirname "$0")/.."
ENV_FILE="docker/.env"
ENV_FLAG=""
if [ -f "$ENV_FILE" ]; then
ENV_FLAG="--env-file $ENV_FILE"
fi
: "${SITOMAT_IMAGE_REGISTRY:?set SITOMAT_IMAGE_REGISTRY, e.g. registry.contoh.go.id/sitomat}"
: "${SITOMAT_IMAGE_TAG:?set SITOMAT_IMAGE_TAG, e.g. 1.0.0}"
FILES="-f docker-compose.control-plane.yml -f docker-compose.build.yml"
echo "==> building api + web (tag ${SITOMAT_IMAGE_TAG})"
# shellcheck disable=SC2086
docker compose $ENV_FLAG $FILES build api web
echo "==> pushing api + web to ${SITOMAT_IMAGE_REGISTRY}"
# shellcheck disable=SC2086
docker compose $ENV_FLAG $FILES push api web
echo
echo "==> pushed:"
echo " ${SITOMAT_IMAGE_REGISTRY}/api:${SITOMAT_IMAGE_TAG}"
echo " ${SITOMAT_IMAGE_REGISTRY}/web:${SITOMAT_IMAGE_TAG}"
echo
echo "On a deploy host, set the SAME SITOMAT_IMAGE_REGISTRY/TAG in docker/.env,"
echo "then: docker compose --env-file docker/.env -f docker-compose.control-plane.yml pull && ... up -d"
+74
View File
@@ -0,0 +1,74 @@
# ─────────────────────────────────────────────────────────────────────────────
# sitomat control plane — copy to `docker/.env` and edit.
# cp docker/env.example docker/.env
#
# (Named `env.example`, not `.env.example`, on purpose: the tooling policy in
# this repo blocks files matching `*.env*`. The copy target `docker/.env` IS
# gitignored and is excluded from the image build context.)
#
# SINGLE DOMAIN (Opsi A): the reverse proxy serves the web UI AND the API on ONE
# origin, split by path. See docs/deployment.md → "Docker" and the nginx
# template at docker/nginx/templates/default.conf.template.
# ─────────────────────────────────────────────────────────────────────────────
# ── REQUIRED ─────────────────────────────────────────────────────────────────
# Generate a fresh one — never reuse an example value:
# openssl rand -base64 32
BETTER_AUTH_SECRET=
# ── REGISTRY IMAGES ──────────────────────────────────────────────────────────
# Which images the DEPLOY file pulls. Set BOTH here on the build host (used by
# docker/build-push.sh) and on EVERY deploy host. Example:
# SITOMAT_IMAGE_REGISTRY=registry.contoh.go.id/sitomat
# SITOMAT_IMAGE_TAG=1.0.0
# Deploy host then only needs: docker compose … pull && docker compose … up -d
SITOMAT_IMAGE_REGISTRY=sitomat
SITOMAT_IMAGE_TAG=latest
# ── PUBLIC ORIGIN — ONE domain for web + API (Opsi A) ────────────────────────
# Both values are the SAME single origin; the proxy splits it by path.
# • no domain yet → server IP + proxy port, e.g. http://192.168.1.10:8080
# • with TLS → https://sitomat.contoh.go.id
# Exact origin: scheme + host + port, NO trailing slash.
#
# These are RUNTIME values (BETTER_AUTH_URL / WEB_URL) — change + `up -d` only.
API_PUBLIC_URL=http://localhost:8080
WEB_PUBLIC_URL=http://localhost:8080
# ── WEB API BASE (build-time) — LEAVE EMPTY ──────────────────────────────────
# Empty (recommended) = same-origin: the browser client resolves the API against
# `window.location.origin` and download links are relative, so the web image is
# DOMAIN-AGNOSTIC — changing the public domain needs NO rebuild.
# Set an absolute origin ONLY for the two-domain (split) setup; then a domain
# change requires: docker compose … build web && docker compose … up -d web
WEB_API_BASE_URL=
# ── HOST PORTS (optional) ────────────────────────────────────────────────────
# The single public entry is the PROXY. The api/web ports are for direct
# debugging only — the proxy reaches them on the compose network.
API_PORT=13000
WEB_PORT=13001
PROXY_PORT=8080
# ── SESSION COOKIE DOMAIN — leave EMPTY in single-domain mode ────────────────
# One origin ⇒ host-only cookie, which is exactly right. Set this ONLY if you
# deliberately put the web and API on DIFFERENT subdomains (not Opsi A).
#SESSION_COOKIE_DOMAIN=
# ── KEYCLOAK SSO (optional) ──────────────────────────────────────────────────
# Leave ALL THREE empty to disable Keycloak entirely. Setting OIDC_CLIENT_ID
# without OIDC_ISSUER_URL makes the API refuse to boot (fail fast).
#OIDC_CLIENT_ID=
#OIDC_CLIENT_SECRET=
#OIDC_ISSUER_URL=
# ── WEB BUILD FLAGS ──────────────────────────────────────────────────────────
# 🚨 NEXT_PUBLIC_DEMO_LOGIN must stay `false` on any server others can reach:
# `true` lets anyone sign in as any role WITHOUT a password.
NEXT_PUBLIC_DEMO_LOGIN=false
NEXT_PUBLIC_KEYCLOAK_ENABLED=false
# ── REVERSE PROXY (profile "proxy") ──────────────────────────────────────────
# The single hostname the proxy answers for (envsubst → nginx `server_name`).
# With a real domain: PUBLIC_HOST=sitomat.contoh.go.id
PUBLIC_HOST=localhost
@@ -0,0 +1,61 @@
# ─────────────────────────────────────────────────────────────────────────────
# sitomat — SINGLE-DOMAIN reverse proxy (Opsi A).
#
# ONE public origin serves BOTH the web UI and the API; routing is by PATH. A
# small, explicit set of API prefixes goes to the API, everything else to the
# web — which is what lets the deployment expose a single port / domain.
#
# WHY ONLY THESE PREFIXES: most API routes (`/datasets`, `/submissions`,
# `/services`, `/standards`, …) COLLIDE with web pages of the same name. They do
# not need a public path because the web calls them SERVER-SIDE (Eden →
# `http://api:13000`) over the internal network. Only endpoints the BROWSER — or
# an external M2M client — hits directly are routed here:
#
# /api/auth/* better-auth (login/session) ← NEXT_PUBLIC_API_URL
# /api/v1/* documented read API (F09-A)
# /public/* open-data downloads (F07)
# /ingest/v1/* push ingest (F09-B)
# /docs swagger
# /health health check
# /datasets/<id>/versions/<v>/download browser download (F02)
#
# The last one is a REGEX on purpose: it is the ONLY browser-facing API path at
# the ROOT, and a plain prefix would swallow the web's `/datasets/[id]` page.
#
# With web + API on ONE origin there is no cross-origin request, so CORS is moot
# and the session cookie stays host-only (no `SESSION_COOKIE_DOMAIN`).
#
# `${PUBLIC_HOST}` is substituted by the nginx image's envsubst step.
# This is HTTP; add `listen 443 ssl` + certificates when TLS terminates here.
# ─────────────────────────────────────────────────────────────────────────────
# Uploads (F02) can be several MB — nginx's 1m default would answer 413.
client_max_body_size 100m;
server {
listen 80;
server_name ${PUBLIC_HOST};
# Inherited by every `location` below.
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# better-auth checks the browser's Origin against WEB_URL; forward it as-is.
proxy_set_header Origin $http_origin;
# ── API ──────────────────────────────────────────────────────────────────
location = /health { proxy_pass http://api:13000; }
location /docs { proxy_pass http://api:13000; }
location /api/ { proxy_pass http://api:13000; }
location /public/ { proxy_pass http://api:13000; }
location /ingest/ { proxy_pass http://api:13000; }
# The one root-level browser API path (see the header note). Regex locations
# are evaluated BEFORE the prefix `location /` below, so this wins.
location ~ ^/datasets/[^/]+/versions/[^/]+/download$ {
proxy_pass http://api:13000;
}
# ── Web (everything else) ────────────────────────────────────────────────
location / { proxy_pass http://web:13001; }
}