Application Development

Hardcoded Localhost in Production: How to Find and Fix It

J
James Eriksson
··9 min read
Fix hardcoded localhost URLs breaking production apps. Detect with grep, fix with env vars, prevent with CI/CD checks. Why managed hosting is the safest option.
TL;DR
  • AI code generators default to localhost URLs which slip into production builds and cause silent API failures.
  • Search your codebase and compiled output for "localhost" before deploying to catch this before users do.
  • Replace hardcoded URLs with environment variables using your framework's standard mechanism (VITE_, NEXT_PUBLIC_, VUE_APP_).
  • Prevent future occurrences with CI/CD checks, pre-commit hooks, and staging environment tests.
  • Managed hosting like Ship eliminates this class of error by injecting variables automatically.

Your app runs perfectly on localhost but fails immediately after deploying to production with cryptic 404s or connection errors. The culprit is almost always a hardcoded URL in your codebase: usually injected by an AI code generator like Lovable or V0 and missed during testing because tests run against the same hardcoded URL you built with.

The Hardcoded Localhost Problem in AI-Built Apps

When AI code generators write JavaScript or Python code, they default to localhost:3000, localhost:5173, or 127.0.0.1:8000 for API calls. This works perfectly during development. The app compiles, tests pass, and everything runs. When you push to production, the same hardcoded URL persists into the built artifact. Your frontend still tries to call localhost:3000 from your user's browser, which points to their machine, not your server.

The reason tests don't catch this is mechanical: your development environment uses the same localhost address as the AI generator did. Your test runner, your local API server, and your app's compiled code all share the assumption. No cross-browser test, unit test, or integration test will fail because everything is consistently pointing to the same machine. The error only surfaces when a real user (or your production environment) tries to access the app.

This is especially common in LLM-generated code because code generators optimize for working locally first. They are trained on thousands of tutorial code snippets where localhost is the default. The generator has no context about your deployment target, no knowledge of your production domain, and no enforcement rule that prevents hardcoding URL schemes into applications. So every API call, WebSocket connection, or asset fetch can become a ticking time bomb. Pranay Joshi, VP of Product and Engineering at Vibe Coder, has written extensively about this failure mode in AI-generated applications, noting that this is one of the top production gotchas for teams using Lovable or similar tools.

The symptoms are predictable and silent. API calls return 404 or connection timeouts. Forms appear to hang. Real-time features stop working. But your server logs show nothing. The requests never arrive. Your production database is untouched. The app itself loaded fine in the browser. The only clue is the network tab in browser DevTools showing requests to localhost or similar addresses that resolve to the user's own machine and fail immediately.

How to Spot It Before Deploying

Before you ship, run a text search across your entire codebase. Search for "localhost", "127.0.0.1", and protocol schemes. Do not assume these are obvious. LLM generators sometimes embed localhost inside environment variable names, config objects, or even in comments that accidentally affect code behavior.

Open browser DevTools on your local version of the app. Go to the Network tab and trigger every action that makes an API call or loads external content. Look at the request URLs. Are they absolute paths (good) or hardcoded domains (bad)? If you see localhost addresses in production build output, you have a problem. This is your first line of defense before the app ships.

For static analysis, use tools like Heimdall Scan or grep-based linting. Heimdall Scan specifically checks for hardcoded development URLs and flags them before deployment. If you run CI/CD, add a pre-deployment step that searches for localhost and fails the build if found. This costs nothing and catches the error before it ships. Following a structured pre-launch validation checklist ensures you never miss this step again, especially when teams grow beyond one developer.

