docs: deployment notes for nginx, Apache httpd and Tomcat
This commit is contained in:
+232
@@ -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.
|
||||||
Reference in New Issue
Block a user