Why does my Next.js site work locally but 502 on the VPS?
TL;DR: 502 Bad Gateway is a message from your reverse proxy (almost always Nginx on a typical VPS setup), not from Next.js itself, meaning Nginx tried to forward the request to your Next.js process and got no valid response, because the process was not running, was listening on a different port than Nginx expected, crashed on startup due to a missing environment variable, or ran out of memory. Working locally tells you the code is fine; the four causes below are all about the production environment around that same code.
Why "works locally" doesn't rule much out
Local development runs Next.js's dev server directly, on a machine with your own environment variables already set (often in a .env.local file you never commit), with your own installed dependencies, and no reverse proxy in between. Production on a VPS typically runs the built app (next start or a custom server) behind Nginx acting as a reverse proxy, handling TLS termination and routing, with environment variables that need to be explicitly configured on the server rather than inherited from your local .env.local.
This gap, dev server versus production build, no proxy versus a proxy in front, locally-set env vars versus server-configured ones, means "works locally" only confirms your application code is functionally correct. It says nothing about whether the production environment is correctly configured to actually run that code, which is exactly where a 502 originates.
Step 1: is the Next.js process even running?
Before anything else, check directly whether the Next.js process is actually alive on the server. If you are running it under PM2 (pm2 status or pm2 list) or systemd (systemctl status yourapp), check its current state immediately, since a process that has crashed or never successfully started explains a 502 completely on its own: Nginx has nothing to forward the request to and returns 502 as the honest reflection of that.
If the process shows as running, the next check is whether it is actually listening on the port Nginx expects, covered in Cause 1, since a process that is technically "running" but bound to the wrong port produces the same practical symptom as not running at all from Nginx's perspective.
Cause 1: port mismatch between Next.js and Nginx
Next.js's default port is 3000, but production deployments commonly run it on a different port, either deliberately (to avoid conflicts with other services) or accidentally (an environment variable like PORT being set differently than expected). Nginx's reverse proxy configuration (the proxy_pass directive) specifies a fixed port it forwards requests to. If Next.js starts and binds to a port that does not match what Nginx's config specifies, Nginx cannot reach it, and every request results in a 502 even though Next.js itself is running perfectly.
This is especially easy to introduce without noticing when a PORT environment variable is set in one place (a .env file, a systemd unit's Environment= directive, a PM2 ecosystem config) but Nginx's configuration was written assuming the default 3000, or when the two were originally aligned but one was later changed without updating the other.
Confirm directly: check what port Next.js actually bound to (visible in its own startup log output, or via netstat/ss showing what is listening on which port), and compare that exactly against the port in your Nginx site configuration's proxy_pass line.
Cause 2: missing environment variables in production
Next.js applications commonly depend on environment variables for API keys, database connection strings, and build-time configuration (NEXT_PUBLIC_ prefixed variables in particular, which get baked in at build time rather than read at runtime). If these are present in your local .env.local (which is typically gitignored and never deployed) but not configured anywhere on the production server, the application can fail to start entirely, or fail during the build step, producing a 502 because there is genuinely nothing running for Nginx to forward to.
The failure mode here is often silent from the deploy process's own perspective: a build might succeed even with missing runtime environment variables if those variables are only accessed when a specific code path actually runs (a database connection attempted on first request, for example), meaning the crash happens the moment real traffic hits it rather than during deployment itself, which can make this cause confusing to trace back after the fact.
Check the application's own logs (via pm2 logs or journalctl -u yourapp) for the actual startup error, which in the case of a missing environment variable is usually explicit and specific, rather than guessing.
Cause 3: the process crashed or ran out of memory
On a memory-constrained VPS, particularly smaller tiers, a Next.js build or the running server process itself can be killed by the Linux OOM (out-of-memory) killer if it exceeds available memory, which happens more often than expected since Next.js's build process specifically can be memory-intensive for larger applications, sometimes exceeding what the running application itself needs day to day.
This produces a process that appears to have simply stopped, sometimes with no obvious error message in the application's own logs at all, since the OOM killer terminates the process at the OS level rather than the application failing gracefully with its own error output. Checking system logs (dmesg or journalctl -k, filtered for "Out of memory" or "Killed process") confirms this specifically, and it is a distinct cause from Cause 2 above even though both can produce a silently-stopped process.
If this is the cause, either upgrading to a VPS with more RAM, or reducing build-time memory usage (Next.js supports memory limit flags and incremental build strategies for large apps), addresses it directly; restarting the process alone only delays the same failure recurring under similar conditions.
Cause 4: build succeeded but start command is wrong
Less common but worth checking directly: confirming the actual command used to start the application in production matches what the app actually needs. A Next.js app built with next build needs to be started with next start (or a custom server script if one is used) to serve the built output; if a deployment script or process manager configuration is instead running next dev in production (sometimes left over from an initial quick setup that was never updated for production), or pointing at the wrong directory or entry file after a project restructuring, the process either fails to start correctly or serves something Nginx cannot route to as expected.
A diagnostic order
| Step | What to check | Command/method |
|---|---|---|
| 1 | Is the process running at all | pm2 status / systemctl status |
| 2 | What port is it actually listening on | Startup logs, netstat -tlnp |
| 3 | Does that port match Nginx's proxy_pass |
Nginx site config file |
| 4 | Are required environment variables present in production | App startup logs, server env config |
| 5 | Was the process killed for memory | dmesg / journalctl -k for OOM |
| 6 | Is the correct start command being used | Process manager config, deploy script |
Preventing this on future deploys
A short checklist that catches most of these before they become a production 502 rather than after. Keep a documented, explicit list of required production environment variables separate from your local .env.local, and confirm it is fully populated on the server as part of every deployment, not just the first one. Pin the port explicitly in both your app's configuration and Nginx's config, rather than relying on defaults matching by coincidence, and add a comment in each file referencing the other so a future change is less likely to break the pairing silently. Monitor memory usage during builds specifically, not just during runtime, if your VPS is on a smaller tier, since build-time memory spikes are a distinct and common failure point separate from steady-state running memory. Use a process manager (PM2 or systemd) that logs startup failures clearly and restarts automatically, rather than a bare background process that fails silently with no record of why.
FAQs
Does a 502 mean my code has a bug?
Not directly; 502 specifically means the reverse proxy could not get a valid response from your application process, which is usually an infrastructure or configuration issue (process not running, wrong port, missing environment variables) rather than a bug in your application logic, since "works locally" already confirms the code itself functions correctly.
How is this different from a 504 Gateway Timeout?
502 means the proxy got an invalid or no response from the upstream process (often because it is not running or not reachable at all); 504 means the proxy reached the process successfully but the process took too long to respond and the proxy gave up waiting, which points at a performance problem in a running application rather than a process that is down or unreachable entirely.
I restarted the server and it worked temporarily, then 502'd again. What does that mean?
This pattern strongly suggests a crash-on-specific-condition rather than a startup failure, commonly Cause 3 (memory exhaustion building up over time until the OOM killer intervenes) or a runtime error triggered by a specific request pattern rather than something present from the moment the process starts.
Does this happen with other Node frameworks too, or is it Next.js-specific?
The general pattern (reverse proxy unable to reach a backend process) is not Next.js-specific at all; it applies to any Node application, or any application in any language, run behind Nginx or a similar reverse proxy, since the underlying cause is about the process and the proxy configuration, not about Next.js's own code.
How do I check what port Next.js is actually running on?
Check the console output when the process starts, which typically states the port explicitly ("ready on port 3000" or similar), or use a command like ss -tlnp on the server to see which process is bound to which port directly, which is more reliable than assuming based on configuration files alone if there is any doubt the configuration is actually being applied as expected.
Could my SSL/CDN setup be causing this instead of my server?
If a CDN or additional proxy layer sits in front of Nginx as well, a 502 could theoretically originate from a different hop in the chain, though the more common case for a "works locally, 502 in production" pattern is the direct Nginx-to-application connection described here. Checking Nginx's own error log (/var/log/nginx/error.log) will typically show explicitly which upstream connection failed, clarifying which layer actually generated the error.
Is there a way to test this before deploying to catch the issue earlier?
Running the exact production build and start command (next build && next start) locally, with the same environment variables you plan to set on the server rather than your dev .env.local, and ideally through a local reverse proxy configuration mirroring production, catches most of these causes before they reach the live server, since it reproduces the actual production startup path rather than the dev server's more forgiving behavior.
Conclusion
A 502 on a Next.js VPS deployment is a reverse proxy telling you it could not reach your application process, and since your local environment never goes through that same reverse proxy, "works locally" tells you nothing about whether it is configured correctly. The four usual causes, the process not running, a port mismatch with Nginx, missing production environment variables, and memory exhaustion, are all environment and configuration issues rather than code issues.
Start by confirming the process is actually running and listening on the port Nginx expects, since that single check resolves the largest share of cases immediately and directs you toward the right remaining cause if it does not.
HostCloud's VPS plans include Node.js hosting guidance and enough baseline RAM headroom to avoid the most common build-time OOM failures on small deployments. See VPS plans at https://hostcloud.in.
