From 88702f2b4312cdf7992ecc724675e5d13523b0bd Mon Sep 17 00:00:00 2001 From: LincPay Date: Sat, 15 Aug 2026 15:33:19 +0530 Subject: [PATCH] docs: deployment notes for nginx, Apache httpd and Tomcat --- DEPLOYMENT.md | 232 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 DEPLOYMENT.md diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..2d787cf --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,232 @@ +# Deploying UdyamLinc + +Two artifacts, one host. + +| Artifact | Built from | Output | +|---|---|---| +| API | `UdyamLinc-Backend` | `target/udyamlinc-backend.jar` (~80 MB, executable) | +| UI | `UdyamLinc-UI` | `dist/` (static files, ~880 kB) | + +The UI bundle is built with `VITE_UDYAM_API_BASE_URL=/api/v1/udyam` — a **relative** +path. It therefore contains no hostname, serves correctly from any domain, and is +same-origin with the API. That last point is the one worth keeping: same-origin means +CORS never applies, and CORS is the single largest source of "worked locally, broken on +the server" in a split deployment. + +The only requirement on the web server is that it does two things: + +1. serve `dist/` with an SPA fallback — unknown paths return `index.html`, because + React Router owns `/officer/**`, `/nabard/**` and `/app/**`, not the file system; +2. pass `/api/**` through to the Spring Boot process on port 8082. + +Any of nginx, Apache httpd or Tomcat can do both. Pick the section that matches your box. + +--- + +## 1. The API (identical in all three cases) + +The jar embeds Tomcat 11. It does not need an application server installed, and it does +not go in `webapps/`. + +```bash +java -jar udyamlinc-backend.jar +``` + +Configuration is entirely by environment variable — nothing needs editing inside the jar: + +```bash +export UDYAM_DB_URL=jdbc:postgresql://localhost:5432/UdyamLinc_DEMO +export UDYAM_DB_USERNAME=postgres +export UDYAM_DB_PASSWORD= + +# Set this. The default is committed to the repository, so it is public, and a +# signing key anyone can read signs anyone's token. +export UDYAM_JWT_SECRET= +``` + +`spring.jpa.hibernate.ddl-auto=update` builds the schema on first boot, so the database +only has to exist and be empty: + +```sql +CREATE DATABASE "UdyamLinc_DEMO"; +``` + +### As a systemd service + +```ini +[Unit] +Description=UdyamLinc API +After=network.target postgresql.service + +[Service] +User=udyamlinc +WorkingDirectory=/opt/udyamlinc +Environment=UDYAM_DB_URL=jdbc:postgresql://localhost:5432/UdyamLinc_DEMO +Environment=UDYAM_DB_USERNAME=postgres +Environment=UDYAM_DB_PASSWORD=change-me +Environment=UDYAM_JWT_SECRET=change-me +ExecStart=/usr/bin/java -jar /opt/udyamlinc/udyamlinc-backend.jar +SuccessExitStatus=143 +Restart=on-failure + +[Install] +WantedBy=multi-user.target +``` + +### First boot takes a while, and the failure mode is silent + +`UdyamDataSeeder` populates 263 enterprises across 8 partner banks and 16 branches, and +it runs as **one transaction**. Nothing is visible in the database until it commits. + +This has bitten us once already: the machine slept mid-seed, the transaction stayed open +overnight, and every figure read from the console during that window was the pre-seed +state — reported confidently, with no warning anywhere. **Watch for the seeder's final +log line before trusting any number on screen**, and check its timestamp rather than +just its text. + +Verify the API is up: + +```bash +curl -s localhost:8082/api/v1/udyam/auth/regional-offices +``` + +That endpoint is deliberately public — the login page reads it before anyone has signed +in — so it is a clean liveness check. + +--- + +## 2a. Front door: nginx + +`UdyamLinc-UI/nginx.conf` is ready to use as-is. Copy `dist/` to the document root and +the conf to `/etc/nginx/conf.d/`. It already carries the `/api/` proxy block, the SPA +fallback, gzip, and asset caching. + +Adjust `listen 5173` to `80`/`443` for a real deployment. + +--- + +## 2b. Front door: Apache httpd + +Enable the modules first — on a stock install `proxy_http` and `rewrite` are usually off: + +```bash +a2enmod proxy proxy_http rewrite headers +``` + +Virtual host: + +```apache + + ServerName udyamlinc.lincpay.tech + DocumentRoot /var/www/udyamlinc + + # ── API ───────────────────────────────────────────── + # The backend's context-path is already /api/v1/udyam, so the prefix passes + # through unchanged rather than being rewritten. + ProxyPreserveHost On + ProxyPass /api/ http://127.0.0.1:8082/api/ + ProxyPassReverse /api/ http://127.0.0.1:8082/api/ + + # Ingestion and the three-year backfill are long-running admin calls. The + # default proxy timeout cuts them off mid-run and reports a gateway error + # over a job that is still working. + ProxyTimeout 300 + + + Require all granted + Options -Indexes + + # ── SPA fallback ──────────────────────────────── + # Anything that is not a real file, not a real directory, and not the + # API is handed to index.html for React Router to resolve. Without the + # !^/api/ guard this rule swallows API calls and returns HTML to axios, + # which surfaces as a JSON parse error rather than a routing problem. + RewriteEngine On + RewriteCond %{REQUEST_URI} !^/api/ + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule . /index.html [L] + + + # Fingerprinted assets are immutable; index.html must never be cached or a + # deploy cannot reach browsers that already have it. + + Header set Cache-Control "public, max-age=2592000, immutable" + + + Header set Cache-Control "no-cache, no-store, must-revalidate" + + +``` + +--- + +## 2c. Front door: Tomcat + +Two things are worth separating here, because "Tomcat" can mean either. + +### Serving the UI from Tomcat — fine + +Copy `dist/` into `$CATALINA_HOME/webapps/ROOT/` and add the SPA fallback via the +rewrite valve. In `conf/Catalina/localhost/ROOT.xml` (or the `` element): + +```xml + + + +``` + +with `webapps/ROOT/WEB-INF/rewrite.config`: + +``` +RewriteCond %{REQUEST_URI} !^/api/ +RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} !-f +RewriteRule ^(.*)$ /index.html +``` + +The API still runs as its own `java -jar` process on 8082, and Tomcat proxies to it — +or you put httpd/nginx in front of both. + +### Deploying the API *as a WAR* into Tomcat — do not do this today + +It is possible but it is not a same-day change, and the constraint is hard rather than +stylistic: + +- This project is **Spring Boot 4.0.1**, which is built on Jakarta EE 11 / Servlet 6.1. + That requires **Tomcat 11**. On Tomcat 9 or 10 the application does not start — + Tomcat 9 is `javax.*` and cannot load a `jakarta.*` application at all. +- It needs `war`, `spring-boot-starter-tomcat` moved to + `provided`, and `UdyamLincApplication` extending `SpringBootServletInitializer`. +- The context path moves from the configured `/api/v1/udyam` to the WAR's deployed + name, so the proxy rules above stop matching. + +The jar already embeds Tomcat 11, so running it directly gives the same container with +none of that risk. **Recommendation: keep the jar.** + +--- + +## Checklist before showing anyone + +- [ ] `curl localhost:8082/api/v1/udyam/auth/regional-offices` returns JSON +- [ ] The seeder's final log line has appeared, with a plausible timestamp +- [ ] The site loads, and a hard refresh on a deep link such as `/nabard/geography` + returns the app rather than a 404 — this is the SPA fallback, and it is the + single most common thing to get wrong +- [ ] The browser network tab shows `/api/v1/udyam/...` calls returning 200, not HTML +- [ ] `UDYAM_JWT_SECRET` is not the committed default + +## What is deliberately off + +`udyam.ingestion.enabled` is `false`, so a demo boot never calls a government endpoint, +and `DATAGOV_API_KEY` is unset. The portfolio is seeded, which is enough for every +screen. To refresh from live sources, set the key and run, in order: + +``` +POST /admin/ingestion/run +POST /admin/features/compute +POST /admin/scoring/run +``` + +Space bulk ingestion runs a couple of minutes apart — data.gov.in meters by quota per +window, so a run started too soon after another returns 429 on every pair at once and +records the whole ingestion as failed with zero rows.