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..1d5a09a 100644 --- a/apps/Administration-FE/Dockerfile +++ b/apps/Administration-FE/Dockerfile @@ -5,14 +5,15 @@ 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 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 ./ 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/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/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/Backend/src/flycatch_api/main.py b/apps/Backend/src/flycatch_api/main.py index 1606e02..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, @@ -74,7 +75,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..6c9b736 100644 --- a/apps/Frontend/Dockerfile +++ b/apps/Frontend/Dockerfile @@ -3,13 +3,15 @@ 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 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 ./ 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/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: { 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", 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/Caddyfile b/deployment/Caddyfile deleted file mode 100644 index 6c525ac..0000000 --- a/deployment/Caddyfile +++ /dev/null @@ -1,27 +0,0 @@ -# Path-based gateway — single origin for Frontend, Administration FE, and Backend - -:{$GATEWAY_PORT:8080} { - # Keep /admin on the Administration FE (never the public site) - redir /admin /admin/ 308 - - # Backend API - handle /api/* { - reverse_proxy backend:8000 - } - - # Administration FE — astro preview serves built assets at /_astro, not /admin/_astro - handle /admin/_astro/* { - uri strip_prefix /admin - reverse_proxy administration-fe:4173 - } - - # Administration FE - handle /admin* { - reverse_proxy administration-fe:4173 - } - - # Public Frontend (static) - handle { - reverse_proxy frontend:4321 - } -} 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..988bac4 --- /dev/null +++ b/deployment/k8s/README.md @@ -0,0 +1,249 @@ +# 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. + +## 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 +`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/k8s/base/Caddyfile b/deployment/k8s/base/Caddyfile new file mode 100644 index 0000000..9bbb862 --- /dev/null +++ b/deployment/k8s/base/Caddyfile @@ -0,0 +1,47 @@ +# 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 + +(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 + + # Backend API + handle /api/* { + reverse_proxy backend:8000 + } + + # 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 + } +} 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..214db80 --- /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/administration-fe + newName: registry.k3s.flycatchtech.in/flycatch-website/administration-fe + newTag: 0b278535ef365379639be4f76ad8f4ac6fe84425 +- name: registry.k3s.flycatchtech.in/flycatch-website/backend + newName: registry.k3s.flycatchtech.in/flycatch-website/backend + newTag: 0b278535ef365379639be4f76ad8f4ac6fe84425 +- name: registry.k3s.flycatchtech.in/flycatch-website/frontend + newName: registry.k3s.flycatchtech.in/flycatch-website/frontend + newTag: 0b278535ef365379639be4f76ad8f4ac6fe84425 + +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`