How to fix PHP session loss in Docker containers using Redis as session store — without sticky sessions
By Kevin Marcondes · Published on · 8 min read
PHP session loss in containerized environments usually stems from mismatches between the cookie, PHP and Redis, combined with a proxy/load balancer that doesn’t use sticky sessions. This guide helps you diagnose the cause within minutes and apply stable fixes: PHP session handler settings, Redis TTL, cookie flags, proxy headers and common pitfalls like regenerating the ID on every request. The goal is stable sessions even when requests hit different containers.
Symptoms and quick confirmation
Before changing configuration, validate the symptom. The typical case: a user logs in, navigates and then loses authentication when switching routes, opening a new tab or after a few minutes. This happens more often when there are no sticky sessions.
Useful checks:
- Does the session cookie change between requests?
- Does the session_id exist in Redis and does it expire too soon?
- Is the proxy signaling HTTPS to the app so the cookie can be marked Secure?
Use the browser (Network tab) to inspect Set-Cookie and verify the cookie is sent on subsequent requests. From the command line, check the Set-Cookie header:
- Session cookie missing or replaced on every request
- Low TTL in Redis (expires earlier than expected)
- Cookies with inappropriate SameSite for cross-site flows
- Proxy missing X-Forwarded-Proto when serving HTTPS
curl -sI https://app.seu-dominio.com | grep -i -E 'set-cookie|location'
Why this happens without sticky sessions
Without sticky sessions, any request may land on a different container. That’s fine when the session lives outside the container (Redis), but it requires total consistency:
- Stable cookie: correct domain, path, SameSite and Secure flags.
- Aligned PHP: session.save_handler=redis, matching TTL (gc_maxlifetime) and strict mode to avoid accepting invalid IDs.
- Predictable Redis: key TTL ≥ session lifetime and a memory policy that won’t evict active session keys incorrectly.
- An honest proxy: correct X-Forwarded-Proto/Host headers; otherwise the app may think it’s on HTTP and omit the Secure flag.
Common mistakes include calling session_regenerate_id() on every request, low Redis TTL, wrong serializer/connection settings and cookies with an inappropriate SameSite for redirect flows between domains.
PHP + Redis: essential settings for consistent sessions
Set Redis as the session handler and enable practices that avoid collisions and reusing IDs. Adjust these in php.ini (or a custom ini for PHP-FPM).
Tips: use a per-application prefix, enable strict mode, set a TTL consistent with your business needs and ensure cookies are secure when traffic is HTTPS fronted by a proxy. See PHP's official session directives for reference.
In Docker, inject these directives via a mounted .ini file or environment variables supported by your PHP image.
- Unique prefix per app (avoids collisions between services)
- session.use_strict_mode=1 prevents adopting non-existent IDs
- gc_maxlifetime ≥ your expected session duration
- Cookie Secure + appropriate SameSite for your login flow
; php.ini
session.save_handler = redis
session.save_path = "tcp://redis:6379?database=2&prefix=sess_&timeout=2&read_timeout=2&persistent=1"
session.use_strict_mode = 1
session.gc_maxlifetime = 14400
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Lax
session.sid_length = 48
session.sid_bits_per_character = 6
session.lazy_write = 1
session.serialize_handler = php_serialize
Align timeouts and policies: PHP, cookie, Redis and proxy
Sessions break when time settings don’t match. Ensure Redis TTL doesn’t expire before the application expects it, and that the cookie isn’t discarded prematurely. Avoid memory policies that evict active session keys.
Use this table as a quick reference:
| Component | What to adjust | Practical guidance |
|---|---|---|
| PHP (sessions) | gc_maxlifetime | Match it to your maximum session time (e.g., hours, not minutes) |
| Cookie | cookie_lifetime / SameSite / Secure | 0 (browser session) or a value ≥ gc_maxlifetime |
| Redis | TTL per session key | Should be ≥ gc_maxlifetime; ideally a few minutes longer |
| Redis (memory) | maxmemory / policy | Avoid allkeys-* on shared stores; prefer volatile-* |
| Proxy/LB | idle timeout and headers | Timeout ≥ long requests; forward X-Forwarded-Proto/Host |
Reverse proxy without sticky sessions: correct headers and TLS
When the proxy doesn't maintain affinity, headers must be flawless so the app can set cookies correctly. Without X-Forwarded-Proto=HTTPS, many apps will not mark the cookie Secure and the browser won’t send it on the next request.
Example Nginx reverse-proxy configuration. Adjust it for your stack. For comparisons of proxies in Docker, consult dedicated resources on that topic.
- Propagate the real Host and Proto to the app
- Enable healthchecks on the load balancer to remove bad nodes from the pool
- Avoid rewriting cookies incorrectly in the proxy
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://app_upstream;
}
Practical debugging: inspect the session_id and what’s stored in Redis
Log the session_id per request to confirm stability and detect unexpected regenerations. In production, reduce log verbosity and centralize logs. In Redis, check the TTL of the key (prefix + ID) to see if it's being refreshed.
Avoid using MONITOR in high-traffic environments; prefer targeted checks on a specific session key.
- Correlate session_id with request_id in your logs
- Check TTL with targeted commands for the specific key
- Confirm multiple containers access the same Redis and prefix
// middleware simples para depuração
session_start();
error_log('sid_before=' . session_id());
// ... sua lógica de autenticação ...
// Evite chamar session_regenerate_id() em toda requisição
session_write_close();
ID regeneration, concurrency and lost writes
session_regenerate_id(true) is useful after login, but calling it on every request causes session loss under concurrency. Two tabs can alternate IDs and overwrite each other's data.
Best practices: regenerate only on security events (login, privilege change), allow a single write per request and close the session early when you no longer need to write (session_write_close).
- Regenerate only on critical events
- Avoid multiple writes within the same request
- Close the session early when only reading
Cookies, domains and SameSite for redirect flows
SameSite=Lax solves most cases; for SSO or iframe flows you may need SameSite=None and Secure. Set the cookie domain when using subdomains (e.g., .example.com) and keep the path consistent (/).
Be cautious with mixed HTTP/HTTPS environments: if the app marks cookies Secure but the user reaches the app over HTTP at some point, the cookie won’t be sent. Enforce HTTPS end-to-end at the proxy level.
- SameSite=Lax for normal navigation; None+Secure for SSO/iframe
- Set cookie_domain when using subdomains
- Standardize HTTPS end-to-end (proxy should signal via X-Forwarded-Proto)
Final validation checklist (no sticky sessions)
Use this list to close the incident with confidence. Test across two different containers and monitor for a few hours before declaring the issue resolved.
- Stable cookie (name, domain, path, Secure, SameSite)
- session.save_handler=redis with prefix and strict mode
- gc_maxlifetime ≥ Redis TTL; session TTL renews on access
- Proxy sends X-Forwarded-Proto/Host correctly
- No session_regenerate_id on every request
- Redis policy does not evict active session keys
- Readiness/healthchecks remove bad nodes from the pool
Frequently asked questions
How do I configure PHP to use Redis as the session handler in Docker?
Load the redis extension, set session.save_handler=redis and point session.save_path to your Redis service (host, port, database and prefix). Adjust gc_maxlifetime, cookie_secure and SameSite according to your flow. Mount a .ini in the PHP-FPM container or use environment variables supported by your image.
Why do sessions disappear if I call session_regenerate_id on every request?
Because each request creates a new ID and, under concurrency (multiple tabs), one request can invalidate another's ID. This is more visible without sticky sessions. Regenerate only after login or privilege changes and close the session early to reduce collisions.
Can Redis evict my sessions even with TTL set?
Yes—if the memory policy allows evicting keys (e.g., allkeys-* under memory pressure). Prefer volatile-* policies for stores where keys have TTL and ensure you have enough memory for peak usage. Align Redis TTL with PHP's gc_maxlifetime and monitor memory.
Do I need sticky sessions for stability?
No. If sessions live in Redis and cookie and headers are configured correctly, you can load balance freely. Sticky sessions only mask configuration problems and reduce load distribution. Fix cookies, PHP/Redis and proxy instead.
How can I test if the proxy is breaking cookies?
Check Set-Cookie and Cookie in the browser Network panel and compare with curl to confirm headers. The proxy must forward X-Forwarded-Proto=HTTPS, preserve Host and not rewrite Set-Cookie. Enable healthchecks to remove nodes with clock skew or transient issues.
Need to stabilize PHP sessions in production?
If you want a quick review of your stack (PHP-FPM, Docker, Redis and proxy) and a low-risk remediation plan, let's talk.
I want hands-on helpSee also
- Intermittent 502 Bad Gateway in Docker Compose with Nginx & PHP‑FPM: how to diagnose and fix timeouts and connection overloads
- How to diagnose and capture packets for intermittent network issues between Docker containers on Debian
- How to detect and fix duplicate SKU inconsistencies between WooCommerce and your ERP without losing orders
- How to reduce build time and avoid duplicate artifacts in CI for large PHP monoliths