Another simple check: build your app locally (npm run build, yarn build, or your framework's equivalent) and inspect the compiled output. Bundled JavaScript is minified but still searchable. Open dist/bundle.js or similar and grep for "localhost". If it is there, your app will fail in production.

Fix It Now: Framework-Specific Solutions

Once you find the hardcoded URL, the fix is the same across all frameworks: replace the hardcoded string with an environment variable, and inject that variable at build time (for static apps) or runtime (for server-rendered apps).

React and Vite

In React with Vite, create a.env.local file for development and a.env.production file for production. Set VITE_API_URL in each with the appropriate server address.

VITE_API_URL=[your_dev_server_url]

In your API calls, reference the injected variable:

const apiUrl = import.meta.env.VITE_API_URL;
const response = await fetch(`${apiUrl}/api/users`);

Vite automatically loads the correct file based on your build command. When you run npm run build, it injects the production value. The.env files are never compiled into your final bundle.

Next.js

Next.js uses NEXT_PUBLIC_ prefix for variables exposed to the browser. Create.env files for development and production with your server URLs:

NEXT_PUBLIC_API_URL=[your_dev_server_url]

In your code, Next.js injects the appropriate value:

const apiUrl = process.env.NEXT_PUBLIC_API_URL;

The NEXT_PUBLIC_ prefix tells Next.js to bake this into the client-side bundle.

Vue

Vue uses VUE_APP_ prefix (Vue 2) or VITE_ (Vue 3 with Vite). Create.env.local and.env.production files with your server URLs:

VUE_APP_API_URL=[your_dev_server_url]

Access it in your app:

const apiUrl = process.env.VUE_APP_API_URL;

Like React, Vue respects.env files at build time.

Angular

Angular environments live in src/environments/. Create separate files for development and production:

export const environment = {
  apiUrl: 'your_dev_server_url'
};

Inject it in your service:

import { environment } from '../environments/environment';

@Injectable()
export class ApiService {
  private apiUrl = environment.apiUrl;
}

Svelte

Svelte with Vite uses.env files. Create.env.local and.env.production with your server URLs:

VITE_API_URL=[your_dev_server_url]

In your component:

const apiUrl = import.meta.env.VITE_API_URL;

The pattern is identical across all frameworks: externalize the URL via environment variables and let the build tool inject the correct value. Do not hardcode any URL string. Even if it works now, the next developer or the next deployment will trip over it.

Prevent It From Happening Again

Once you fix this for the first time, automate the detection. Add a GitHub Actions step to your CI/CD that runs grep on the compiled output and fails if "localhost" is found:

- name: Check for hardcoded localhost in build
  run: |
    if grep -r "localhost" dist/ ; then
      echo "Hardcoded localhost found in production build!"
      exit 1
    fi

Set up pre-commit hooks using Husky or similar to catch localhost strings before they are committed:

#!/bin/bash
if grep -r "localhost" --include="*.js" --include="*.ts" --include="*.jsx" --include="*.tsx" src/ ; then
  echo "Hardcoded localhost found in source code"
  exit 1
fi

Configure your linter (ESLint) with a custom rule or third-party plugin that warns on hardcoded API URLs. This gives developers immediate feedback while editing.

Beyond detection, standardize your environment variable naming across your team. Establish a rule: "All API URLs come from environment variables with the VITE_API_URL or NEXT_PUBLIC_API_URL pattern." Make this part of your code review checklist. When an AI generator produces a new feature, reviewers should spot hardcoded URLs as automatically as they spot SQL injection. For teams deploying AI-generated apps, following a structured deployment workflow for AI-built applications helps catch these issues systematically.

Test your app in a staging environment before deploying to production. Use the production domain in your staging build. If something fails in staging, you catch it before real users see it. This is the most reliable safeguard beyond automation.

Why Managed Hosting Prevents This

If you deploy using a DIY approach (Hetzner VPS with Coolify, for example), you manage every build step yourself. You set environment variables manually, you build the app, you upload it. This is where hardcoded localhost survives into production.

Managed platforms like Ship or Northflank automate this. When you deploy an app to Opsily's Ship, you specify your production API URL once in the platform UI, not in config files. The platform injects it during the build process, before your code is even compiled. There is no chance for a hardcoded localhost to survive. Ship includes built-in staging environments that let you test your app with production configuration before sending traffic to real users.

This eliminates the entire class of "works locally, fails in production" surprises. Ship's deployment workflow handles environment-specific configuration automatically. Your code never needs to know about production details; Ship bridges that gap.

These platforms also provide transparent logging of every build step and environment variable. If something is hardcoded, you see it in the build logs. You cannot hide a mistake. The tradeoff is cost and control. Managed platforms charge for convenience; you pay monthly for hosting instead of a one-time server cost. But for teams without a dedicated DevOps engineer, this is the most reliable way to ensure environment-specific configurations never make it into production.

Frequently Asked Questions

Should I use 127.0.0.1 or localhost?

In development, both point to your local machine. Use localhost. It is more readable and requires no hardcoding of IP addresses. In production, neither belongs in your code. Always use an environment variable.

What is localhost?

Localhost is an alias for 127.0.0.1, your local machine. It is not accessible from outside. If you need external access during development, use an IP address or a tool like ngrok.

How to set 127.0.0.1 to localhost?

Most systems already alias 127.0.0.1 to localhost via /etc/hosts on Linux and macOS or Windows System32 drivers etc hosts. You do not need to set it manually.

Is localhost HTTPS or http?

By default, localhost uses the unencrypted transport protocol. If your production uses a secure connection, your environment variable should point to the secure protocol URL. Some dev tools like Vite can generate self-signed certificates for local testing.

How do I find my localhost address?

Check where your dev server is running. It usually logs to console on startup. You can also visit localhost in your browser and append the port. Your dev server output always tells you the address.

How to write a local server address?

Write it as localhost:PORT where PORT is 3000, 5173, 8000, etc. Do not use spaces. Using no space is correct; including a space is not.

The Bottom Line

Hardcoded localhost addresses are silent killers in production, especially in AI-generated code. The fix is always an environment variable; prevention is CI/CD checks and staging environments. Before you deploy, search your codebase for "localhost". Use your framework's standard env mechanism to externalize URLs.

For maximum safety with minimal overhead, managed hosting like Opsily's Ship injects environment variables automatically and provides staging environments to test before production.

Host AI-built apps safely
Ship handles environment configuration automatically, staging tests before production, and zero hardcoded secrets.
Get Started Free

Ready to self-host your own apps?

One server. Multiple apps. No per-app fees.

Get started →