Your Users Get Logged Out Every Time You Deploy
If everyone has to log in again after each deploy, the cause is one of two things and both are invisible on a laptop. Your sessions live in the container's memory, which is discarded every time the container is recreated — on every deploy, and on every environment variable, port, volume, or preset change. Or your session signing secret is generated at startup, so each restart produces a new key and invalidates every cookie signed with the old one. Express's own documentation is blunt about the second: "Changing the secret value will invalidate all existing sessions" (Express). Both are quick fixes once you know which one you have.
Which one is it?
A fast way to tell them apart:
- Restart the app without deploying — if everyone is logged out, it's one of these two.
- Look at how your secret is set. If it's
crypto.randomBytes(...),os.urandom(...), orsecrets.token_hex()evaluated at import time, that's your answer. A regenerated secret is the more common one in generated code, because it looks responsible. - Otherwise check your session store. If it's the framework default and you never configured one, it's probably in memory.
The regenerated secret
This pattern shows up constantly in AI-written code, and it reads as good practice — nothing hardcoded, nothing committed:
// looks careful, breaks every deploy
const secret = crypto.randomBytes(32).toString("hex");
It runs at startup, so every restart mints a new key and every existing cookie fails verification. Locally you restart rarely and log in constantly, so you never notice.
The secret has to be stable and supplied from outside the process. Express's guidance is exactly this — "the use of environment variables to store the secret, ensuring the secret itself does not exist in your repository" — so generate one once and set it as an environment variable on the app.
Two follow-ons worth knowing. The docs note the secret "should contain at least 32 bytes of entropy". And if you ever need to rotate it without logging everyone out, most frameworks accept a list — the new secret signs, the old ones still verify.
The in-memory session store
The other half. Express is explicit that its default store "is purposely not designed for a production environment. It will leak memory under most conditions, does not scale past a single process, and is meant for debugging and developing."
Defaults vary by framework, which is why "check yours" beats assuming:
- Signed-cookie sessions (Flask's default, Rails' cookie store) hold state in the cookie itself. These survive restarts fine — provided the secret is stable, which brings you back to the section above.
- Database-backed sessions (Django's default) survive as long as your database does.
- Memory stores don't survive anything.
- Filesystem stores are memory stores wearing a disguise here, unless the directory is on a volume — the container's own filesystem is discarded along with it.
For a small app, a SQLite-backed session store on a volume is the straightforward fix, and it's the same choice you'd make for the rest of your data.
Why "just use JWTs" isn't automatically the answer
JWTs are stateless, so restarts don't touch them — genuinely one problem solved. But the same signing-secret rule applies, and you inherit revocation as a new problem: you can't invalidate a token you didn't store, so "log out everywhere" and "ban this user immediately" stop being simple.
The usual compromise is short-lived access tokens with refresh tokens you do store, at which point you have server-side state again — just less of it. That's a reasonable design; it isn't a way of avoiding the decision.
Password hashing costs real CPU here
Worth planning for, because it's the one auth operation that's deliberately expensive.
bcrypt and argon2 are slow by design, and your CPU allocation is a time budget shared by the whole container. A work factor copied from a blog post written for a dedicated server can turn every login into a noticeable stall on a fractional core — and a burst of logins into a queue.
The right move is to measure on your actual allocation, not to guess: time a hash at your chosen cost, and pick the highest setting that keeps login latency acceptable. If that setting comes out lower than you're comfortable with, size the app up rather than weakening the hash — this is not the parameter to economise on. And never hash on every request; hash at login, then carry a session or token.
Rate-limit login, and persist that too
Login endpoints attract credential stuffing, and there's no rate limiting in front of your app — the edge terminates TLS and routes, nothing more.
So you need a limiter, and it needs to survive restarts for the same reason sessions do: an in-memory counter is reset by your own deploys, which is precisely when you least want the protection missing. Same volume, same SQLite file, a couple of columns.
A short checklist
- Secret from an environment variable, never generated at startup.
- Session store that outlives the container — signed cookies, or a store on a volume.
- Cookie flags set:
Secure,HttpOnly, and a sensibleSameSite. You're served over HTTPS, so there's no reason not to. - Login rate limiting, persisted.
- Hashing cost measured on the allocation you actually run.
FAQ
Why are my users logged out after every deploy?
Either sessions are stored in the container's memory, or your session secret is regenerated at startup. Both are cleared when the container is recreated.
Where should the session secret come from?
An environment variable on the app, generated once. Changing the secret invalidates every existing session.
Is an in-memory session store okay for a small app?
No. Express's own docs say its default store is meant for debugging and leaks memory. Use signed cookies or a store on a volume.
Do JWTs fix this?
They remove the session store, but the signing secret must still be stable, and revoking a token becomes harder.
Does a redeploy really clear everything?
Yes — the container is replaced, so anything in memory or in its filesystem goes. Volumes survive.
How should I set my bcrypt cost factor?
Measure it on your actual CPU allocation and pick the highest value with acceptable login latency. If that's too low, increase the allocation rather than the risk.
Wrapping up
Authentication is where generated code most often looks careful and behaves badly, because the failure needs a restart to show up and nothing restarts on your laptop. Pull the secret out of the process, put the sessions somewhere the container's lifecycle doesn't reach, and measure your hashing cost against the CPU you actually bought. After that, deploying stops being something your users notice.