8.0 KiB
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:
- serve
dist/with an SPA fallback — unknown paths returnindex.html, because React Router owns/officer/**,/nabard/**and/app/**, not the file system; - 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/.
java -jar udyamlinc-backend.jar
Configuration is entirely by environment variable — nothing needs editing inside the jar:
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:
CREATE DATABASE "UdyamLinc_DEMO";
As a systemd service
[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:
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:
a2enmod proxy proxy_http rewrite headers
Virtual host:
<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):
<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 ajakarta.*application at all. - It needs
<packaging>war</packaging>,spring-boot-starter-tomcatmoved toprovided, andUdyamLincApplicationextendingSpringBootServletInitializer. - The context path moves from the configured
/api/v1/udyamto 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-officesreturns 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/geographyreturns 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_SECRETis 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.