docs: deployment notes for nginx, Apache httpd and Tomcat

This commit is contained in:
LincPay
2026-08-15 15:33:19 +05:30
parent 053a1cee05
commit 88702f2b43
+232
View File
@@ -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=<the real one>
# 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=<a long random string>
```
`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
<VirtualHost *:80>
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
<Directory /var/www/udyamlinc>
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]
</Directory>
# Fingerprinted assets are immutable; index.html must never be cached or a
# deploy cannot reach browsers that already have it.
<LocationMatch "^/assets/">
Header set Cache-Control "public, max-age=2592000, immutable"
</LocationMatch>
<LocationMatch "^/index\.html$">
Header set Cache-Control "no-cache, no-store, must-revalidate"
</LocationMatch>
</VirtualHost>
```
---
## 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 `<Context>` element):
```xml
<Context>
<Valve className="org.apache.catalina.valves.rewrite.RewriteValve" />
</Context>
```
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 `<packaging>war</packaging>`, `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.