build: package an executable war, and document runtime UI config

This commit is contained in:
LincPay
2026-08-15 17:01:48 +05:30
parent 351da2f67d
commit bfbb933fe1
3 changed files with 121 additions and 18 deletions
+68 -16
View File
@@ -4,11 +4,50 @@ Two artifacts, one host.
| Artifact | Built from | Output | | Artifact | Built from | Output |
|---|---|---| |---|---|---|
| API | `UdyamLinc-Backend` | `target/udyamlinc-backend.jar` (~80 MB, executable) | | API | `UdyamLinc-Backend` | `target/udyamlinc-backend.war` (~80 MB) |
| UI | `UdyamLinc-UI` | `dist/` (static files, ~880 kB) | | UI | `UdyamLinc-UI` | `dist/` (static files, ~880 kB) |
The UI bundle is built with `VITE_UDYAM_API_BASE_URL=/api/v1/udyam` — a **relative** **The war is also executable.** `spring-boot-maven-plugin` repackages it with the
path. It therefore contains no hostname, serves correctly from any domain, and is provided-scope Tomcat under `WEB-INF/lib-provided`, so one artifact both runs on its own
and deploys into a container:
```bash
java -jar udyamlinc-backend.war # embeds Tomcat 11 — recommended
# or drop it into a servlet container — see §2c
```
### Pointing the UI at the API without rebuilding
The bundle reads its API base from **`config.js`**, served unbundled from the static
root. Deploy the backend first, then edit one line on the server — no rebuild, no
redeploy of the UI:
```js
// config.js, at the web root next to index.html
window.__UDYAM_CONFIG__ = {
apiBaseUrl: '', // empty = same-origin /api/v1/udyam (the default)
}
```
Set it only when the API is on another host:
```js
apiBaseUrl: 'https://udyamlinc-api.lincpay.tech/api/v1/udyam'
```
Resolution order is `config.js` → the build-time `VITE_UDYAM_API_BASE_URL` →
`/api/v1/udyam`. An empty string means "unset", so the shipped file needs no edit for a
same-origin deployment.
Two things that bite:
- **`config.js` must never be cached.** The nginx and httpd configs below send
`no-store` for it. Cached, it pins the old URL for everyone who has already loaded
the app, and nothing on the server side shows it.
- **A cross-origin value needs `UDYAM_CORS_ALLOWED_ORIGINS`** set to the UI's origin, or
the browser blocks every call while the API itself looks perfectly healthy.
The UI bundle is otherwise built with a **relative** default. 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 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 CORS never applies, and CORS is the single largest source of "worked locally, broken on
the server" in a split deployment. the server" in a split deployment.
@@ -153,7 +192,7 @@ Virtual host:
<LocationMatch "^/assets/"> <LocationMatch "^/assets/">
Header set Cache-Control "public, max-age=2592000, immutable" Header set Cache-Control "public, max-age=2592000, immutable"
</LocationMatch> </LocationMatch>
<LocationMatch "^/index\.html$"> <LocationMatch "^/(index\.html|config\.js)$">
Header set Cache-Control "no-cache, no-store, must-revalidate" Header set Cache-Control "no-cache, no-store, must-revalidate"
</LocationMatch> </LocationMatch>
</VirtualHost> </VirtualHost>
@@ -187,21 +226,34 @@ RewriteRule ^(.*)$ /index.html
The API still runs as its own `java -jar` process on 8082, and Tomcat proxies to it — 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. or you put httpd/nginx in front of both.
### Deploying the API *as a WAR* into Tomcat — do not do this today ### Deploying the API as a WAR into Tomcat
It is possible but it is not a same-day change, and the constraint is hard rather than Supported — the build produces a war and `UdyamLincApplication` extends
stylistic: `SpringBootServletInitializer`. Two conditions, and both are hard:
- This project is **Spring Boot 4.0.1**, which is built on Jakarta EE 11 / Servlet 6.1. **1. Tomcat 11 or newer.** Spring Boot 4.0.1 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; Tomcat 10 is
Tomcat 9 is `javax.*` and cannot load a `jakarta.*` application at all. short of the servlet level. Check before deploying:
- 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 ```bash
none of that risk. **Recommendation: keep the jar.** $CATALINA_HOME/bin/version.sh # or version.bat
```
**2. Deploy it as `api#v1#udyam.war`.** Under a container `server.servlet.context-path`
is ignored — the file name decides the path, and Tomcat reads `#` as a separator. So:
```bash
cp udyamlinc-backend.war $CATALINA_HOME/webapps/api#v1#udyam.war
```
serves `/api/v1/udyam`, which is what the UI's relative API base expects. Deployed under
any other name every endpoint moves and the UI gets 404s that look like a broken build.
Environment variables are read the same way; set them in `setenv.sh` or the service
definition rather than per-shell.
**Still, running the war directly is the lower-risk option** — it embeds Tomcat 11, so
neither condition can bite, and the same file works both ways.
--- ---
+23 -1
View File
@@ -14,7 +14,18 @@
<groupId>com.lincfusion</groupId> <groupId>com.lincfusion</groupId>
<artifactId>udyamlinc-backend</artifactId> <artifactId>udyamlinc-backend</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging> <!--
war, but still executable. spring-boot-maven-plugin repackages a war with
the provided-scope container under WEB-INF/lib-provided, so the one
artifact both runs with `java -jar` and deploys into a servlet container.
Nothing is given up by choosing war here.
If deploying into Tomcat rather than running it directly, that container
must be Tomcat 11+: Spring Boot 4 is Jakarta EE 11 / Servlet 6.1, and
Tomcat 9 (javax.*) cannot load it. Deploy the file as api#v1#udyam.war so
the container path matches the context path this app expects.
-->
<packaging>war</packaging>
<name>udyamlinc-backend</name> <name>udyamlinc-backend</name>
<description>UdyamLinc — predictive cash-flow intelligence and early warning for rural microenterprises (NABARD Hackathon)</description> <description>UdyamLinc — predictive cash-flow intelligence and early warning for rural microenterprises (NABARD Hackathon)</description>
@@ -31,6 +42,17 @@
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<!--
provided, so a servlet container supplies its own Tomcat instead of
the app carrying a second one into it. The repackaged war still holds
these under WEB-INF/lib-provided, so `java -jar` keeps working.
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<!-- Security --> <!-- Security -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
@@ -2,6 +2,8 @@ package com.lincfusion.udyamlinc;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
/** /**
* UdyamLinc — predictive cash-flow intelligence and early warning for rural * UdyamLinc — predictive cash-flow intelligence and early warning for rural
@@ -13,9 +15,36 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
* ingestion pipeline are separate components and are not implemented here. * ingestion pipeline are separate components and are not implemented here.
*/ */
@SpringBootApplication @SpringBootApplication
public class UdyamLincApplication { public class UdyamLincApplication extends SpringBootServletInitializer {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(UdyamLincApplication.class, args); SpringApplication.run(UdyamLincApplication.class, args);
} }
/**
* Entry point when a servlet container starts this instead of {@code main}.
*
* <p>The artifact is packaged as a war so it can be deployed to Tomcat, but
* {@code spring-boot-maven-plugin} still repackages it as executable — the
* provided-scope container lands in {@code WEB-INF/lib-provided}, which the
* launcher puts on the classpath. So {@code java -jar udyamlinc-backend.war}
* and dropping the same file into {@code webapps/} are both supported, from
* one build.
*
* <p><b>Tomcat 11 or newer.</b> Spring Boot 4 is built on Jakarta EE 11 and
* Servlet 6.1. Tomcat 9 is {@code javax.*} and cannot load this at all;
* Tomcat 10 is short of the servlet level. Running the war directly embeds
* Tomcat 11 and sidesteps the question entirely, which is why that remains
* the recommendation.
*
* <p><b>The context path moves.</b> {@code server.servlet.context-path} is
* ignored under a container — the deployed file name decides it. Tomcat reads
* {@code #} as a path separator, so the war must be deployed as
* {@code api#v1#udyam.war} to serve {@code /api/v1/udyam}. Deployed under any
* other name every endpoint moves and the UI's relative API base misses.
*/
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(UdyamLincApplication.class);
}
} }