# 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= # 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= ``` `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 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 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] # Fingerprinted assets are immutable; index.html must never be cached or a # deploy cannot reach browsers that already have it. Header set Cache-Control "public, max-age=2592000, immutable" Header set Cache-Control "no-cache, no-store, must-revalidate" ``` --- ## 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 `` element): ```xml ``` 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 `war`, `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.