Works locally but not when deployed: common causes

Your server is a different machine from your laptop, and the app depends on something that exists only on the laptop. The usual culprits are environment variables, the .env file, runtime versions, file name case, the port and missing dependencies.

Updated 8 min read

Learning objectives

After reading this article you will be able to:

  • Rank the most likely causes of a deploy-only failure
  • Compare your local and production environments line by line
  • Make the app fail loudly and early instead of silently
On this page

You ran the app a hundred times on your machine. You deployed it, and now it crashes on startup, returns errors, or starts and does nothing. The code did not change between those two moments, so the code is rarely the problem.

What changed is everything around the code: the operating system, the runtime version, the files present, the environment variables, the network and the memory available. Your laptop has years of accumulated state that you stopped noticing. A server starts empty and has only what you gave it.

deploy log: read the first errorTypeError: Cannot read properties of undefined1Environment variablesset themon the server2Versionspin versions, installfrom the lockfile3Files and caseexact name andrelative path4Port and hostPORT from the env,host 0.0.0.05Resourcesmore memory or asmaller startup peak
Read the first error in the log, then check the environment in this order, from the most common cause to the least.

Where do you start?#

Read the log from the start of the run, not the end. The first error is the cause, and later ones are usually fallout. If you need help reading it, see how to read app logs.

Then work through the checks below, in order. They go from the most common cause to the least, and each one takes a few minutes.

Are environment variables missing?#

This is the number one cause. A twelve-factor style app keeps config such as credentials and URLs in environment variables, separate from the code (The Twelve-Factor App). Locally you probably get them from a .env file. That file is, and should be, listed in .gitignore, so a deploy from Git never carries it.

Typical symptoms:

text
TypeError: Cannot read properties of undefined (reading 'split')
DiscordjsError [TokenInvalid]: An invalid token was provided.
connect ECONNREFUSED 127.0.0.1:5432

The last one is a classic: the database URL is missing, the library falls back to localhost, and nothing listens there on the server.

Check three things:

  1. Does production have every variable? List the names your code reads and compare them with the names set on the server. Names are exact and case sensitive.
  2. Does your code load the file at all? process.env only sees what the process was started with. On recent Node.js versions, you can start with node --env-file=.env index.js (Node.js docs). Plain node index.js reads no file, and Python needs a loader such as python-dotenv.
  3. Are the values strings? Environment variables are always strings. DEBUG=false is the string "false", which is truthy in JavaScript. Node.js also converts anything you assign to process.env to a string, so assigning undefined stores the text "undefined" (Node.js docs).

Fail early so the next miss is obvious:

js
const required = ["DISCORD_TOKEN", "DATABASE_URL"];
const missing = required.filter((name) => !process.env[name]);
if (missing.length > 0) {
  throw new Error(`Missing environment variables: ${missing.join(", ")}`);
}

For the difference between values read at build time and at run time, see build-time vs runtime variables. For the basics, see what are environment variables.

Do the runtime and package versions match?#

Your laptop may run a new Node.js release while the server defaults to an older one, or the reverse. A syntax or API that exists in one version throws SyntaxError or TypeError in another. The same applies to Python: a match statement or a newer standard library function fails on an older interpreter.

  • Print the version in both places: node --version, python --version.
  • Declare the version your app needs. In package.json, the engines field states which Node.js versions the package works with, but it is advisory by default (npm docs), so it will not stop a wrong version by itself. Check the runtime setting of your host too.
  • Commit the lockfile (package-lock.json, poetry.lock, or a pinned requirements.txt from pip freeze, see the pip docs). Without it, the server may resolve newer dependency versions than the ones you tested.

Install from the lockfile on the server instead of trusting a folder you copied:

bash
npm ci

npm ci needs an existing lockfile, deletes any existing node_modules and fails if package.json and the lockfile disagree (npm docs). That failure is useful, because it surfaces a mismatch at install time instead of at runtime.

Is a dependency or file missing?#

Errors like Cannot find module 'express' or No module named 'discord' mean the package was never installed where the app runs. Common reasons: it sits in devDependencies but is needed at runtime, it was installed globally on your laptop, or you ship node_modules built on another operating system. The full walkthrough is in Cannot find module errors.

Files behave the same way. A file can be missing because:

  • It is ignored by .gitignore or left out of the ZIP you uploaded (a config.json, a data folder, a font).
  • It is generated by a build step that you ran locally but never ran on the server. TypeScript dist/ folders are the usual case.
  • You read it with an absolute path such as C:\Users\me\project\data.json. Use paths relative to the file (path.join(__dirname, "data.json")) so the app does not depend on where it lives.

Does a letter-case or path difference break it?#

Windows and macOS file systems are usually case insensitive, so require("./Utils") finds utils.js. Linux treats them as two names and throws MODULE_NOT_FOUND. Servers are overwhelmingly Linux, which makes this the classic "works on my Windows machine" failure.

