"It Works Locally" — Why Your Bot Breaks the Moment You Deploy

·5 min read·Updated
botsdebuggingtroubleshootingpythonnodejs

Your bot runs fine on your laptop and dies immediately in a container, and the code didn't change — so the difference is the machine. The usual culprits are a small, boring set: environment variables you never uploaded, a case-sensitive filesystem that no longer forgives import Utils when the file is utils.py, Windows line endings breaking a shell script, a clock that's UTC rather than your timezone, and files your app writes that don't survive. Almost every "works locally" failure is one of eight things, and you can check all of them in a few minutes.

1. Your environment variables aren't there

The most common one by a distance. Your local run reads a .env file, or variables you exported in your shell months ago and forgot about. Neither exists in the container.

Set them as environment variables on the app. And check the .env file itself isn't the thing you're relying on — if you deploy a zip and your code calls load_dotenv(), it'll silently find nothing and hand you None rather than erroring.

The fast check: open the app's shell and run env | grep BOT (or whatever prefix). It settles the question in one command.

2. The filesystem is case-sensitive

This one bites Windows and macOS developers specifically, and the error looks like your code is wrong.

Windows and macOS default to case-insensitive filesystems, so import Utils finds utils.py and require('./Config') finds config.js. Linux doesn't. Deploy, and you get ModuleNotFoundError: No module named 'Utils' — for a file that's plainly sitting right there.

Check the exact casing of every import and file path against the actual filenames. Directory names count too.

3. Windows line endings break scripts

If you have a start.sh or any shell script, and you're on Windows, git may have given it CRLF line endings. Linux reads the trailing \r as part of the command:

/usr/bin/env: 'python\r': No such file or directory

That error message is the tell — a stray \r after an interpreter name. Fix it with a .gitattributes:

*.sh text eol=lf

Same applies to any file your app parses line-by-line where a trailing carriage return would break a value.

4. The clock is UTC

Your container's timezone is UTC, not yours. A bot posting a "daily 9am digest" fires at 09:00 UTC, which is a different time where you live, and the gap shifts twice a year with daylight saving.

Set the timezone explicitly in your scheduler rather than relying on the system default — in-process scheduling covers this, along with why an in-memory schedule resets on restart.

5. Files you write don't stick

Locally, a file your bot writes stays put forever. In a container it lives in the writable layer and is discarded when the container is recreated, which happens on every redeploy.

If your bot keeps a SQLite database, a JSON config, or a cache next to its code, it needs a volume. The symptom is data that survives restarts and then mysteriously resets after you ship a change.

6. A dependency isn't in your manifest

It works locally because the package is in your environment from something else you installed. Your requirements.txt or package.json never got it.

Test it properly: create a clean virtualenv, or clone into a fresh directory and run npm ci, then start the app. Two minutes, and it catches this class entirely.

7. A different language version

Your laptop has Python 3.13 or Node 22; the build picks a version from your project's signals. If your code uses a newer syntax feature than the runtime it gets, you'll see a syntax error rather than a logic bug.

Declare what you need — engines in package.json, a version marker in pyproject.toml — rather than leaving it to inference. What the build decides for Python and for Node covers what those signals are.

8. You can't see the error

If your bot dies and the logs are empty, that's Python buffering stdout when it isn't attached to a terminal — output sits in a buffer and dies with the process. PYTHONUNBUFFERED=1 fixes it.

Once you can see output, remember a crash-looping bot still reports as running, so read the logs for the exit-code line rather than trusting the status indicator.

The five-minute version

In order, because each rules out a chunk:

  1. Read the logs. Find the exit code and the first failure, not the most recent.
  2. Check env vars exist from the shell.
  3. Check the casing of your imports and paths against actual filenames.
  4. Look for \r in any error message mentioning an interpreter.
  5. Install from your manifest into a clean environment locally and run it.

If all five pass and it still fails, the difference is probably something the container can't reach — an API allowing only your home IP, a service on your local network, or a file that only exists on your machine.

FAQ

Why does my bot work locally but not when deployed?

Usually environment variables that only exist on your machine, a case-sensitive Linux filesystem rejecting an import that Windows or macOS accepted, or a dependency missing from your manifest.

Why do I get ModuleNotFoundError for a file that exists?

Linux filesystems are case-sensitive. Utils.py and utils.py are different files there, and identical on Windows and macOS.

What does 'python\r': No such file or directory mean?

Your script has Windows CRLF line endings and Linux is reading the carriage return as part of the command. Force LF with *.sh text eol=lf in .gitattributes.

Why are my scheduled messages at the wrong time?

The container clock is UTC. Set the timezone explicitly in your scheduler.

Why does my data reset after I deploy?

It's stored outside a volume. The container's filesystem is discarded when it's recreated, which every redeploy does.

My logs are completely empty — why?

Python buffers stdout when not attached to a terminal, so output can die with the process. Set PYTHONUNBUFFERED=1.

Wrapping up

"Works on my machine" is rarely mysterious once you accept that your machine is doing you several favours — remembering your environment variables, forgiving your capitalisation, running in your timezone, and keeping every file you write. A container does none of those, and that's the whole list. Go through the eight, and the one that's biting you is almost always in there.