DEV Community

linou518
linou518

Posted on

How a Hardcoded IP Silently Killed External Access — A Vite + Nginx Cautionary Tale

How a Hardcoded IP Silently Killed External Access — A Vite + Nginx Cautionary Tale

A hardcoded private IP address buried in frontend code completely broke external access. Here's the full investigation, fix, and deployment journey.

Background

We needed to make our internal business system (Vite + React frontend + Express backend) accessible from external networks via erp.example.com. Everything worked perfectly within the LAN, but external access simply wouldn't function.

The Configuration Problem We Discovered

Investigation revealed this setup:

  • erp.example.com nginx configuration:
    • /api/127.0.0.1:3210 (backend) ✅
    • /127.0.0.1:3101 (frontend) ❌ — Nothing was running on this port
  • The frontend was actually served by a vite dev server (port 3101) on a different machine — accessible only within LAN

Essentially, the development vite dev server was being used as the "production" service.

The Real Killer: Hardcoded IPs

The bigger problem was private IP addresses hardcoded throughout the frontend source code:

// erpApi.ts
const BASE_URL = 'http://192.168.x.x:8520/api';

// Various page components
fetch('http://192.168.x.x:8520/api/sales/report/ledger', ...)
fetch('http://192.168.x.x:3210/api/v1/arrivals/slip?ids=1', ...)
Enter fullscreen mode Exit fullscreen mode

Browsers within the LAN could reach 192.168.x.x directly, so it worked. External clients obviously couldn't. Even if we built with vite and served via nginx, these IPs would be baked into the compiled JavaScript.

Result: External access led to all API calls failing with ERR_CONNECTION_TIMED_OUT.

The Fix

1. Replace Hardcoded IPs with Relative Paths

// Before
const BASE_URL = 'http://192.168.x.x:8520/api';

// After
const BASE_URL = '/api';
Enter fullscreen mode Exit fullscreen mode

Target files: erpApi.ts, erpCompat.ts, various list pages (SaleList, KaikakeList, NyukinList, etc.). Used grep to find all instances of 192.168.x and eliminated them.

2. Create .env.production

VITE_API_URL=/api/v1
VITE_ERP_URL=/api
Enter fullscreen mode Exit fullscreen mode

Set up Vite environment variables to switch API base URLs between development and production.

3. Update Nginx Configuration

# Before: proxy to vite dev server (not running)
location / {
    proxy_pass http://127.0.0.1:3101;
}

# After: serve built static files directly
location / {
    root /www/apps/erp-admin;
    try_files $uri $uri/ /index.html;
}

location /api/ {
    proxy_pass http://127.0.0.1:3210;
}
Enter fullscreen mode Exit fullscreen mode

Classic SPA configuration with try_files fallback to index.html for all routes.

4. Build & Deploy

cd /mnt/shared/projects/platform/erp-admin
npm run build    # generates hashed files in dist/
scp -r dist/* admin@webserver:/www/apps/erp-admin/
ssh admin@webserver "nginx -t && nginx -s reload"
Enter fullscreen mode Exit fullscreen mode

The Vite Dev Proxy Trap

A related gotcha: server.proxy settings in vite.config.ts only apply during development (npm run dev) and have zero impact on npm run build output.

// vite.config.ts — this is dev server config, not build config
server: {
  proxy: {
    '/api/v1': 'http://192.168.x.x:8520',
  }
}
Enter fullscreen mode Exit fullscreen mode

This creates the "works in dev but breaks in build" pattern. Production proxying must be configured on the web server side (nginx).

Lessons Learned

  1. Never hardcode private IPs in frontend code. Use relative paths (/api) or environment variables (import.meta.env.VITE_API_URL).
  2. Don't use vite dev server for production serving. The correct approach is npm run build → nginx static file serving.
  3. server.proxy is dev-only. Production reverse proxying must be configured in the web server.
  4. "Working in LAN" isn't real validation. Without testing external access, hardcoded IPs will go unnoticed.

These seem obvious in hindsight, but "it works for now" shortcuts accumulate quickly. When you finally try to go public, everything breaks. Start by grepping for 192.168.x in your codebase.


Tags: #vite #nginx #frontend #deployment #spa #erp #hardcoded-ip #devops

Top comments (0)