- Published on
- •2 min read
Docker + Node.js + bcrypt: Why It Worked Locally but Failed in Production
- Authors

- Name
- Pulathisi Kariyawasam

While working on a Node.js authentication service, I ran into a strange issue that took some time to understand.
Everything worked perfectly on my local machine, but once I deployed the same code inside Docker, the API started failing without any clear error.
In this article, I want to share what the issue was, how I approached debugging it, what I missed at first, and the solution that finally fixed it.
Hopefully, this will save time for someone facing the same problem.
The Problem I Faced
I had a simple authentication API with a register endpoint.
Locally, the API worked without any issues.
But when I ran the same application inside Docker and tried to call the register endpoint, I got this error:
curl: (52) Empty reply from server
No error message.
No stack trace.
The container just restarted.
At first, this was confusing because the same request worked fine on my local machine.
My Environment Setup
This was my setup at the time:
Local machine
- Node.js v20.19.4
- OS: Ubuntu 24.04.3 LTS
- Framework: Express.js with TypeScript
- Password hashing: bcrypt
Docker
- Base image: node:18-alpine
- Same codebase
- Same environment variables
This difference looked small, but it was actually the main reason for the issue.
How the Issue Appeared
Local request (working)
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"TestPass123!"}'
Response:
{
"success": true,
"message": "Registration successful"
}
Docker request (failing)
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"TestPass123!"}'
Response:
curl: (52) Empty reply from server
First Debugging Steps
The container was restarting again and again.
I checked the container status:
docker ps
It showed the service restarting repeatedly.
Health checks were also failing, which confirmed that the application was crashing.
Adding Debug Logs
Since there was no clear error, I added logs step by step inside the register endpoint.
logger.info('Register called');
logger.info('Validation passed');
logger.info('Checking email');
logger.info('About to hash password');
The logs stopped at:
logger.info('About to hash password');
Anything after that never ran.
This told me one important thing:
👉 The crash was happening during password hashing
Finding the Exact Line
Inside my password service, I had this code:
import bcrypt from 'bcrypt';
const hash = await bcrypt.hash(password, saltRounds);
This line worked locally, but inside Docker it caused the app to crash silently.
What I Missed at First
The mistake I made was assuming Docker is just another runtime.
In reality, my environments were different in important ways:
- Local: Node.js 20 + glibc
- Docker: Node.js 18 + Alpine Linux (musl)
And this matters a lot for native modules.
Why bcrypt Failed in Docker
bcrypt is not a pure JavaScript library.
It uses native code and needs to be compiled for the system it runs on.
In my case:
- bcrypt depends on native binaries
- Alpine Linux uses musl instead of glibc
- Node version was different (20 locally, 18 in Docker)
- The native binary did not match the Docker environment
Because of this mismatch, Node crashed instead of throwing a normal error.
That is why I saw "Empty reply from server" instead of a proper exception.
Why It Worked Locally
On my local machine:
- Node version matched the compiled binary
- Full build tools were available
- The OS libraries were compatible
So bcrypt worked without any problem.
The Solution I Chose
After understanding the root cause, I had a few options:
- Rebuild bcrypt inside Docker with extra tools
- Change the Docker base image
- Avoid native modules completely
I chose the simplest and safest option.
Switching to bcryptjs
bcryptjs is a pure JavaScript implementation of bcrypt.
It does not depend on native binaries.
Code Changes
package.json
{
"dependencies": {
"bcryptjs": "^2.4.3"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6"
}
}
Import change
// Before
import bcrypt from 'bcrypt';
// After
import bcrypt from 'bcryptjs';
Usage stays the same
const hash = await bcrypt.hash(password, saltRounds);
const isValid = await bcrypt.compare(password, hash);
Rebuilding Docker
npm install
docker compose build --no-cache
docker compose up -d
Result After the Fix
After switching to bcryptjs, the Docker container stopped crashing.
The same API call now worked correctly inside Docker.
{
"success": true,
"message": "Registration successful"
}
Performance and Security Notes
| Feature | bcrypt | bcryptjs |
|---|---|---|
| Implementation | Native | JavaScript |
| Docker friendly | ❌ | ✅ |
| Performance | Faster | Slightly slower |
| Security | Strong | Strong |
For most APIs, the performance difference is not noticeable, but the stability improvement is huge.
Other Possible Solutions (I Did Not Choose)
- Install build tools and rebuild bcrypt inside Docker
- Use a non-Alpine base image like
node:18-bullseye - Fully standardize Node versions across all environments
These options work, but they add more complexity.
Lessons I Learned
Here are the key things I learned from this issue:
- Docker environments are not the same as local machines
- Native Node.js modules can fail silently
- Alpine Linux can cause issues with native libraries
- Logging step by step helps find silent crashes
- Pure JavaScript libraries are safer in containers
- Node version differences matter more than expected
Final Thoughts
This issue took time to debug because there was no clear error message.
But once I understood how native modules work in Docker, the fix was simple.
If your Node.js app works locally but crashes inside Docker, check native dependencies first.
I hope this experience helps someone avoid the same problem.
Thanks for reading 👋
