Core Principles
/projects/<slug> และ listen ใน container ที่ port 3100.
Repository Layout
templates/
README.md
add-service-checklist.md
nginx/project-location.conf
portal/card.html
nextjs/
vuejs/
nuxt/
nestjs/
fastapi/
golang/
services/
docker-compose.yml
docker-compose.local.yml
nginx/
nginx.conf
conf.d/
default.conf
local.conf
portal/
index.html
projects/
demo1/
task-assignee/
templates/
nextjs.compose.yml
fastapi.compose.yml
go.compose.yml
microservice.compose.yml
root repo ใช้เก็บเอกสารระดับทีม เช่น README.md, AGENTS.md, handbook.html, deployment.html, standard.html และ starter scaffolds ใน templates/.
Routing Contract
| Layer | Standard | Reason |
|---|---|---|
| Public path | /projects/<slug>/ |
ทำให้ portal, nginx และ app config สื่อสารด้วย key เดียว |
| Container port | 3100 |
ลด port mismatch และทำให้ nginx location เพิ่มง่าย |
| Network | <DOCKER_NETWORK> |
nginx resolve service name ผ่าน Docker DNS |
| Forwarded prefix | X-Forwarded-Prefix: /projects/<slug> |
framework ที่รองรับ prefix จะ generate URL ได้ถูก |
Next.js Standard
Config
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "/projects/my-app";
const nextConfig = {
output: "standalone",
basePath,
assetPrefix: basePath || undefined
};
Links and URLs
ใช้ next/link สำหรับ internal route ปกติ. ถ้าใช้ raw <a>, static public file, redirect, หรือ fetch ใน browser ต้องผ่าน helper กลาง เช่น withBasePath("/portal/plan").
import { withBasePath } from "@/lib/base-path";
await fetch(withBasePath("/api/db/events"), { method: "POST" });
<a href={withBasePath("/portal/plan")}>Research Plan</a>
href="/portal/plan" หรือ fetch("/api/...") ใน app ที่ mount ใต้ subpath.
Component Style
- แยก data helpers ไปไว้ใน
lib/หรือdata/. - ใช้ TypeScript type สำหรับ payload และ domain object.
- หลีกเลี่ยง generated output เช่น
.next/. - ถ้า page เป็น client component ให้ keep state เฉพาะ UI state; business transformation ควรแยก helper.
Vue.js / Vite Standard
Vue.js services use Vite and must set base from VITE_BASE_PATH. Use a helper for raw anchors or browser fetches.
const base = process.env.VITE_BASE_PATH || "/projects/vue-demo/";
export default defineConfig({
base,
plugins: [vue()]
});
environment:
PORT: 3100
VITE_BASE_PATH: /projects/vue-demo/
Nuxt Standard
Nuxt services use NUXT_APP_BASE_URL so routes, assets, and API calls stay under /projects/<slug>/.
export default defineNuxtConfig({
app: {
baseURL: process.env.NUXT_APP_BASE_URL || "/projects/nuxt-demo/"
}
});
environment:
NITRO_HOST: 0.0.0.0
NITRO_PORT: 3100
NUXT_APP_BASE_URL: /projects/nuxt-demo/
NestJS Standard
NestJS APIs must honor BASE_PATH. The template uses app.setGlobalPrefix(), so nginx can preserve the external prefix.
const prefix = (process.env.BASE_PATH || "")
.replace(/^\/+/, "")
.replace(/\/+$/, "");
if (prefix) {
app.setGlobalPrefix(prefix);
}
environment:
PORT: 3100
BASE_PATH: /projects/nestjs-demo
Python / FastAPI Standard
Runtime
services:
fastapi-demo:
expose:
- "3100"
environment:
PORT: 3100
ROOT_PATH: /projects/fastapi-demo
command: >
uvicorn main:app
--host 0.0.0.0
--port 3100
--proxy-headers
--forwarded-allow-ips="*"
--root-path /projects/fastapi-demo
Code
import os
from fastapi import FastAPI
app = FastAPI(root_path=os.getenv("ROOT_PATH", ""))
@app.get("/healthz")
def healthz() -> dict[str, str]:
return {"status": "ok"}
- ใช้ type hints ทุก public function.
- ใช้ Pydantic models สำหรับ request/response ที่ไม่ trivial.
- แยก router เป็น
routers/, business logic เป็นservices/, config เป็นsettings.py. - log เป็น structured fields เท่าที่ทำได้ เช่น request id, actor, action, status.
Golang Standard
Runtime
services:
go-demo:
expose:
- "3100"
environment:
PORT: 3100
BASE_PATH: /projects/go-demo
Code
package main
import (
"log/slog"
"net/http"
"os"
)
func main() {
port := getenv("PORT", "3100")
basePath := getenv("BASE_PATH", "")
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"ok"}`))
})
slog.Info("server starting", "port", port, "basePath", basePath)
if err := http.ListenAndServe(":"+port, mux); err != nil {
slog.Error("server stopped", "error", err)
os.Exit(1)
}
}
func getenv(key, fallback string) string {
if value := os.Getenv(key); value != "" {
return value
}
return fallback
}
- ใช้
gofmtและgo test ./...ก่อนส่งงาน. - ใช้
context.Contextกับงาน IO หรือ request scoped work. - ใช้
log/slogสำหรับ structured logging. - อย่า hardcode public URL; อ่านจาก
BASE_PATHหรือX-Forwarded-Prefix.
Backend + PostgreSQL Standard
เมื่อ backend ต้องต่อ PostgreSQL ใน Docker Compose ให้ต่อผ่าน Docker service name ไม่ใช่ localhost.
Compose DB Service
<DB_SERVICE>:
image: postgres:16-alpine
container_name: <DB_SERVICE>
restart: unless-stopped
environment:
POSTGRES_DB: <DB_NAME>
POSTGRES_USER: <DB_USER>
POSTGRES_PASSWORD: <DB_PASSWORD_ENV>
volumes:
- <DB_SERVICE>-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U <DB_USER> -d <DB_NAME>"]
interval: 10s
timeout: 5s
retries: 5
expose:
- "5432"
networks:
- <DOCKER_NETWORK>
Backend Connection
<APP_SERVICE>:
environment:
DATABASE_URL: postgresql://<DB_USER>:<DB_PASSWORD_ENV>@<DB_SERVICE>:5432/<DB_NAME>
depends_on:
<DB_SERVICE>:
condition: service_healthy
<DB_SERVICE> หรือชื่อ service ของ Postgres. ห้ามใช้ localhost จาก backend container.
Framework Usage
# FastAPI / SQLAlchemy / asyncpg DATABASE_URL=postgresql+asyncpg://<DB_USER>:<DB_PASSWORD_ENV>@<DB_SERVICE>:5432/<DB_NAME> # NestJS / TypeORM / Prisma / node-postgres DATABASE_URL=postgresql://<DB_USER>:<DB_PASSWORD_ENV>@<DB_SERVICE>:5432/<DB_NAME> # Go / pgx DATABASE_URL=postgres://<DB_USER>:<DB_PASSWORD_ENV>@<DB_SERVICE>:5432/<DB_NAME>?sslmode=disableData Safety
- ใช้ named volume สำหรับข้อมูล Postgres เสมอ.
- อย่า commit password จริงลง repo.
- อย่า publish
5432:5432ใน production ยกเว้นมีเหตุผล admin ชัดเจน. - migration ควรรันเป็น command/job แยก หรือ entrypoint ที่ idempotent.
nginx Standard
File Boundaries
nginx.confเท่านั้นที่มีeventsและhttp.conf.d/*.confต้องเป็นserverblock เท่านั้น.- proxy headers, timeouts และ Docker resolver อยู่ใน
nginx.confเป็นค่ากลาง.
Project Location Pattern
location /projects/demo1/ {
set $demo1_upstream <APP_SERVICE>:3100;
set $project_prefix /projects/demo1;
proxy_pass http://$demo1_upstream;
}
proxy_pass http://service:3100/; เพราะจะ strip prefix.
Docker Compose Standard
services:
my-demo:
build:
context: ./projects/my-demo
restart: unless-stopped
expose:
- "3100"
environment:
PORT: 3100
PUBLIC_BASE_PATH: /projects/my-demo
networks:
- <DOCKER_NETWORK>
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
- ใช้
exposeสำหรับ service ภายใน proxy network. - ใช้
portsเฉพาะ reverse proxy หรือกรณี debug เฉพาะกิจ. - container name ควรอ่านออกและไม่ชนกันระหว่าง local/production.
- local compose ต้องไม่ต้องใช้ production certificate.
Docker Compose + Portal Setup Step By Step
ใช้ขั้นตอนนี้ทุกครั้งที่เอา service ใหม่เข้า stack. ตัวอย่างใช้ slug reports-demo.
Step 1: เลือก slug
reports-demo
/projects/reports-demo/
slug ต้องเป็น lowercase kebab-case และควรตรงกับ Docker service name.
Step 2: copy template
cp -R templates/nextjs services/projects/reports-demo
# หรือเลือก templates/vuejs, templates/nuxt, templates/nestjs, templates/fastapi, templates/golang
Step 3: เพิ่ม service ใน docker-compose.yml
reports-demo:
build:
context: <DEPLOY_PATH>/projects/reports-demo
container_name: reports-demo
restart: unless-stopped
expose:
- "3100"
environment:
PORT: 3100
PUBLIC_BASE_PATH: /projects/reports-demo
networks:
- <DOCKER_NETWORK>
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
ใช้ expose ไม่ใช้ host ports สำหรับ app service ที่อยู่หลัง nginx.
Step 4: ตั้ง framework base path
# Next.js
NEXT_PUBLIC_BASE_PATH: /projects/reports-demo
# Vue.js / Vite
VITE_BASE_PATH: /projects/reports-demo/
# Nuxt
NUXT_APP_BASE_URL: /projects/reports-demo/
# NestJS / Go
BASE_PATH: /projects/reports-demo
# FastAPI
ROOT_PATH: /projects/reports-demo
Step 5: เพิ่ม depends_on ให้ reverse-proxy service
<NGINX_SERVICE>:
depends_on:
<APP_SERVICE>:
condition: service_started
ถ้า service นั้น run จาก compose แยก ให้ไม่ต้องเพิ่ม depends_on แต่ต้องอยู่ network เดียวกัน.
Step 6: เพิ่ม PostgreSQL ถ้า backend ต้องใช้ DB
<DB_SERVICE>:
image: postgres:16-alpine
environment:
POSTGRES_DB: <DB_NAME>
POSTGRES_USER: <DB_USER>
POSTGRES_PASSWORD: <DB_PASSWORD_ENV>
volumes:
- <DB_SERVICE>-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U <DB_USER> -d <DB_NAME>"]
expose:
- "5432"
networks:
- <DOCKER_NETWORK>
reports-demo:
environment:
DATABASE_URL: postgresql://<DB_USER>:<DB_PASSWORD_ENV>@<DB_SERVICE>:5432/<DB_NAME>
depends_on:
<DB_SERVICE>:
condition: service_healthy
backend ต้องใช้ host <DB_SERVICE> ไม่ใช่ localhost.
Step 7: เพิ่ม nginx route
location = /projects/reports-demo {
set $reports_demo_upstream <APP_SERVICE>:3100;
set $project_prefix /projects/reports-demo;
proxy_pass http://$reports_demo_upstream;
}
location /projects/reports-demo/ {
set $reports_demo_upstream <APP_SERVICE>:3100;
set $project_prefix /projects/reports-demo;
proxy_pass http://$reports_demo_upstream;
}
proxy_pass http://<APP_SERVICE>:3100/; กับ base-path-aware app เพราะ slash ท้าย upstream จะ strip prefix.
Step 8: เพิ่ม portal card
<a class="card" href="/projects/reports-demo/">
<div class="card-header">
<span class="card-badge">Demo</span>
<span class="card-status">Ready</span>
</div>
<h3>Reports Demo</h3>
<p>Short description of what this service demonstrates.</p>
<code>/projects/reports-demo/</code>
</a>
Step 9: validate
docker compose -f services/docker-compose.yml config
docker exec <NGINX_CONTAINER> nginx -t
docker exec <NGINX_CONTAINER> nginx -s reload
curl -sL -o /dev/null -w '%{http_code} %{url_effective}\n' https://<PUBLIC_APP_HOST>/projects/reports-demo/
curl -sL -o /dev/null -w '%{http_code} %{url_effective}\n' https://<PUBLIC_APP_HOST>/projects/reports-demo/healthz
สำหรับรายละเอียดเต็ม ใช้ templates/docker-compose-portal-steps.md และ templates/postgres/README.md.
Templates Standard
templates/ คือ source of truth สำหรับเริ่ม service ใหม่. อย่า copy จาก app เก่าโดยตรงถ้า app นั้นมี history หรือ legacy config.
| Template | Use For | Includes |
|---|---|---|
templates/nextjs |
Next.js App Router service | standalone Dockerfile, basePath config, withBasePath, health route |
templates/vuejs |
Vue.js Vite service | Vite base config, Dockerfile, base path helper example |
templates/nuxt |
Nuxt SSR service | Nuxt baseURL config, Nitro port env, health API |
templates/nestjs |
NestJS API service | global prefix from BASE_PATH, Dockerfile, health route |
templates/postgres |
Backend database service | Postgres compose pattern, env example, init schema, healthcheck |
templates/fastapi |
Python API service | FastAPI root_path, Uvicorn command, health route |
templates/golang |
Go HTTP service | Dockerfile, env-driven port/base path, health route |
templates/nginx/project-location.conf |
New reverse proxy route | prefix-preserving locations with $project_prefix |
templates/portal/card.html |
Portal app card | standard link to /projects/<slug>/ |
3100, base path /projects/<slug>, และ Docker network <DOCKER_NETWORK>.
Quality Gates
Quality gate คือด่านผ่าน/ไม่ผ่านก่อน merge หรือ deploy. งาน production ต้องไม่จบแค่ container start ได้ แต่ต้องผ่าน dependency CVE, static analysis, image scan, build, nginx และ smoke route.
| Area | Command | Expected |
|---|---|---|
| npm CVE | npm audit --audit-level=moderate |
0 vulnerability for runtime Next.js apps |
| SonarQube | docker compose --profile quality up -d sonarqube sonar-postgres then run scanner |
Quality Gate passed; no new blocker/critical issue |
| Container CVE | trivy image <image> or docker scout cves <image> |
no fixable high/critical CVE before deploy |
| Local gate script | ./scripts/quality-gate.sh |
npm audit, Next build, compose config and Trivy filesystem scan pass |
| Service smoke script | ./scripts/check-service.sh <slug> |
route, healthz และ base path asset check ผ่าน |
| Compose | docker compose -f services/docker-compose.local.yml config |
config renders without error |
| nginx | nginx -t inside nginx container |
syntax is ok and test is successful |
| Next.js | npm run build |
compile and TypeScript pass |
| Route | curl -sL -o /dev/null -w '%{http_code}' http://localhost/projects/demo1/ |
200 |
| Assets | grep /projects/demo1/_next |
assets are base-path prefixed |
| Pipeline monitor | curl -I http://localhost/pipeline/ |
portal can show deployment, gate and rollback checklist |
Standard Severity: MUST / SHOULD / MAY
- ทุก service ต้องอยู่ใต้
/projects/<slug>/. - Container ต้องฟังบน port
3100. - Browser URL และ asset ต้องใช้ base path helper เสมอ.
- Backend ระหว่าง container ติดต่อด้วย Docker service name ไม่ใช้
localhost. - ทุก service ต้องมี
/healthzและ log ที่ติด requestId. - ห้าม commit secret, token, password หรือ private key.
- ใช้ template ล่าสุดก่อนสร้าง service ใหม่.
- มี structured logging, correlation id, และ error schema เดียวกัน.
- มี compose healthcheck + rollback note.
- มี docs/flow, lab, และ checklist ที่ team ใช้ซ้ำได้.
- เพิ่ม framework helper (Auth helper, API client helper) ตามทีม.
- เพิ่ม local script ช่วยตรวจ route/asset.
- เพิ่ม fixture สำหรับ training และ demo.
Role-based Standards
| Role | ต้องอ่าน | ต้องทำได้ |
|---|---|---|
| Frontend Dev | Routing contract, FE architecture, Next.js/Nuxt/Vue standard | ทำ base path helper ได้, แก้ link/fetch หลุด, เปิด dev server ผ่าน subpath |
| Backend Dev | API standard, Migration, PostgreSQL pattern, logging | ทำ /healthz, validation schema, request tracing, migrate safely |
| DevOps | Nginx, compose, deploy flow, rollback, maintenance | เพิ่ม route, debug 502/504, validate healthz, สรุป rollback note |
| Tech Lead | Arch, security, quality gate, PR checklist | กำหนด severity, approve exception, enforce DoD ก่อนปล่อยงาน |
Bad vs Good Examples (สำคัญมาก)
// Next.js fetch ที่หลุด base path
fetch('/api/healthz')
fetch(withBasePath('/api/healthz'))
proxy_pass http://app-service:3100/;
proxy_pass http://app-service:3100;
Definition of Done: New Service
- [ ] มี slug แบบ lowercase, path
/projects/<slug>/. - [ ] ใส่ base path และตรวจ route ด้วย curl.
- [ ] ควบคุม env ผ่าน .env/.compose ไม่ hardcode.
- [ ] มี
/healthz, structured log, requestId. - [ ] ไม่ commit secret, password, token, key.
- [ ] มี portal card และเชื่อมผ่าน nginx route ได้.
- [ ] ผ่าน quality gate และมี rollback note.
PR Review Checklist
- [ ] route และ base path ผ่าน checklist.
- [ ] architecture ไม่กระทบ service เดิม.
- [ ] API contract/response format ตรง standard.
- [ ] logging + audit ครอบ event สำคัญ.
- [ ] migration ใช้ up/down และมี rollback recovery.
- [ ] security ไม่มี secret จริงใน diff.
- [ ] run quality gate และผูก evidence ใน PR description.
Add New Service Checklist
- เลือก slug เช่น
reports-demo. - สร้าง app ใต้
services/projects/reports-demoหรือใช้ image ภายนอก. - ตั้ง container port เป็น
3100. - ตั้ง env base path เป็น
/projects/reports-demo. - เพิ่ม service ใน compose หรือใช้ template ที่ตรง framework.
- เพิ่ม nginx location ที่กำหนด
$project_prefixและ upstream. - เพิ่ม portal card ที่ชี้
/projects/reports-demo/. - รัน compose config, nginx test, build, curl route และ asset check.