From fd0c30f579f7834c1d41454b5ac20d36aafaf8c7 Mon Sep 17 00:00:00 2001 From: Liju Kuriakose Date: Tue, 1 Sep 2026 20:04:30 +0530 Subject: [PATCH 01/12] Add k3s deploy layout, compose consolidation, and SEO noindex for non-production. Consolidate Docker Compose under deployment/compose and add Kustomize manifests under deployment/k8s for the Flycatch cluster, with environment-wide noindex controls so the new development environment is not SEO-indexed. Co-authored-by: Cursor --- .dockerignore | 3 +- .gitignore | 3 +- README.md | 31 +-- apps/Administration-FE/Dockerfile | 5 +- apps/Backend/src/flycatch_api/main.py | 5 +- apps/Frontend/Dockerfile | 4 + apps/Frontend/public/robots.txt | 7 - apps/Frontend/src/layouts/BaseLayout.astro | 4 +- apps/Frontend/src/pages/robots.txt.ts | 24 ++ deployment/README.md | 42 +--- deployment/{ => compose}/.env.example | 5 + deployment/compose/README.md | 49 ++++ deployment/{ => compose}/docker-compose.yml | 17 +- deployment/k8s/README.md | 225 ++++++++++++++++++ deployment/{ => k8s/base}/Caddyfile | 2 + .../base/administration-fe-deployment.yaml | 52 ++++ .../k8s/base/administration-fe-service.yaml | 17 ++ deployment/k8s/base/backend-deployment.yaml | 57 +++++ deployment/k8s/base/backend-service.yaml | 17 ++ deployment/k8s/base/configmap.yaml | 16 ++ deployment/k8s/base/frontend-deployment.yaml | 52 ++++ deployment/k8s/base/frontend-service.yaml | 17 ++ deployment/k8s/base/gateway-deployment.yaml | 60 +++++ deployment/k8s/base/gateway-service.yaml | 17 ++ deployment/k8s/base/kustomization.yaml | 24 ++ deployment/k8s/base/namespace.yaml | 6 + deployment/k8s/overlays/dev/ingress.yaml | 26 ++ .../k8s/overlays/dev/kustomization.yaml | 30 +++ .../k8s/overlays/dev/noindex-middleware.yaml | 10 + .../k8s/overlays/dev/secret.example.yaml | 25 ++ deployment/k8s/scripts/deploy-dev.sh | 93 ++++++++ docs/onboarding.md | 12 +- specs/001-website-foundation/quickstart.md | 8 +- specs/001-website-foundation/research.md | 2 +- specs/001-website-foundation/tasks.md | 4 +- specs/002-auth-rbac/quickstart.md | 6 +- specs/002-auth-rbac/tasks.md | 2 +- 37 files changed, 896 insertions(+), 83 deletions(-) delete mode 100644 apps/Frontend/public/robots.txt create mode 100644 apps/Frontend/src/pages/robots.txt.ts rename deployment/{ => compose}/.env.example (81%) create mode 100644 deployment/compose/README.md rename deployment/{ => compose}/docker-compose.yml (79%) create mode 100644 deployment/k8s/README.md rename deployment/{ => k8s/base}/Caddyfile (83%) create mode 100644 deployment/k8s/base/administration-fe-deployment.yaml create mode 100644 deployment/k8s/base/administration-fe-service.yaml create mode 100644 deployment/k8s/base/backend-deployment.yaml create mode 100644 deployment/k8s/base/backend-service.yaml create mode 100644 deployment/k8s/base/configmap.yaml create mode 100644 deployment/k8s/base/frontend-deployment.yaml create mode 100644 deployment/k8s/base/frontend-service.yaml create mode 100644 deployment/k8s/base/gateway-deployment.yaml create mode 100644 deployment/k8s/base/gateway-service.yaml create mode 100644 deployment/k8s/base/kustomization.yaml create mode 100644 deployment/k8s/base/namespace.yaml create mode 100644 deployment/k8s/overlays/dev/ingress.yaml create mode 100644 deployment/k8s/overlays/dev/kustomization.yaml create mode 100644 deployment/k8s/overlays/dev/noindex-middleware.yaml create mode 100644 deployment/k8s/overlays/dev/secret.example.yaml create mode 100755 deployment/k8s/scripts/deploy-dev.sh diff --git a/.dockerignore b/.dockerignore index 60774fc..52182e9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,7 +4,8 @@ node_modules *.md .env .env.* -deployment/.env +deployment/compose/.env +deployment/**/.env dist .astro __pycache__ diff --git a/.gitignore b/.gitignore index 47bf39e..9f5fdc4 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,8 @@ htmlcov/ .env .env.* !.env.example -deployment/.env +deployment/compose/.env +deployment/**/.env # Logs *.log diff --git a/README.md b/README.md index b88e8ba..f628d51 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Ordinary public browsing does **not** require the Backend at runtime. Administra - **Frontend / Administration FE**: Astro 5, React 19 (Administration FE only), TypeScript - **Backend**: FastAPI, Python 3.12, PostgreSQL 16 - **Object storage**: S3-compatible (MinIO locally) -- **Deployment**: Docker Compose in `deployment/` +- **Deployment**: Docker Compose + k3s under `deployment/` ## Repository layout @@ -25,7 +25,9 @@ apps/ ├── Frontend/ ├── Administration-FE/ └── Backend/ -deployment/ # Docker Compose, environment config, gateway +deployment/ +├── compose/ # Docker Compose, .env, Caddy gateway +└── k8s/ # Kustomize manifests for Flycatch k3s specs/001-website-foundation/ # Feature spec, plan, contracts, quickstart specs/002-auth-rbac/ # JWT auth + RBAC spec, plan, contracts, quickstart docs/ # Conventions and onboarding (implementation phase) @@ -57,25 +59,25 @@ Compose does **not** create staff accounts or apply migrations. There is no defa 1. Copy environment config and set secrets (`JWT_SECRET` and the other `change-me` values): ```bash - cp deployment/.env.example deployment/.env + cp deployment/compose/.env.example deployment/compose/.env ``` - Do not commit `deployment/.env`. Variable names are documented in `deployment/.env.example`. + Do not commit `deployment/compose/.env`. Variable names are documented in `deployment/compose/.env.example`. 2. Build and start all services from the repository root: ```bash - docker compose -f deployment/docker-compose.yml up -d --build + docker compose -f deployment/compose/docker-compose.yml --env-file deployment/compose/.env up -d --build ``` - From `deployment/` you can use `docker compose up -d --build` instead. + From `deployment/compose/` you can use `docker compose up -d --build` instead. 3. After Postgres and MinIO are healthy, migrate, seed, and bootstrap two staff users: ```bash - docker compose -f deployment/docker-compose.yml exec backend alembic upgrade head - docker compose -f deployment/docker-compose.yml exec backend flycatch-seed-records - docker compose -f deployment/docker-compose.yml exec backend flycatch-bootstrap \ + docker compose -f deployment/compose/docker-compose.yml --env-file deployment/compose/.env exec backend alembic upgrade head + docker compose -f deployment/compose/docker-compose.yml --env-file deployment/compose/.env exec backend flycatch-seed-records + docker compose -f deployment/compose/docker-compose.yml --env-file deployment/compose/.env exec backend flycatch-bootstrap \ --user-1-email admin1@example.com \ --user-2-email admin2@example.com \ --user-2-role editor @@ -90,10 +92,10 @@ Compose does **not** create staff accounts or apply migrations. There is no defa 5. Rebuild app images after Frontend or Administration FE changes: ```bash - docker compose -f deployment/docker-compose.yml up -d --build + docker compose -f deployment/compose/docker-compose.yml --env-file deployment/compose/.env up -d --build ``` - Stop the stack with `docker compose -f deployment/docker-compose.yml down`. Add `-v` only if you intend to wipe Postgres and MinIO volumes. + Stop the stack with `docker compose -f deployment/compose/docker-compose.yml --env-file deployment/compose/.env down`. Add `-v` only if you intend to wipe Postgres and MinIO volumes. Gateway (default `http://localhost:8080`, `GATEWAY_PORT` in `.env`): @@ -107,9 +109,10 @@ Gateway (default `http://localhost:8080`, `GATEWAY_PORT` in `.env`): | File | Purpose | | --- | --- | -| `deployment/docker-compose.yml` | All foundation services | -| `deployment/.env.example` | Shared environment configuration | -| `deployment/Caddyfile` | Path-based gateway routing | +| `deployment/compose/docker-compose.yml` | All foundation services | +| `deployment/compose/.env.example` | Shared environment configuration | +| `deployment/k8s/base/Caddyfile` | Path-based gateway routing (compose + k8s) | +| `deployment/k8s/` | Kubernetes manifests for the Flycatch k3s cluster | ## Local Development (Running Individually) diff --git a/apps/Administration-FE/Dockerfile b/apps/Administration-FE/Dockerfile index 2c20111..32b3d40 100644 --- a/apps/Administration-FE/Dockerfile +++ b/apps/Administration-FE/Dockerfile @@ -5,7 +5,10 @@ RUN npm ci COPY . . COPY --from=specs . /specs ENV CONTRACTS_DIR=/specs/002-auth-rbac/contracts -ENV PUBLIC_ORIGIN=http://localhost:8080 +ARG PUBLIC_ORIGIN=http://localhost:8080 +ARG PUBLIC_ENVIRONMENT=development +ENV PUBLIC_ORIGIN=$PUBLIC_ORIGIN +ENV PUBLIC_ENVIRONMENT=$PUBLIC_ENVIRONMENT RUN npm run generate:client && npm run build FROM node:22-alpine diff --git a/apps/Backend/src/flycatch_api/main.py b/apps/Backend/src/flycatch_api/main.py index 1606e02..88a7548 100644 --- a/apps/Backend/src/flycatch_api/main.py +++ b/apps/Backend/src/flycatch_api/main.py @@ -74,7 +74,10 @@ async def security_headers(request: Request, call_next): response.headers["X-Content-Type-Options"] = "nosniff" response.headers["X-Frame-Options"] = "DENY" response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" - if request.url.path.startswith("/admin") or request.url.path.startswith("/api/v1/admin"): + is_admin_path = request.url.path.startswith("/admin") or request.url.path.startswith( + "/api/v1/admin" + ) + if settings.environment != "production" or is_admin_path: response.headers["X-Robots-Tag"] = "noindex, nofollow" return response diff --git a/apps/Frontend/Dockerfile b/apps/Frontend/Dockerfile index 3b1f83c..c96cd32 100644 --- a/apps/Frontend/Dockerfile +++ b/apps/Frontend/Dockerfile @@ -3,6 +3,10 @@ WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY . . +ARG PUBLIC_ORIGIN=http://localhost:8080 +ARG PUBLIC_ENVIRONMENT=development +ENV PUBLIC_ORIGIN=$PUBLIC_ORIGIN +ENV PUBLIC_ENVIRONMENT=$PUBLIC_ENVIRONMENT RUN npm run build FROM node:22-alpine diff --git a/apps/Frontend/public/robots.txt b/apps/Frontend/public/robots.txt deleted file mode 100644 index 9937433..0000000 --- a/apps/Frontend/public/robots.txt +++ /dev/null @@ -1,7 +0,0 @@ -User-agent: * -Allow: / - -Disallow: /admin -Disallow: /api - -Sitemap: /sitemap-index.xml diff --git a/apps/Frontend/src/layouts/BaseLayout.astro b/apps/Frontend/src/layouts/BaseLayout.astro index 6415cfd..80345be 100644 --- a/apps/Frontend/src/layouts/BaseLayout.astro +++ b/apps/Frontend/src/layouts/BaseLayout.astro @@ -13,6 +13,8 @@ interface Props { } const { title, metadata, structuredData = [], lang = 'en', dir = 'ltr' } = Astro.props; +const isProduction = (import.meta.env.PUBLIC_ENVIRONMENT || 'development') === 'production'; +const shouldNoindex = !metadata.indexable || !isProduction; --- @@ -22,7 +24,7 @@ const { title, metadata, structuredData = [], lang = 'en', dir = 'ltr' } = Astro {title} - {!metadata.indexable && } + {shouldNoindex && } diff --git a/apps/Frontend/src/pages/robots.txt.ts b/apps/Frontend/src/pages/robots.txt.ts new file mode 100644 index 0000000..b69a22e --- /dev/null +++ b/apps/Frontend/src/pages/robots.txt.ts @@ -0,0 +1,24 @@ +import type { APIRoute } from 'astro'; + +const isProduction = (import.meta.env.PUBLIC_ENVIRONMENT || 'development') === 'production'; + +export const GET: APIRoute = () => { + const body = isProduction + ? [ + 'User-agent: *', + 'Allow: /', + '', + 'Disallow: /admin', + 'Disallow: /api', + '', + 'Sitemap: /sitemap-index.xml', + '', + ].join('\n') + : ['User-agent: *', 'Disallow: /', ''].join('\n'); + + return new Response(body, { + headers: { + 'Content-Type': 'text/plain; charset=utf-8', + }, + }); +}; diff --git a/deployment/README.md b/deployment/README.md index a8dbc8d..1b81a9d 100644 --- a/deployment/README.md +++ b/deployment/README.md @@ -1,41 +1,13 @@ # Deployment -Deployment-specific files for Docker Compose and environment configuration. +How this app runs locally and on the cluster. Everything lives under this folder. -**Project overview, stack, and full setup instructions:** see [README.md](../README.md) at the repository root. - -## Files in this directory - -| File | Purpose | +| Path | Purpose | | --- | --- | -| `docker-compose.yml` | Frontend, Administration FE, Backend, PostgreSQL, MinIO, gateway | -| `.env.example` | Shared environment variables — copy to `.env` | -| `Caddyfile` | Gateway routing: `/`, `/admin`, `/api` | - -## Quick start - -From this directory: - -```bash -cp .env.example .env -# Set JWT_SECRET and other change-me values - -docker compose up -d --build -``` - -From the repository root: `docker compose -f deployment/docker-compose.yml up -d --build`. - -Compose does not provision staff. After services are healthy: - -```bash -docker compose exec backend alembic upgrade head -docker compose exec backend flycatch-seed-records -docker compose exec backend flycatch-bootstrap \ - --user-1-email admin1@example.com \ - --user-2-email admin2@example.com \ - --user-2-role editor -``` +| [compose/](compose/) | Docker Compose stack (local / preview): Postgres, MinIO, apps, Caddy gateway | +| [k8s/](k8s/) | Kubernetes manifests (Kustomize) for the Flycatch k3s cluster | -There is no default password. Bootstrap prompts for two passwords (min 12 characters). Sign in at `http://localhost:8080/admin`. Full startup notes: [README.md](../README.md#quick-start-docker-compose) and [docs/onboarding.md](../docs/onboarding.md). +Shared gateway routing for both compose and k8s: [k8s/base/Caddyfile](k8s/base/Caddyfile) +(Compose mounts this file; k8s loads it via ConfigMap). -Validation scenarios: [quickstart.md](../specs/001-website-foundation/quickstart.md). +**Project overview and day-to-day setup:** [README.md](../README.md). diff --git a/deployment/.env.example b/deployment/compose/.env.example similarity index 81% rename from deployment/.env.example rename to deployment/compose/.env.example index 90fbad6..6b83e53 100644 --- a/deployment/.env.example +++ b/deployment/compose/.env.example @@ -4,6 +4,11 @@ PUBLIC_ORIGIN=http://localhost:8080 GATEWAY_PORT=8080 +# Environment: development | production +# Non-production disables SEO indexing (robots.txt Disallow, noindex meta, X-Robots-Tag). +PUBLIC_ENVIRONMENT=development +ENVIRONMENT=development + # PostgreSQL POSTGRES_DB=flycatch POSTGRES_USER=flycatch diff --git a/deployment/compose/README.md b/deployment/compose/README.md new file mode 100644 index 0000000..9e19db4 --- /dev/null +++ b/deployment/compose/README.md @@ -0,0 +1,49 @@ +# Docker Compose + +Local and preview stack: Frontend, Administration FE, Backend, PostgreSQL, MinIO, and the Caddy gateway. + +**Project overview:** see [README.md](../../README.md) at the repository root. +**k3s / cluster deploy:** see [../k8s/README.md](../k8s/README.md). + +## Files in this directory + +| File | Purpose | +| --- | --- | +| `docker-compose.yml` | Frontend, Administration FE, Backend, PostgreSQL, MinIO, gateway | +| `.env.example` | Shared environment variables — copy to `.env` | + +Gateway routing is shared with k8s: [../k8s/base/Caddyfile](../k8s/base/Caddyfile). + +## Quick start + +From this directory: + +```bash +cp .env.example .env +# Set JWT_SECRET and other change-me values + +docker compose up -d --build +``` + +From the repository root: + +```bash +docker compose -f deployment/compose/docker-compose.yml --env-file deployment/compose/.env up -d --build +``` + +Compose does not provision staff. After services are healthy: + +```bash +docker compose -f deployment/compose/docker-compose.yml --env-file deployment/compose/.env exec backend alembic upgrade head +docker compose -f deployment/compose/docker-compose.yml --env-file deployment/compose/.env exec backend flycatch-seed-records +docker compose -f deployment/compose/docker-compose.yml --env-file deployment/compose/.env exec backend flycatch-bootstrap \ + --user-1-email admin1@example.com \ + --user-2-email admin2@example.com \ + --user-2-role editor +``` + +There is no default password. Bootstrap prompts for two passwords (min 12 characters). Sign in at `http://localhost:8080/admin`. Full startup notes: [README.md](../../README.md#quick-start-docker-compose) and [docs/onboarding.md](../../docs/onboarding.md). + +`PUBLIC_ENVIRONMENT` / `ENVIRONMENT` default to `development` so pages are not SEO-indexed. + +Validation scenarios: [quickstart.md](../../specs/001-website-foundation/quickstart.md). diff --git a/deployment/docker-compose.yml b/deployment/compose/docker-compose.yml similarity index 79% rename from deployment/docker-compose.yml rename to deployment/compose/docker-compose.yml index fc53110..246fb85 100644 --- a/deployment/docker-compose.yml +++ b/deployment/compose/docker-compose.yml @@ -32,7 +32,7 @@ services: backend: build: - context: ../apps/Backend + context: ../../apps/Backend env_file: .env environment: DATABASE_URL: ${DATABASE_URL} @@ -46,6 +46,7 @@ services: JWT_ACCESS_MINUTES: ${JWT_ACCESS_MINUTES:-15} BUILD_EXPORT_TOKEN: ${BUILD_EXPORT_TOKEN} PUBLIC_ORIGIN: ${PUBLIC_ORIGIN} + ENVIRONMENT: ${ENVIRONMENT:-development} depends_on: postgres: condition: service_healthy @@ -54,15 +55,21 @@ services: frontend: build: - context: ../apps/Frontend + context: ../../apps/Frontend + args: + PUBLIC_ORIGIN: ${PUBLIC_ORIGIN:-http://localhost:8080} + PUBLIC_ENVIRONMENT: ${PUBLIC_ENVIRONMENT:-development} depends_on: - backend administration-fe: build: - context: ../apps/Administration-FE + context: ../../apps/Administration-FE additional_contexts: - specs: ../specs + specs: ../../specs + args: + PUBLIC_ORIGIN: ${PUBLIC_ORIGIN:-http://localhost:8080} + PUBLIC_ENVIRONMENT: ${PUBLIC_ENVIRONMENT:-development} environment: PUBLIC_ORIGIN: ${PUBLIC_ORIGIN} depends_on: @@ -75,7 +82,7 @@ services: environment: GATEWAY_PORT: "8080" volumes: - - ./Caddyfile:/etc/caddy/Caddyfile:ro + - ../k8s/base/Caddyfile:/etc/caddy/Caddyfile:ro depends_on: - frontend - administration-fe diff --git a/deployment/k8s/README.md b/deployment/k8s/README.md new file mode 100644 index 0000000..bb3bb23 --- /dev/null +++ b/deployment/k8s/README.md @@ -0,0 +1,225 @@ +# Kubernetes (dev) — Harbor + Argo CD + +Secrets and credentials must never be committed. Bootstrap against the Flycatch k3s +cluster using this file as the single source of truth. + +Compose / local setup lives next door: [../compose/README.md](../compose/README.md). +The Caddy gateway config is shared at [base/Caddyfile](base/Caddyfile) (Compose mounts the same file). + +## Layout + +``` +deployment/k8s/ + base/ # Namespace, Deployments, Services, ConfigMap, Caddyfile ConfigMap + overlays/dev/ # Ingress (TLS), noindex Middleware, image tags, replica counts + scripts/deploy-dev.sh # Build/push Harbor images + bump overlay tags +``` + +The Argo CD Application is owned by the platform app-of-apps in +[flycatch/k3s-platform](https://github.com/flycatch/k3s-platform): + +`infrastructure/flycatch-website/application.yaml` + +Do **not** `kubectl apply` an Application from this repo — that would duplicate the +app and use the wrong Argo project (`default` instead of `platform`). + +Ingress routes only to `gateway:8080`. Caddy path-splits `/`, `/admin`, and `/api` +to the Frontend, Administration FE, and Backend Services (same names as Compose). + +**SEO:** this overlay is a non-production environment. App builds use +`PUBLIC_ENVIRONMENT=development` / `ENVIRONMENT=development`, and Traefik Middleware +`noindex` adds `X-Robots-Tag: noindex, nofollow` on every response. + +Hostname: `https://flycatch-website-dev.k3s.flycatchtech.in` + +## Prerequisites + +- kubectl context pointing at the Flycatch k3s cluster +- Harbor project `flycatch-website` + robot with push (local script) and pull (cluster) +- Shared Postgres in namespace `database` healthy (Bitnami; container name `postgresql`) +- Shared MinIO in namespace `database` healthy (Service `minio.database.svc.cluster.local:9000`) +- Traefik IngressClass and cert-manager ClusterIssuer `letsencrypt-production` +- Cloudflare DNS access for `*.k3s.flycatchtech.in` +- Local tools: `docker`, `kustomize`, `git` + +Preview manifests without applying: + +```bash +kubectl kustomize deployment/k8s/overlays/dev +``` + +## 0. Argo CD access to the app repo + +If the app repo is private, Argo CD must be able to clone it, or the Application +`flycatch-website-dev` stays `Unknown` with authentication errors. + +```bash +kubectl -n argocd create secret generic repo-flycatch-website \ + --from-literal=type=git \ + --from-literal=url=https://github.com/flycatch/flycatch-website.git \ + --from-literal=username=git \ + --from-literal=password='' \ + --dry-run=client -o yaml | kubectl label --local -f - \ + argocd.argoproj.io/secret-type=repository -o yaml | kubectl apply -f - +``` + +The `url` must match the Application source exactly. Then hard-refresh: + +```bash +kubectl -n argocd annotate application flycatch-website-dev \ + argocd.argoproj.io/refresh=hard --overwrite +``` + +## 1. Namespace + Harbor pull secret + +```bash +kubectl create namespace flycatch-website-dev --dry-run=client -o yaml | kubectl apply -f - + +kubectl -n flycatch-website-dev create secret docker-registry harbor-pull \ + --docker-server=registry.k3s.flycatchtech.in \ + --docker-username='robot$flycatch-website+githubbot' \ + --docker-password='' \ + --dry-run=client -o yaml | kubectl apply -f - +``` + +Create `harbor-pull` **before** workloads start, or pods stay in `ImagePullBackOff`. + +## 2. Postgres role and database (reuse shared cluster Postgres) + +Service is `postgres.database.svc.cluster.local`. Use container `postgresql`. +Do **not** deploy a new Postgres pod for this app. + +First install: + +```bash +kubectl -n database exec -it sts/postgres -c postgresql -- \ + env PGPASSWORD="" \ + psql -U postgres \ + -c "CREATE ROLE flycatch_website LOGIN PASSWORD '';" \ + -c "CREATE DATABASE flycatch_website OWNER flycatch_website;" +``` + +Reinstall (role/database already exist — `CREATE` will fail): + +```bash +kubectl -n database exec -it sts/postgres -c postgresql -- \ + env PGPASSWORD="" \ + psql -U postgres \ + -c "ALTER ROLE flycatch_website LOGIN PASSWORD '';" \ + -c "ALTER DATABASE flycatch_website OWNER TO flycatch_website;" +``` + +Connection string used by the Backend: + +```text +postgresql+psycopg://flycatch_website:@postgres.database.svc.cluster.local:5432/flycatch_website +``` + +Migrations run automatically on Backend container start (`alembic upgrade head`). + +## 3. MinIO bucket and credentials (reuse shared cluster MinIO) + +Service is `minio.database.svc.cluster.local:9000`. Create a dedicated bucket and +least-privilege access key for this app (do not reuse MinIO root credentials in the +app Secret if you can avoid it). + +Example with the MinIO client against a port-forward: + +```bash +kubectl -n database port-forward svc/minio 9000:9000 + +# In another shell, after mc alias set ... +mc mb myminio/flycatch-website +mc admin user add myminio flycatch-website '' '' +# Attach a policy that allows read/write only on bucket flycatch-website +``` + +ConfigMap already points `S3_ENDPOINT` / `S3_BUCKET` at the shared service and +`flycatch-website` bucket. Put the access key pair in the app Secret. + +## 4. App secrets + +Template: [overlays/dev/secret.example.yaml](overlays/dev/secret.example.yaml) +(not applied by Kustomize). + +```bash +kubectl -n flycatch-website-dev create secret generic flycatch-website-secrets \ + --from-literal=DATABASE_URL='postgresql+psycopg://flycatch_website:@postgres.database.svc.cluster.local:5432/flycatch_website' \ + --from-literal=S3_ACCESS_KEY='' \ + --from-literal=S3_SECRET_KEY='' \ + --from-literal=SESSION_SECRET='' \ + --from-literal=CSRF_SECRET='' \ + --from-literal=JWT_SECRET='' \ + --from-literal=BUILD_EXPORT_TOKEN='' \ + --dry-run=client -o yaml | kubectl apply -f - +``` + +## 5. DNS + +Create a Cloudflare A (or CNAME) record: + +```text +flycatch-website-dev.k3s.flycatchtech.in → +``` + +(Same LB IP used by other `*.k3s.flycatchtech.in` apps.) + +## 6. Build, push, and bump image tags + +From a machine that can reach Harbor (LAN/VPN), with a clean git working tree: + +```bash +export HARBOR_USERNAME='robot$flycatch-website+githubbot' +export HARBOR_PASSWORD='...' +./deployment/k8s/scripts/deploy-dev.sh +``` + +The script builds `linux/amd64` images, pushes `:SHA` and `:latest` to Harbor, +updates `overlays/dev/kustomization.yaml` image tags, commits, and pushes so Argo CD +can sync. + +Images: + +- `registry.k3s.flycatchtech.in/flycatch-website/backend` +- `registry.k3s.flycatchtech.in/flycatch-website/frontend` +- `registry.k3s.flycatchtech.in/flycatch-website/administration-fe` + +Frontend and Administration FE are built with +`PUBLIC_ORIGIN=https://flycatch-website-dev.k3s.flycatchtech.in` and +`PUBLIC_ENVIRONMENT=development`. + +## 7. Verify Argo CD sync + +```bash +kubectl -n argocd get application flycatch-website-dev +kubectl -n flycatch-website-dev get pods,ingress,certificate +``` + +## 8. One-time seed and staff bootstrap + +After the Backend pod is Ready: + +```bash +kubectl -n flycatch-website-dev exec -it deploy/backend -- flycatch-seed-records +kubectl -n flycatch-website-dev exec -it deploy/backend -- flycatch-bootstrap \ + --user-1-email admin1@example.com \ + --user-2-email admin2@example.com \ + --user-2-role editor +``` + +Sign in at `https://flycatch-website-dev.k3s.flycatchtech.in/admin`. + +## 9. SEO / noindex checks + +```bash +curl -sI https://flycatch-website-dev.k3s.flycatchtech.in/ | grep -i robots +curl -s https://flycatch-website-dev.k3s.flycatchtech.in/robots.txt +``` + +Expect `X-Robots-Tag: noindex, nofollow` and `Disallow: /` in robots.txt. + +## Rollback + +Revert the image-tag commit in `overlays/dev/kustomization.yaml` (or re-run +`deploy-dev.sh` from an older commit) and let Argo CD sync. Secrets, DNS, and the +shared Postgres/MinIO data are unchanged by that rollback. diff --git a/deployment/Caddyfile b/deployment/k8s/base/Caddyfile similarity index 83% rename from deployment/Caddyfile rename to deployment/k8s/base/Caddyfile index 6c525ac..e10740b 100644 --- a/deployment/Caddyfile +++ b/deployment/k8s/base/Caddyfile @@ -1,3 +1,5 @@ +# Shared gateway routing for Docker Compose and k8s. +# Compose mounts this path; k8s generates a ConfigMap from it. # Path-based gateway — single origin for Frontend, Administration FE, and Backend :{$GATEWAY_PORT:8080} { diff --git a/deployment/k8s/base/administration-fe-deployment.yaml b/deployment/k8s/base/administration-fe-deployment.yaml new file mode 100644 index 0000000..c90e7b0 --- /dev/null +++ b/deployment/k8s/base/administration-fe-deployment.yaml @@ -0,0 +1,52 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: administration-fe + labels: + app.kubernetes.io/name: flycatch-website + app.kubernetes.io/component: administration-fe +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: flycatch-website + app.kubernetes.io/component: administration-fe + template: + metadata: + labels: + app.kubernetes.io/name: flycatch-website + app.kubernetes.io/component: administration-fe + spec: + imagePullSecrets: + - name: harbor-pull + containers: + - name: administration-fe + image: registry.k3s.flycatchtech.in/flycatch-website/administration-fe:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 4173 + protocol: TCP + readinessProbe: + httpGet: + path: /admin/ + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + livenessProbe: + httpGet: + path: /admin/ + port: http + initialDelaySeconds: 10 + periodSeconds: 20 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 256Mi diff --git a/deployment/k8s/base/administration-fe-service.yaml b/deployment/k8s/base/administration-fe-service.yaml new file mode 100644 index 0000000..9c8f243 --- /dev/null +++ b/deployment/k8s/base/administration-fe-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: administration-fe + labels: + app.kubernetes.io/name: flycatch-website + app.kubernetes.io/component: administration-fe +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: flycatch-website + app.kubernetes.io/component: administration-fe + ports: + - name: http + port: 4173 + targetPort: http + protocol: TCP diff --git a/deployment/k8s/base/backend-deployment.yaml b/deployment/k8s/base/backend-deployment.yaml new file mode 100644 index 0000000..539e904 --- /dev/null +++ b/deployment/k8s/base/backend-deployment.yaml @@ -0,0 +1,57 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend + labels: + app.kubernetes.io/name: flycatch-website + app.kubernetes.io/component: backend +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: flycatch-website + app.kubernetes.io/component: backend + template: + metadata: + labels: + app.kubernetes.io/name: flycatch-website + app.kubernetes.io/component: backend + spec: + imagePullSecrets: + - name: harbor-pull + containers: + - name: backend + image: registry.k3s.flycatchtech.in/flycatch-website/backend:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8000 + protocol: TCP + envFrom: + - configMapRef: + name: flycatch-website-config + - secretRef: + name: flycatch-website-secrets + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 15 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 45 + periodSeconds: 20 + timeoutSeconds: 5 + failureThreshold: 3 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi diff --git a/deployment/k8s/base/backend-service.yaml b/deployment/k8s/base/backend-service.yaml new file mode 100644 index 0000000..3b96eaf --- /dev/null +++ b/deployment/k8s/base/backend-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: backend + labels: + app.kubernetes.io/name: flycatch-website + app.kubernetes.io/component: backend +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: flycatch-website + app.kubernetes.io/component: backend + ports: + - name: http + port: 8000 + targetPort: http + protocol: TCP diff --git a/deployment/k8s/base/configmap.yaml b/deployment/k8s/base/configmap.yaml new file mode 100644 index 0000000..91da033 --- /dev/null +++ b/deployment/k8s/base/configmap.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: flycatch-website-config + labels: + app.kubernetes.io/name: flycatch-website +data: + PUBLIC_ORIGIN: https://flycatch-website-dev.k3s.flycatchtech.in + PUBLIC_ENVIRONMENT: development + ENVIRONMENT: development + S3_ENDPOINT: http://minio.database.svc.cluster.local:9000 + S3_BUCKET: flycatch-website + S3_REGION: us-east-1 + S3_USE_SSL: "false" + JWT_ACCESS_MINUTES: "15" + GATEWAY_PORT: "8080" diff --git a/deployment/k8s/base/frontend-deployment.yaml b/deployment/k8s/base/frontend-deployment.yaml new file mode 100644 index 0000000..261b44d --- /dev/null +++ b/deployment/k8s/base/frontend-deployment.yaml @@ -0,0 +1,52 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend + labels: + app.kubernetes.io/name: flycatch-website + app.kubernetes.io/component: frontend +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: flycatch-website + app.kubernetes.io/component: frontend + template: + metadata: + labels: + app.kubernetes.io/name: flycatch-website + app.kubernetes.io/component: frontend + spec: + imagePullSecrets: + - name: harbor-pull + containers: + - name: frontend + image: registry.k3s.flycatchtech.in/flycatch-website/frontend:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 4321 + protocol: TCP + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 10 + periodSeconds: 20 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 256Mi diff --git a/deployment/k8s/base/frontend-service.yaml b/deployment/k8s/base/frontend-service.yaml new file mode 100644 index 0000000..deeed4e --- /dev/null +++ b/deployment/k8s/base/frontend-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: frontend + labels: + app.kubernetes.io/name: flycatch-website + app.kubernetes.io/component: frontend +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: flycatch-website + app.kubernetes.io/component: frontend + ports: + - name: http + port: 4321 + targetPort: http + protocol: TCP diff --git a/deployment/k8s/base/gateway-deployment.yaml b/deployment/k8s/base/gateway-deployment.yaml new file mode 100644 index 0000000..447b6f1 --- /dev/null +++ b/deployment/k8s/base/gateway-deployment.yaml @@ -0,0 +1,60 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gateway + labels: + app.kubernetes.io/name: flycatch-website + app.kubernetes.io/component: gateway +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: flycatch-website + app.kubernetes.io/component: gateway + template: + metadata: + labels: + app.kubernetes.io/name: flycatch-website + app.kubernetes.io/component: gateway + spec: + containers: + - name: gateway + image: caddy:2-alpine + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + protocol: TCP + env: + - name: GATEWAY_PORT + value: "8080" + volumeMounts: + - name: caddyfile + mountPath: /etc/caddy/Caddyfile + subPath: Caddyfile + readOnly: true + readinessProbe: + tcpSocket: + port: http + initialDelaySeconds: 3 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + livenessProbe: + tcpSocket: + port: http + initialDelaySeconds: 10 + periodSeconds: 20 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + requests: + cpu: 25m + memory: 32Mi + limits: + cpu: 200m + memory: 128Mi + volumes: + - name: caddyfile + configMap: + name: flycatch-website-caddyfile diff --git a/deployment/k8s/base/gateway-service.yaml b/deployment/k8s/base/gateway-service.yaml new file mode 100644 index 0000000..d2507d7 --- /dev/null +++ b/deployment/k8s/base/gateway-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: gateway + labels: + app.kubernetes.io/name: flycatch-website + app.kubernetes.io/component: gateway +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: flycatch-website + app.kubernetes.io/component: gateway + ports: + - name: http + port: 8080 + targetPort: http + protocol: TCP diff --git a/deployment/k8s/base/kustomization.yaml b/deployment/k8s/base/kustomization.yaml new file mode 100644 index 0000000..d096a89 --- /dev/null +++ b/deployment/k8s/base/kustomization.yaml @@ -0,0 +1,24 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - namespace.yaml + - configmap.yaml + - backend-deployment.yaml + - backend-service.yaml + - frontend-deployment.yaml + - frontend-service.yaml + - administration-fe-deployment.yaml + - administration-fe-service.yaml + - gateway-deployment.yaml + - gateway-service.yaml + +configMapGenerator: + - name: flycatch-website-caddyfile + files: + - Caddyfile + +labels: + - pairs: + app.kubernetes.io/part-of: flycatch-website + includeSelectors: false diff --git a/deployment/k8s/base/namespace.yaml b/deployment/k8s/base/namespace.yaml new file mode 100644 index 0000000..e3e834f --- /dev/null +++ b/deployment/k8s/base/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: flycatch-website-dev + labels: + app.kubernetes.io/part-of: flycatch-website diff --git a/deployment/k8s/overlays/dev/ingress.yaml b/deployment/k8s/overlays/dev/ingress.yaml new file mode 100644 index 0000000..4eebbc9 --- /dev/null +++ b/deployment/k8s/overlays/dev/ingress.yaml @@ -0,0 +1,26 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: flycatch-website + labels: + app.kubernetes.io/name: flycatch-website + annotations: + cert-manager.io/cluster-issuer: letsencrypt-production + traefik.ingress.kubernetes.io/router.middlewares: flycatch-website-dev-noindex@kubernetescrd +spec: + ingressClassName: traefik + tls: + - hosts: + - flycatch-website-dev.k3s.flycatchtech.in + secretName: flycatch-website-dev-tls + rules: + - host: flycatch-website-dev.k3s.flycatchtech.in + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: gateway + port: + number: 8080 diff --git a/deployment/k8s/overlays/dev/kustomization.yaml b/deployment/k8s/overlays/dev/kustomization.yaml new file mode 100644 index 0000000..1c593cc --- /dev/null +++ b/deployment/k8s/overlays/dev/kustomization.yaml @@ -0,0 +1,30 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: flycatch-website-dev + +resources: + - ../../base + - ingress.yaml + - noindex-middleware.yaml + +images: + - name: registry.k3s.flycatchtech.in/flycatch-website/backend + newName: registry.k3s.flycatchtech.in/flycatch-website/backend + newTag: latest + - name: registry.k3s.flycatchtech.in/flycatch-website/frontend + newName: registry.k3s.flycatchtech.in/flycatch-website/frontend + newTag: latest + - name: registry.k3s.flycatchtech.in/flycatch-website/administration-fe + newName: registry.k3s.flycatchtech.in/flycatch-website/administration-fe + newTag: latest + +replicas: + - count: 1 + name: backend + - count: 1 + name: frontend + - count: 1 + name: administration-fe + - count: 1 + name: gateway diff --git a/deployment/k8s/overlays/dev/noindex-middleware.yaml b/deployment/k8s/overlays/dev/noindex-middleware.yaml new file mode 100644 index 0000000..7fdaa08 --- /dev/null +++ b/deployment/k8s/overlays/dev/noindex-middleware.yaml @@ -0,0 +1,10 @@ +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: noindex + labels: + app.kubernetes.io/name: flycatch-website +spec: + headers: + customResponseHeaders: + X-Robots-Tag: "noindex, nofollow" diff --git a/deployment/k8s/overlays/dev/secret.example.yaml b/deployment/k8s/overlays/dev/secret.example.yaml new file mode 100644 index 0000000..30831d0 --- /dev/null +++ b/deployment/k8s/overlays/dev/secret.example.yaml @@ -0,0 +1,25 @@ +# Opaque Secret — create in-cluster; do NOT commit real values. +# Example: +# kubectl -n flycatch-website-dev create secret generic flycatch-website-secrets \ +# --from-literal=DATABASE_URL='postgresql+psycopg://flycatch_website:PASSWORD@postgres.database.svc.cluster.local:5432/flycatch_website' \ +# --from-literal=S3_ACCESS_KEY='...' \ +# --from-literal=S3_SECRET_KEY='...' \ +# --from-literal=SESSION_SECRET='...' \ +# --from-literal=CSRF_SECRET='...' \ +# --from-literal=JWT_SECRET='...' \ +# --from-literal=BUILD_EXPORT_TOKEN='...' +apiVersion: v1 +kind: Secret +metadata: + name: flycatch-website-secrets + labels: + app.kubernetes.io/name: flycatch-website +type: Opaque +stringData: + DATABASE_URL: postgresql+psycopg://flycatch_website:REPLACE_ME_POSTGRES_PASSWORD@postgres.database.svc.cluster.local:5432/flycatch_website + S3_ACCESS_KEY: REPLACE_ME_MINIO_ACCESS_KEY + S3_SECRET_KEY: REPLACE_ME_MINIO_SECRET_KEY + SESSION_SECRET: REPLACE_ME_LONG_RANDOM_SESSION_SECRET + CSRF_SECRET: REPLACE_ME_LONG_RANDOM_CSRF_SECRET + JWT_SECRET: REPLACE_ME_LONG_RANDOM_JWT_SECRET + BUILD_EXPORT_TOKEN: REPLACE_ME_BUILD_EXPORT_TOKEN diff --git a/deployment/k8s/scripts/deploy-dev.sh b/deployment/k8s/scripts/deploy-dev.sh new file mode 100755 index 0000000..9dc1104 --- /dev/null +++ b/deployment/k8s/scripts/deploy-dev.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# Build + push dev images to Harbor and bump GitOps tags for Argo CD. +# Requires: docker, kustomize, git; env HARBOR_USERNAME + HARBOR_PASSWORD. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +cd "$ROOT" + +REGISTRY="${REGISTRY:-registry.k3s.flycatchtech.in}" +BACKEND_IMAGE="${BACKEND_IMAGE:-${REGISTRY}/flycatch-website/backend}" +FRONTEND_IMAGE="${FRONTEND_IMAGE:-${REGISTRY}/flycatch-website/frontend}" +ADMIN_FE_IMAGE="${ADMIN_FE_IMAGE:-${REGISTRY}/flycatch-website/administration-fe}" +OVERLAY="deployment/k8s/overlays/dev" +PUBLIC_ORIGIN="${PUBLIC_ORIGIN:-https://flycatch-website-dev.k3s.flycatchtech.in}" +PUBLIC_ENVIRONMENT="${PUBLIC_ENVIRONMENT:-development}" + +die() { + echo "error: $*" >&2 + exit 1 +} + +[[ -n "${HARBOR_USERNAME:-}" ]] || die "HARBOR_USERNAME is required" +[[ -n "${HARBOR_PASSWORD:-}" ]] || die "HARBOR_PASSWORD is required" + +command -v docker >/dev/null || die "docker is required" +command -v kustomize >/dev/null || die "kustomize is required on PATH" +command -v git >/dev/null || die "git is required" + +if [[ -n "$(git status --porcelain)" ]]; then + die "working tree is dirty; commit or stash changes before deploying" +fi + +TAG="$(git rev-parse HEAD)" +echo "==> tag ${TAG}" +echo "==> PUBLIC_ORIGIN=${PUBLIC_ORIGIN}" +echo "==> PUBLIC_ENVIRONMENT=${PUBLIC_ENVIRONMENT}" + +echo "==> docker login ${REGISTRY}" +echo "${HARBOR_PASSWORD}" | docker login "${REGISTRY}" -u "${HARBOR_USERNAME}" --password-stdin + +echo "==> build backend (linux/amd64)" +docker build --platform linux/amd64 -f apps/Backend/Dockerfile \ + -t "${BACKEND_IMAGE}:${TAG}" \ + -t "${BACKEND_IMAGE}:latest" \ + apps/Backend + +echo "==> build frontend (linux/amd64)" +docker build --platform linux/amd64 -f apps/Frontend/Dockerfile \ + --build-arg "PUBLIC_ORIGIN=${PUBLIC_ORIGIN}" \ + --build-arg "PUBLIC_ENVIRONMENT=${PUBLIC_ENVIRONMENT}" \ + -t "${FRONTEND_IMAGE}:${TAG}" \ + -t "${FRONTEND_IMAGE}:latest" \ + apps/Frontend + +echo "==> build administration-fe (linux/amd64)" +docker build --platform linux/amd64 -f apps/Administration-FE/Dockerfile \ + --build-context "specs=specs" \ + --build-arg "PUBLIC_ORIGIN=${PUBLIC_ORIGIN}" \ + --build-arg "PUBLIC_ENVIRONMENT=${PUBLIC_ENVIRONMENT}" \ + -t "${ADMIN_FE_IMAGE}:${TAG}" \ + -t "${ADMIN_FE_IMAGE}:latest" \ + apps/Administration-FE + +echo "==> push images" +docker push "${BACKEND_IMAGE}:${TAG}" +docker push "${BACKEND_IMAGE}:latest" +docker push "${FRONTEND_IMAGE}:${TAG}" +docker push "${FRONTEND_IMAGE}:latest" +docker push "${ADMIN_FE_IMAGE}:${TAG}" +docker push "${ADMIN_FE_IMAGE}:latest" + +echo "==> bump kustomize image tags" +( + cd "${OVERLAY}" + kustomize edit set image \ + "${BACKEND_IMAGE}=${BACKEND_IMAGE}:${TAG}" \ + "${FRONTEND_IMAGE}=${FRONTEND_IMAGE}:${TAG}" \ + "${ADMIN_FE_IMAGE}=${ADMIN_FE_IMAGE}:${TAG}" +) + +echo "==> commit and push GitOps tag bump" +git add "${OVERLAY}/kustomization.yaml" +if git diff --staged --quiet; then + echo "Image tags already up to date; nothing to commit" +else + git commit -m "chore(deploy): bump dev images to ${TAG}" + git push origin HEAD +fi + +echo +echo "Done. Argo CD should sync shortly." +echo " kubectl -n argocd get application flycatch-website-dev" +echo " kubectl -n flycatch-website-dev get pods,ingress" diff --git a/docs/onboarding.md b/docs/onboarding.md index b62b5f8..740d8a1 100644 --- a/docs/onboarding.md +++ b/docs/onboarding.md @@ -4,14 +4,14 @@ Docker Compose starts Frontend, Administration FE, Backend, PostgreSQL, MinIO, and the gateway. It does **not** create staff users or apply migrations. There is no default login and no sign-up screen. -1. `cp deployment/.env.example deployment/.env` and set `JWT_SECRET` (and other `change-me` values) to long random secrets. Do not commit `deployment/.env`. -2. `docker compose -f deployment/docker-compose.yml up -d --build` -3. Backend migrations: `docker compose -f deployment/docker-compose.yml exec backend alembic upgrade head` -4. Seed records: `docker compose -f deployment/docker-compose.yml exec backend flycatch-seed-records` +1. `cp deployment/compose/.env.example deployment/compose/.env` and set `JWT_SECRET` (and other `change-me` values) to long random secrets. Do not commit `deployment/compose/.env`. +2. `docker compose -f deployment/compose/docker-compose.yml up -d --build` +3. Backend migrations: `docker compose -f deployment/compose/docker-compose.yml exec backend alembic upgrade head` +4. Seed records: `docker compose -f deployment/compose/docker-compose.yml exec backend flycatch-seed-records` 5. Bootstrap default roles and two staff users: ```bash - docker compose -f deployment/docker-compose.yml exec backend flycatch-bootstrap \ + docker compose -f deployment/compose/docker-compose.yml exec backend flycatch-bootstrap \ --user-1-email admin1@example.com \ --user-2-email admin2@example.com \ --user-2-role editor @@ -24,7 +24,7 @@ Docker Compose starts Frontend, Administration FE, Backend, PostgreSQL, MinIO, a These emails are examples only. Passwords are **not** stored in the repo: they are prompted (minimum 12 characters) unless you pass `--user-1-password` and `--user-2-password`. Re-running with the same emails is idempotent and does not change existing passwords. Pytest fixtures (`editor1@example.com` / test passwords) are not created by this command. -6. Later staff: `docker compose -f deployment/docker-compose.yml exec backend flycatch-provision-admin --email someone@example.com --role editor` (`--role` is required: `administrator` or `editor`). +6. Later staff: `docker compose -f deployment/compose/docker-compose.yml exec backend flycatch-provision-admin --email someone@example.com --role editor` (`--role` is required: `administrator` or `editor`). 7. Generate Administration FE types: `cd apps/Administration-FE && npm run generate:client` 8. Build Frontend: `cd apps/Frontend && pnpm install && pnpm run build` diff --git a/specs/001-website-foundation/quickstart.md b/specs/001-website-foundation/quickstart.md index 069bfc8..3b10780 100644 --- a/specs/001-website-foundation/quickstart.md +++ b/specs/001-website-foundation/quickstart.md @@ -15,13 +15,13 @@ Related artifacts: [spec.md](./spec.md), [data-model.md](./data-model.md), [cont ## Setup -1. Copy environment config: `cp deployment/.env.example deployment/.env` and adjust values. -2. Start all services: `docker compose -f deployment/docker-compose.yml up -d --build` (see [README.md](../../README.md#quick-start-docker-compose)). +1. Copy environment config: `cp deployment/compose/.env.example deployment/compose/.env` and adjust values. +2. Start all services: `docker compose -f deployment/compose/docker-compose.yml up -d --build` (see [README.md](../../README.md#quick-start-docker-compose)). 3. Apply Backend migrations, seed records, and bootstrap two staff users (`flycatch-bootstrap`). Compose does not create a default login. Full commands: [docs/onboarding.md](../../docs/onboarding.md). 4. Generate OpenAPI consumers for Frontend and Administration FE from `specs/001-website-foundation/contracts/`; confirm Backend served OpenAPI matches the same files. 5. Export the published snapshot (empty or seed `home` + `site_settings`) into `apps/Frontend/src/data/published.json`. 6. Build `apps/Frontend` with `astro build` (`output: 'static'`). -7. Rebuild compose services when app images change: `docker compose -f deployment/docker-compose.yml up -d --build`. +7. Rebuild compose services when app images change: `docker compose -f deployment/compose/docker-compose.yml up -d --build`. 8. Open the gateway origin from `.env` (default `http://localhost:8080`): `/` → Frontend, `/admin` → Administration FE, `/api` → Backend. Do not point the public site at live API URLs for ordinary browsing. @@ -95,7 +95,7 @@ Do not point the public site at live API URLs for ordinary browsing. | Environment | Public HTML | Admin / API | | --- | --- | --- | -| Local | `docker compose -f deployment/docker-compose.yml up` | Gateway origin from `.env` | +| Local | `docker compose -f deployment/compose/docker-compose.yml up` | Gateway origin from `.env` | | Preview | Same build command, HTTPS | HTTPS, production-like headers | | Production | Same build command, cacheable assets, invalidate on new published revision | HTTPS, idle session timeout enforced | diff --git a/specs/001-website-foundation/research.md b/specs/001-website-foundation/research.md index d28c0e7..53ce9d0 100644 --- a/specs/001-website-foundation/research.md +++ b/specs/001-website-foundation/research.md @@ -204,7 +204,7 @@ Promotion is blocked when any gate fails (FR-041, FR-043, SC-006). ## 13. Environments and hosting -**Decision**: Hosting vendor is out of scope (spec). Local/preview/production MUST share the same HTML production path: export published snapshot → `astro build` in `apps/Frontend` → deploy static files with cache-busting. All foundation services (Frontend, Administration FE, Backend, PostgreSQL, object storage) share `deployment/docker-compose.yml` and `.env` configuration. Gateway provides one origin (`/`, `/admin`, `/api`). +**Decision**: Hosting vendor is out of scope (spec). Local/preview/production MUST share the same HTML production path: export published snapshot → `astro build` in `apps/Frontend` → deploy static files with cache-busting. All foundation services (Frontend, Administration FE, Backend, PostgreSQL, object storage) share `deployment/compose/docker-compose.yml` and `.env` configuration. Gateway provides one origin (`/`, `/admin`, `/api`). **Rationale**: FR-004, FR-030, FR-041. Equivalent public HTML for the same revision in preview and production. One deployment folder avoids duplicated compose/env setup. diff --git a/specs/001-website-foundation/tasks.md b/specs/001-website-foundation/tasks.md index f682d65..a048d66 100644 --- a/specs/001-website-foundation/tasks.md +++ b/specs/001-website-foundation/tasks.md @@ -40,10 +40,10 @@ description: "Task list for Website Foundation feature implementation" - [x] T006 [P] Configure ESLint/Prettier for Frontend and Administration-FE (`apps/Frontend/`, `apps/Administration-FE/`) - [x] T007 [P] Configure Ruff and pytest for Backend in `apps/Backend/pyproject.toml` - [x] T008 [P] Add Dockerfiles for Frontend, Administration-FE, and Backend (`apps/Frontend/Dockerfile`, `apps/Administration-FE/Dockerfile`, `apps/Backend/Dockerfile`) -- [x] T009 Complete `deployment/docker-compose.yml`, `deployment/Caddyfile`, and `deployment/.env.example` for gateway path split (`/`, `/admin`, `/api`) +- [x] T009 Complete `deployment/compose/docker-compose.yml`, `deployment/k8s/base/Caddyfile`, and `deployment/compose/.env.example` for gateway path split (`/`, `/admin`, `/api`) - [x] T010 Write root `README.md` with project overview, deployment usage, and contract consumption rules -**Checkpoint**: All three apps scaffolded; `docker compose -f deployment/docker-compose.yml up` starts services (may serve placeholders) +**Checkpoint**: All three apps scaffolded; `docker compose -f deployment/compose/docker-compose.yml up` starts services (may serve placeholders) --- diff --git a/specs/002-auth-rbac/quickstart.md b/specs/002-auth-rbac/quickstart.md index 9a6cb9b..17abc84 100644 --- a/specs/002-auth-rbac/quickstart.md +++ b/specs/002-auth-rbac/quickstart.md @@ -14,10 +14,10 @@ Related artifacts: [spec.md](./spec.md), [data-model.md](./data-model.md), [cont ## Setup -1. Copy or update `deployment/.env` with `jwt_secret` (long random) in addition to existing Backend secrets. -2. Start services: `docker compose -f deployment/docker-compose.yml up -d --build` (see [README.md](../../README.md#quick-start-docker-compose)). +1. Copy or update `deployment/compose/.env` with `jwt_secret` (long random) in addition to existing Backend secrets. +2. Start services: `docker compose -f deployment/compose/docker-compose.yml up -d --build` (see [README.md](../../README.md#quick-start-docker-compose)). 3. Apply Backend migrations (includes roles and refresh-session columns). -4. Run bootstrap (see [bootstrap.cli.yaml](./contracts/bootstrap.cli.yaml)). Prefix with `docker compose -f deployment/docker-compose.yml exec backend` when using Compose: +4. Run bootstrap (see [bootstrap.cli.yaml](./contracts/bootstrap.cli.yaml)). Prefix with `docker compose -f deployment/compose/docker-compose.yml exec backend` when using Compose: ```bash flycatch-bootstrap \ diff --git a/specs/002-auth-rbac/tasks.md b/specs/002-auth-rbac/tasks.md index 489431a..f795ffb 100644 --- a/specs/002-auth-rbac/tasks.md +++ b/specs/002-auth-rbac/tasks.md @@ -34,7 +34,7 @@ description: "Task list for Authentication and Authorisation (RBAC) feature impl - [x] T001 Add `PyJWT` to Backend dependencies in `apps/Backend/pyproject.toml` - [x] T002 [P] Add `jwt_secret` and `jwt_access_minutes` (default 15) settings in `apps/Backend/src/flycatch_api/config.py` -- [x] T003 [P] Add `JWT_SECRET` and `JWT_ACCESS_MINUTES` placeholders (no well-known staff passwords) in `deployment/.env.example` +- [x] T003 [P] Add `JWT_SECRET` and `JWT_ACCESS_MINUTES` placeholders (no well-known staff passwords) in `deployment/compose/.env.example` - [x] T004 [P] Point Administration FE OpenAPI generation at `admin-auth.v2`, `admin-rbac.v1`, `admin-management.v2`, and `publish.v2` in `apps/Administration-FE/scripts/generate-client.mjs` and `apps/Administration-FE/package.json` - [x] T005 [P] Extend `scripts/validate-contracts.mjs` to validate OpenAPI YAML under `specs/002-auth-rbac/contracts/` (skip `bootstrap.cli.yaml`) - [x] T006 Register `flycatch-bootstrap` console script in `apps/Backend/pyproject.toml` From 501ba047d90edd495e488ba4211e629bab6db34d Mon Sep 17 00:00:00 2001 From: Liju Kuriakose Date: Tue, 1 Sep 2026 20:11:19 +0530 Subject: [PATCH 02/12] chore(deploy): bump dev images to fd0c30f579f7834c1d41454b5ac20d36aafaf8c7 --- .../k8s/overlays/dev/kustomization.yaml | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/deployment/k8s/overlays/dev/kustomization.yaml b/deployment/k8s/overlays/dev/kustomization.yaml index 1c593cc..97ed094 100644 --- a/deployment/k8s/overlays/dev/kustomization.yaml +++ b/deployment/k8s/overlays/dev/kustomization.yaml @@ -4,27 +4,27 @@ kind: Kustomization namespace: flycatch-website-dev resources: - - ../../base - - ingress.yaml - - noindex-middleware.yaml +- ../../base +- ingress.yaml +- noindex-middleware.yaml images: - - name: registry.k3s.flycatchtech.in/flycatch-website/backend - newName: registry.k3s.flycatchtech.in/flycatch-website/backend - newTag: latest - - name: registry.k3s.flycatchtech.in/flycatch-website/frontend - newName: registry.k3s.flycatchtech.in/flycatch-website/frontend - newTag: latest - - name: registry.k3s.flycatchtech.in/flycatch-website/administration-fe - newName: registry.k3s.flycatchtech.in/flycatch-website/administration-fe - newTag: latest +- name: registry.k3s.flycatchtech.in/flycatch-website/administration-fe + newName: registry.k3s.flycatchtech.in/flycatch-website/administration-fe + newTag: fd0c30f579f7834c1d41454b5ac20d36aafaf8c7 +- name: registry.k3s.flycatchtech.in/flycatch-website/backend + newName: registry.k3s.flycatchtech.in/flycatch-website/backend + newTag: fd0c30f579f7834c1d41454b5ac20d36aafaf8c7 +- name: registry.k3s.flycatchtech.in/flycatch-website/frontend + newName: registry.k3s.flycatchtech.in/flycatch-website/frontend + newTag: fd0c30f579f7834c1d41454b5ac20d36aafaf8c7 replicas: - - count: 1 - name: backend - - count: 1 - name: frontend - - count: 1 - name: administration-fe - - count: 1 - name: gateway +- count: 1 + name: backend +- count: 1 + name: frontend +- count: 1 + name: administration-fe +- count: 1 + name: gateway From f8807732f2f1474f8faea212b7dbb245d91d5193 Mon Sep 17 00:00:00 2001 From: Liju Kuriakose Date: Tue, 1 Sep 2026 20:12:34 +0530 Subject: [PATCH 03/12] Fix backend noindex middleware by importing settings. Without the import, /health returned 500 and readiness probes failed. Co-authored-by: Cursor --- apps/Backend/src/flycatch_api/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/Backend/src/flycatch_api/main.py b/apps/Backend/src/flycatch_api/main.py index 88a7548..39b272e 100644 --- a/apps/Backend/src/flycatch_api/main.py +++ b/apps/Backend/src/flycatch_api/main.py @@ -3,6 +3,7 @@ from fastapi.responses import JSONResponse from sqlalchemy.exc import SQLAlchemyError +from flycatch_api.config import settings from flycatch_api.api import ( admin_ai_services, admin_auth, From 1331dab854ba093c996417d607681c3d0247b259 Mon Sep 17 00:00:00 2001 From: Liju Kuriakose Date: Tue, 1 Sep 2026 20:13:40 +0530 Subject: [PATCH 04/12] chore(deploy): bump dev images to f8807732f2f1474f8faea212b7dbb245d91d5193 --- deployment/k8s/overlays/dev/kustomization.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deployment/k8s/overlays/dev/kustomization.yaml b/deployment/k8s/overlays/dev/kustomization.yaml index 97ed094..f6b702a 100644 --- a/deployment/k8s/overlays/dev/kustomization.yaml +++ b/deployment/k8s/overlays/dev/kustomization.yaml @@ -11,13 +11,13 @@ resources: images: - name: registry.k3s.flycatchtech.in/flycatch-website/administration-fe newName: registry.k3s.flycatchtech.in/flycatch-website/administration-fe - newTag: fd0c30f579f7834c1d41454b5ac20d36aafaf8c7 + newTag: f8807732f2f1474f8faea212b7dbb245d91d5193 - name: registry.k3s.flycatchtech.in/flycatch-website/backend newName: registry.k3s.flycatchtech.in/flycatch-website/backend - newTag: fd0c30f579f7834c1d41454b5ac20d36aafaf8c7 + newTag: f8807732f2f1474f8faea212b7dbb245d91d5193 - name: registry.k3s.flycatchtech.in/flycatch-website/frontend newName: registry.k3s.flycatchtech.in/flycatch-website/frontend - newTag: fd0c30f579f7834c1d41454b5ac20d36aafaf8c7 + newTag: f8807732f2f1474f8faea212b7dbb245d91d5193 replicas: - count: 1 From 6461f8f57cfec44b4a74965b1bd74ba9d832b6fc Mon Sep 17 00:00:00 2001 From: Liju Kuriakose Date: Tue, 1 Sep 2026 20:15:53 +0530 Subject: [PATCH 05/12] Allow preview hosts for k8s Ingress hostname. Astro/Vite preview was returning 403 for flycatch-website-dev.k3s.flycatchtech.in. Co-authored-by: Cursor --- apps/Administration-FE/astro.config.mjs | 3 +++ apps/Frontend/astro.config.mjs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/apps/Administration-FE/astro.config.mjs b/apps/Administration-FE/astro.config.mjs index 48c973b..4877003 100644 --- a/apps/Administration-FE/astro.config.mjs +++ b/apps/Administration-FE/astro.config.mjs @@ -7,6 +7,9 @@ export default defineConfig({ trailingSlash: 'always', integrations: [react()], vite: { + preview: { + allowedHosts: true, + }, server: { proxy: { '/api': { diff --git a/apps/Frontend/astro.config.mjs b/apps/Frontend/astro.config.mjs index b39b15d..b97c8c8 100644 --- a/apps/Frontend/astro.config.mjs +++ b/apps/Frontend/astro.config.mjs @@ -13,6 +13,9 @@ export default defineConfig({ inlineStylesheets: 'always', }, vite: { + preview: { + allowedHosts: true, + }, build: { rollupOptions: { output: { From 988ac83eab0b6fb475084f6426551663b88c9ddf Mon Sep 17 00:00:00 2001 From: Liju Kuriakose Date: Tue, 1 Sep 2026 20:18:22 +0530 Subject: [PATCH 06/12] chore(deploy): bump dev images to 6461f8f57cfec44b4a74965b1bd74ba9d832b6fc --- deployment/k8s/overlays/dev/kustomization.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deployment/k8s/overlays/dev/kustomization.yaml b/deployment/k8s/overlays/dev/kustomization.yaml index f6b702a..fe5ea88 100644 --- a/deployment/k8s/overlays/dev/kustomization.yaml +++ b/deployment/k8s/overlays/dev/kustomization.yaml @@ -11,13 +11,13 @@ resources: images: - name: registry.k3s.flycatchtech.in/flycatch-website/administration-fe newName: registry.k3s.flycatchtech.in/flycatch-website/administration-fe - newTag: f8807732f2f1474f8faea212b7dbb245d91d5193 + newTag: 6461f8f57cfec44b4a74965b1bd74ba9d832b6fc - name: registry.k3s.flycatchtech.in/flycatch-website/backend newName: registry.k3s.flycatchtech.in/flycatch-website/backend - newTag: f8807732f2f1474f8faea212b7dbb245d91d5193 + newTag: 6461f8f57cfec44b4a74965b1bd74ba9d832b6fc - name: registry.k3s.flycatchtech.in/flycatch-website/frontend newName: registry.k3s.flycatchtech.in/flycatch-website/frontend - newTag: f8807732f2f1474f8faea212b7dbb245d91d5193 + newTag: 6461f8f57cfec44b4a74965b1bd74ba9d832b6fc replicas: - count: 1 From c01d72bbecace3a026051d542625da843421c1e9 Mon Sep 17 00:00:00 2001 From: Liju Kuriakose Date: Tue, 1 Sep 2026 20:18:42 +0530 Subject: [PATCH 07/12] Include astro.config.mjs in runtime images for preview host policy. Vite preview reads the config at runtime; without it, Ingress hostnames were blocked. Co-authored-by: Cursor --- apps/Administration-FE/Dockerfile | 1 + apps/Frontend/Dockerfile | 1 + 2 files changed, 2 insertions(+) diff --git a/apps/Administration-FE/Dockerfile b/apps/Administration-FE/Dockerfile index 32b3d40..674cd5b 100644 --- a/apps/Administration-FE/Dockerfile +++ b/apps/Administration-FE/Dockerfile @@ -17,5 +17,6 @@ COPY package.json package-lock.json ./ RUN npm ci --omit=dev COPY --from=build /app/dist ./dist COPY --from=build /app/package.json ./ +COPY --from=build /app/astro.config.mjs ./ EXPOSE 4173 CMD ["npm", "run", "preview"] diff --git a/apps/Frontend/Dockerfile b/apps/Frontend/Dockerfile index c96cd32..3a5773f 100644 --- a/apps/Frontend/Dockerfile +++ b/apps/Frontend/Dockerfile @@ -15,5 +15,6 @@ COPY package.json package-lock.json ./ RUN npm ci --omit=dev COPY --from=build /app/dist ./dist COPY --from=build /app/package.json ./ +COPY --from=build /app/astro.config.mjs ./ EXPOSE 4321 CMD ["npm", "run", "preview"] From 124a8370f2568f0f0cd755eed445d7a7081ff7fe Mon Sep 17 00:00:00 2001 From: Liju Kuriakose Date: Tue, 1 Sep 2026 20:20:55 +0530 Subject: [PATCH 08/12] chore(deploy): bump dev images to c01d72bbecace3a026051d542625da843421c1e9 --- deployment/k8s/overlays/dev/kustomization.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deployment/k8s/overlays/dev/kustomization.yaml b/deployment/k8s/overlays/dev/kustomization.yaml index fe5ea88..37a9bc5 100644 --- a/deployment/k8s/overlays/dev/kustomization.yaml +++ b/deployment/k8s/overlays/dev/kustomization.yaml @@ -11,13 +11,13 @@ resources: images: - name: registry.k3s.flycatchtech.in/flycatch-website/administration-fe newName: registry.k3s.flycatchtech.in/flycatch-website/administration-fe - newTag: 6461f8f57cfec44b4a74965b1bd74ba9d832b6fc + newTag: c01d72bbecace3a026051d542625da843421c1e9 - name: registry.k3s.flycatchtech.in/flycatch-website/backend newName: registry.k3s.flycatchtech.in/flycatch-website/backend - newTag: 6461f8f57cfec44b4a74965b1bd74ba9d832b6fc + newTag: c01d72bbecace3a026051d542625da843421c1e9 - name: registry.k3s.flycatchtech.in/flycatch-website/frontend newName: registry.k3s.flycatchtech.in/flycatch-website/frontend - newTag: 6461f8f57cfec44b4a74965b1bd74ba9d832b6fc + newTag: c01d72bbecace3a026051d542625da843421c1e9 replicas: - count: 1 From fff63c1a53d3b47393d335786b1a2f0e1a45d283 Mon Sep 17 00:00:00 2001 From: Liju Kuriakose Date: Tue, 1 Sep 2026 20:24:38 +0530 Subject: [PATCH 09/12] Serve static Frontend and Admin images with serve instead of Vite preview. Astro preview host checks blocked the k8s Ingress hostname; serve avoids that. Co-authored-by: Cursor --- apps/Administration-FE/Dockerfile | 7 ++----- apps/Administration-FE/package.json | 2 +- apps/Frontend/Dockerfile | 7 ++----- apps/Frontend/package.json | 2 +- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/apps/Administration-FE/Dockerfile b/apps/Administration-FE/Dockerfile index 674cd5b..1d5a09a 100644 --- a/apps/Administration-FE/Dockerfile +++ b/apps/Administration-FE/Dockerfile @@ -13,10 +13,7 @@ RUN npm run generate:client && npm run build FROM node:22-alpine WORKDIR /app -COPY package.json package-lock.json ./ -RUN npm ci --omit=dev +RUN npm install -g serve@14 COPY --from=build /app/dist ./dist -COPY --from=build /app/package.json ./ -COPY --from=build /app/astro.config.mjs ./ EXPOSE 4173 -CMD ["npm", "run", "preview"] +CMD ["serve", "dist", "-l", "tcp://0.0.0.0:4173", "--no-clipboard", "--no-port-switching"] diff --git a/apps/Administration-FE/package.json b/apps/Administration-FE/package.json index 5d628b0..4117ef6 100644 --- a/apps/Administration-FE/package.json +++ b/apps/Administration-FE/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "astro dev --host --port 4173", "build": "astro build", - "preview": "astro preview --host 0.0.0.0 --port 4173", + "preview": "astro preview --host 0.0.0.0 --port 4173 --allowed-hosts", "check": "astro check", "generate:client": "node scripts/generate-client.mjs", "check:contracts": "node scripts/check-contract-drift.mjs", diff --git a/apps/Frontend/Dockerfile b/apps/Frontend/Dockerfile index 3a5773f..6c9b736 100644 --- a/apps/Frontend/Dockerfile +++ b/apps/Frontend/Dockerfile @@ -11,10 +11,7 @@ RUN npm run build FROM node:22-alpine WORKDIR /app -COPY package.json package-lock.json ./ -RUN npm ci --omit=dev +RUN npm install -g serve@14 COPY --from=build /app/dist ./dist -COPY --from=build /app/package.json ./ -COPY --from=build /app/astro.config.mjs ./ EXPOSE 4321 -CMD ["npm", "run", "preview"] +CMD ["serve", "dist", "-l", "tcp://0.0.0.0:4321", "--no-clipboard", "--no-port-switching"] diff --git a/apps/Frontend/package.json b/apps/Frontend/package.json index d36765f..41ca8e7 100644 --- a/apps/Frontend/package.json +++ b/apps/Frontend/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "astro dev --host", "build": "node scripts/ensure-published-snapshot.mjs && astro build", - "preview": "astro preview --host 0.0.0.0 --port 4321", + "preview": "astro preview --host 0.0.0.0 --port 4321 --allowed-hosts", "check": "node scripts/ensure-published-snapshot.mjs && astro check", "generate:types": "node scripts/generate-types.mjs", "check:contracts": "node scripts/check-contract-drift.mjs", From 35aff5c1a7fdbabc782666c93774acdb5d912ac9 Mon Sep 17 00:00:00 2001 From: Liju Kuriakose Date: Tue, 1 Sep 2026 20:27:24 +0530 Subject: [PATCH 10/12] chore(deploy): bump dev images to fff63c1a53d3b47393d335786b1a2f0e1a45d283 --- deployment/k8s/overlays/dev/kustomization.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deployment/k8s/overlays/dev/kustomization.yaml b/deployment/k8s/overlays/dev/kustomization.yaml index 37a9bc5..88a565e 100644 --- a/deployment/k8s/overlays/dev/kustomization.yaml +++ b/deployment/k8s/overlays/dev/kustomization.yaml @@ -11,13 +11,13 @@ resources: images: - name: registry.k3s.flycatchtech.in/flycatch-website/administration-fe newName: registry.k3s.flycatchtech.in/flycatch-website/administration-fe - newTag: c01d72bbecace3a026051d542625da843421c1e9 + newTag: fff63c1a53d3b47393d335786b1a2f0e1a45d283 - name: registry.k3s.flycatchtech.in/flycatch-website/backend newName: registry.k3s.flycatchtech.in/flycatch-website/backend - newTag: c01d72bbecace3a026051d542625da843421c1e9 + newTag: fff63c1a53d3b47393d335786b1a2f0e1a45d283 - name: registry.k3s.flycatchtech.in/flycatch-website/frontend newName: registry.k3s.flycatchtech.in/flycatch-website/frontend - newTag: c01d72bbecace3a026051d542625da843421c1e9 + newTag: fff63c1a53d3b47393d335786b1a2f0e1a45d283 replicas: - count: 1 From 0b278535ef365379639be4f76ad8f4ac6fe84425 Mon Sep 17 00:00:00 2001 From: Liju Kuriakose Date: Tue, 1 Sep 2026 21:36:33 +0530 Subject: [PATCH 11/12] Add security headers to Caddyfile and document checks in README Implemented security headers in the Caddyfile, including HSTS, CSP, and other policies for both public and admin routes. Updated README to include instructions for verifying these headers via curl commands. --- deployment/k8s/README.md | 24 ++++++++++++++++++++++++ deployment/k8s/base/Caddyfile | 18 ++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/deployment/k8s/README.md b/deployment/k8s/README.md index bb3bb23..988bac4 100644 --- a/deployment/k8s/README.md +++ b/deployment/k8s/README.md @@ -218,6 +218,30 @@ curl -s https://flycatch-website-dev.k3s.flycatchtech.in/robots.txt Expect `X-Robots-Tag: noindex, nofollow` and `Disallow: /` in robots.txt. +## 10. Security headers checks + +The shared Caddy gateway ([base/Caddyfile](base/Caddyfile)) sets HSTS, COOP, +`X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, and +`Permissions-Policy` on every response. CSP is route-scoped: strict on the +public site, wider on `/admin*` (inline scripts for Astro islands, Google +Fonts, and `blob:` media previews). + +```bash +curl -sI https://flycatch-website-dev.k3s.flycatchtech.in/ \ + | grep -iE 'content-security|strict-transport|cross-origin|x-frame|x-content' +curl -sI https://flycatch-website-dev.k3s.flycatchtech.in/admin/ \ + | grep -i content-security +``` + +Expect `Strict-Transport-Security`, `Cross-Origin-Opener-Policy: same-origin`, +`X-Frame-Options: DENY`, and a `Content-Security-Policy` on both `/` and +`/admin/`. The admin policy should include `'unsafe-inline'` in `script-src` +and the Google Fonts origins. After deploy, load `/admin/` in a browser and +confirm the console has no CSP violations. + +Local Compose (HTTP on `:8080`) returns the same headers; browsers ignore +HSTS over non-HTTPS. + ## Rollback Revert the image-tag commit in `overlays/dev/kustomization.yaml` (or re-run diff --git a/deployment/k8s/base/Caddyfile b/deployment/k8s/base/Caddyfile index e10740b..9bbb862 100644 --- a/deployment/k8s/base/Caddyfile +++ b/deployment/k8s/base/Caddyfile @@ -2,7 +2,22 @@ # Compose mounts this path; k8s generates a ConfigMap from it. # Path-based gateway — single origin for Frontend, Administration FE, and Backend +(base_security) { + header { + # defer so these win over headers copied from upstream (e.g. Backend) + defer + Strict-Transport-Security "max-age=31536000; includeSubDomains" + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + Referrer-Policy "strict-origin-when-cross-origin" + Cross-Origin-Opener-Policy "same-origin" + Permissions-Policy "geolocation=(), camera=(), microphone=()" + } +} + :{$GATEWAY_PORT:8080} { + import base_security + # Keep /admin on the Administration FE (never the public site) redir /admin /admin/ 308 @@ -14,16 +29,19 @@ # Administration FE — astro preview serves built assets at /_astro, not /admin/_astro handle /admin/_astro/* { uri strip_prefix /admin + header Content-Security-Policy "default-src 'self'; base-uri 'none'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob:; media-src 'self' blob:; connect-src 'self'; worker-src 'self' blob:" reverse_proxy administration-fe:4173 } # Administration FE handle /admin* { + header Content-Security-Policy "default-src 'self'; base-uri 'none'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob:; media-src 'self' blob:; connect-src 'self'; worker-src 'self' blob:" reverse_proxy administration-fe:4173 } # Public Frontend (static) handle { + header Content-Security-Policy "default-src 'self'; base-uri 'none'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'" reverse_proxy frontend:4321 } } From 6f841362b7cf73776c1d717b8e367e4739e0f648 Mon Sep 17 00:00:00 2001 From: Liju Kuriakose Date: Tue, 1 Sep 2026 21:37:18 +0530 Subject: [PATCH 12/12] chore(deploy): bump dev images to 0b278535ef365379639be4f76ad8f4ac6fe84425 --- deployment/k8s/overlays/dev/kustomization.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deployment/k8s/overlays/dev/kustomization.yaml b/deployment/k8s/overlays/dev/kustomization.yaml index 88a565e..214db80 100644 --- a/deployment/k8s/overlays/dev/kustomization.yaml +++ b/deployment/k8s/overlays/dev/kustomization.yaml @@ -11,13 +11,13 @@ resources: images: - name: registry.k3s.flycatchtech.in/flycatch-website/administration-fe newName: registry.k3s.flycatchtech.in/flycatch-website/administration-fe - newTag: fff63c1a53d3b47393d335786b1a2f0e1a45d283 + newTag: 0b278535ef365379639be4f76ad8f4ac6fe84425 - name: registry.k3s.flycatchtech.in/flycatch-website/backend newName: registry.k3s.flycatchtech.in/flycatch-website/backend - newTag: fff63c1a53d3b47393d335786b1a2f0e1a45d283 + newTag: 0b278535ef365379639be4f76ad8f4ac6fe84425 - name: registry.k3s.flycatchtech.in/flycatch-website/frontend newName: registry.k3s.flycatchtech.in/flycatch-website/frontend - newTag: fff63c1a53d3b47393d335786b1a2f0e1a45d283 + newTag: 0b278535ef365379639be4f76ad8f4ac6fe84425 replicas: - count: 1