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