text
Error: Cannot find module './Utils'

Fix it by making the import match the file name exactly. If Git already tracked the file under the wrong case, rename it with git mv, because a plain rename can be invisible to Git on a case-insensitive disk. A few related traps:

  • Path separators: build paths with path.join, not by concatenating "\\".
  • Shell scripts saved with Windows (CRLF) line endings can fail on Linux with bad interpreter errors.
  • Native modules (sharp, bcrypt, better-sqlite3) include compiled binaries. A binary built on your OS will not run on another. Install on the server so the correct build is fetched.

Are the port and host wrong?#

The app starts, the log looks healthy, but the URL times out or returns a gateway error. Two causes:

  • Hard-coded port. Many hosts tell your app which port to use. Read it from the environment and keep a default for local work: const port = process.env.PORT ?? 3000;. Check your host's documentation for the exact variable.
  • Listening on the wrong address. Binding only to localhost or 127.0.0.1 means that only processes inside the same machine or container can connect. In Node.js, server.listen(port) without a host accepts connections on all interfaces (:: or 0.0.0.0) (Node.js docs); if you pass "localhost" explicitly, switch to "0.0.0.0". Python servers such as Flask's development server (Flask docs) and uvicorn (uvicorn docs) bind to 127.0.0.1 by default, so pass --host 0.0.0.0.

Also check URLs inside your own code. A frontend calling http://localhost:4000/api works for you and for nobody else. Make the base URL a variable.

Is it running out of resources?#

Your laptop may have 16 GB of RAM and a fast CPU. A small server plan has a hard memory ceiling. If the process dies with no stack trace, or the log ends with Killed or JavaScript heap out of memory, you are likely over the limit. Memory spikes at startup (loading a large dataset, building caches) are a common reason. See out-of-memory errors for how to confirm and fix it.

Network access differs too. A database that accepts connections only from your own IP, an API key restricted to localhost, or an allow-list that does not include the server's address will work at home and be refused from the server.

How do you stop it from happening again?#

Shrink the gap between the two environments:

  1. Keep a .env.example with every variable name and no secrets, and validate at startup as shown above.
  2. Pin versions of the runtime and of dependencies, and install from the lockfile.
  3. Test a clean clone. Clone the repo to a new folder, install fresh, copy only the variables production has, and run it. If it breaks, you found a missing file or variable in two minutes.
  4. Use a container locally if the app is complex, so you run the same operating system and base image (what is Docker).
  5. Log the effective config at startup (names and versions, never secret values), so the first lines of every deploy log tell you what the app saw.

How to do this on Square Cloud#

The checks above apply on any host. On Square Cloud, each one has a specific place to look.

Set variables per application in the dashboard, API, CLI or VS Code. The dashboard imports a .env file and exports the variables as KEY=VALUE, and a change applies on the next restart, with no rebuild. "Is my variable set?" becomes a settings page you can check instead of a guess.

Watch the startup in real time through the dashboard, the CLI or the VS Code extension, and read the first error. Each application runs in an isolated container and sees only what you deployed and configured, just like the clean-clone test.

A few platform rules line up with the causes in this article. On start, the platform runs npm install when there is no node_modules, with NODE_ENV=production, so devDependencies are skipped. A web app must listen on 0.0.0.0 on the port in the PORT variable. The squarecloud.app configuration file is optional: the dashboard generates it at deploy time with the runtime, memory and main file, or you write it at the project root yourself. Memory is part of that configuration, so you fix an out-of-memory crash by raising the value, within your plan's RAM, and starting the app again.

See how environment variables work on the environment variables page.

Deploy

Push from GitHub, the CLI or a ZIP and go live in seconds

Learn more

FAQs

Why does my app work on localhost but crash after deploy?

Something your laptop provides is missing on the server: a .env file, a globally installed package, the same runtime version, or a file whose name differs in letter case. The startup log almost always names it, so read the first error, not the last.

Why is my .env file not working in production?

The .env file is usually listed in .gitignore, so it never reaches the server. Production needs the same variables set in its own settings, or an explicit loader that reads the file you ship.

Why does it work on Windows but fail on Linux?

Linux file systems are case sensitive, so ./Utils.js and ./utils.js are different files. Path separators, line endings in shell scripts and native binaries built for another operating system cause the same kind of failure.

How do I reproduce a production bug locally?

Run the app from a clean clone with a fresh install, the production runtime version and only the variables production has. Running it in a container with the same base image removes most remaining differences.

Is it a code bug or an environment problem?

If the same commit runs on one machine and fails on another, the difference is the environment: versions, variables, files, network or resources. Diff the environments before you touch the code.

Next articleDevelopment, staging and production environmentsDevelopment, staging and production are three copies of your app with separate config and data. Learn how to set them up, promote code and keep them from mixing.

More in Deploy

See all 33 articles