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
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.
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:
TypeError: Cannot read properties of undefined (reading 'split')
DiscordjsError [TokenInvalid]: An invalid token was provided.
connect ECONNREFUSED 127.0.0.1:5432The 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:
- 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.
- Does your code load the file at all?
process.envonly sees what the process was started with. On recent Node.js versions, you can start withnode --env-file=.env index.js(Node.js docs). Plainnode index.jsreads no file, and Python needs a loader such aspython-dotenv. - Are the values strings? Environment variables are always strings.
DEBUG=falseis the string"false", which is truthy in JavaScript. Node.js also converts anything you assign toprocess.envto a string, so assigningundefinedstores the text"undefined"(Node.js docs).
Fail early so the next miss is obvious:
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, theenginesfield 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 pinnedrequirements.txtfrompip 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:
npm cinpm 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
.gitignoreor left out of the ZIP you uploaded (aconfig.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.
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 interpretererrors. - 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
localhostor127.0.0.1means 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 (::or0.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 to127.0.0.1by 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:
- Keep a
.env.examplewith every variable name and no secrets, and validate at startup as shown above. - Pin versions of the runtime and of dependencies, and install from the lockfile.
- 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.
- Use a container locally if the app is complex, so you run the same operating system and base image (what is Docker).
- 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
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.
More in Deploy
See all 33 articles- What are environment variables?Environment variables are key-value pairs the operating system hands to a process at startup. Learn to read them in Node.js and Python and keep tokens out of code.
- What is a deployment?A deployment takes a version of your code to a server, prepares it and starts it. See the stages, the methods and how to roll back when it goes wrong.
- How much RAM and CPU does my app need?Size RAM and CPU by measuring your app's real peak and adding about 30% headroom. Learn how to measure in Node.js, Python and Java and what to do if short.
- Automatic deploys with GitHub ActionsAutomatic deploys with GitHub Actions ship your code to the server on every push. See the workflow step by step, secrets, tests before deploy and common errors.