A demo project comparing two nginx-only approaches to sticky session handling:
- IP Hash Sticky - requests from same IP always go to same server
- Cookie-Based Sticky - nginx sets/reads cookie to route to same server
.
├── app/
│ ├── app.py # Flask application
│ ├── Dockerfile
│ └── requirements.txt
├── nginx/
│ ├── nginx.conf # Nginx config with ip_hash
│ └── nginx-cookie.conf # Nginx config with cookie-based routing
├── docker-compose.yml # Option 1: ip_hash sticky sessions
├── docker-compose.cookie.yml # Option 2: Cookie-based sticky sessions
└── README.md
| Endpoint | Description |
|---|---|
/ |
Returns server ID, visit count, servers seen |
/health |
Health check endpoint |
/reset |
Clears the session |
Uses nginx ip_hash directive to route requests from the same client IP to the same backend server.
docker-compose up --buildrm -f cookies.txt
for i in {1..5}; do
curl -s -b cookies.txt -c cookies.txt http://localhost:8080 | jq -r '"server: \(.server_id) | visits: \(.visit_count) | seen: \(.servers_seen | join(","))"'
doneserver: server-1 | visits: 1 | seen: server-1
server: server-1 | visits: 2 | seen: server-1
server: server-1 | visits: 3 | seen: server-1
server: server-1 | visits: 4 | seen: server-1
server: server-1 | visits: 5 | seen: server-1
docker-compose downUses nginx map directive to read a SERVERID cookie and route to the matching server. Nginx sets the cookie on first request.
Privacy benefit: No IP tracking. Routing is based purely on a cookie value.
docker-compose -f docker-compose.cookie.yml up --buildrm -f cookies.txt
for i in {1..5}; do
curl -s -b cookies.txt -c cookies.txt http://localhost:8080 | jq -r '"server: \(.server_id) | visits: \(.visit_count) | seen: \(.servers_seen | join(","))"'
doneserver: server-2 | visits: 1 | seen: server-2
server: server-2 | visits: 2 | seen: server-2
server: server-2 | visits: 3 | seen: server-2
server: server-2 | visits: 4 | seen: server-2
server: server-2 | visits: 5 | seen: server-2
cat cookies.txt | grep SERVERIDYou'll see something like SERVERID=flask2 - this is what nginx uses to route.
docker-compose -f docker-compose.cookie.yml down| Aspect | ip_hash Sticky | Cookie Sticky |
|---|---|---|
| Routing Based On | Client IP | Cookie value |
| Privacy | IP tracked by nginx | No IP tracking |
| First Request | Deterministic (IP hash) | Round-robin |
| Complexity | Simple | Medium |
| Use Case | Simple apps, internal tools | Privacy-focused apps |
Client (IP: 1.2.3.4) → Nginx (ip_hash) → Always Server-1
└── Local Session Store
First request:
Client ──────────────────→ Nginx (round-robin) → Server-2
←─ Set-Cookie: SERVERID=flask2 ─────────────┘
Subsequent requests:
Client ── Cookie: SERVERID=flask2 ──→ Nginx (reads cookie) → Server-2
└── Local Session Store
- Add new service in the docker-compose file:
flask4:
build: ./app
environment:
- SERVER_ID=server-4
- SECRET_KEY=shared-secret-key-123
expose:
- "5000"
networks:
- backend- Add server to the nginx config:
For ip_hash (nginx/nginx.conf):
upstream flask_backend {
ip_hash;
server flask1:5000;
server flask2:5000;
server flask3:5000;
server flask4:5000; # new server
}For cookie-based (nginx/nginx-cookie.conf):
# Add to the map directives
map $cookie_SERVERID $backend_server {
default "";
"flask1" flask1:5000;
"flask2" flask2:5000;
"flask3" flask3:5000;
"flask4" flask4:5000; # new server
}
map $upstream_addr $server_name_cookie {
~^flask1:5000 "flask1";
~^flask2:5000 "flask2";
~^flask3:5000 "flask3";
~^flask4:5000 "flask4"; # new server
default "";
}
upstream flask_backend {
server flask1:5000;
server flask2:5000;
server flask3:5000;
server flask4:5000; # new server
}- Update nginx dependency in docker-compose:
nginx:
depends_on:
- flask1
- flask2
- flask3
- flask4 # add here tooFor the ip_hash setup, you can switch strategies in nginx/nginx.conf:
upstream flask_backend {
# Pick ONE of these strategies:
# 1. Sticky sessions (same IP → same server)
ip_hash;
# 2. Round-robin (default, no directive needed)
# requests cycle through servers in order
# 3. Least connections (send to least busy server)
least_conn;
# 4. Weighted (server1 gets 3x traffic)
# server flask1:5000 weight=3;
# server flask2:5000 weight=1;
server flask1:5000;
server flask2:5000;
server flask3:5000;
}| Variable | Description |
|---|---|
SERVER_ID |
Identifier shown in API responses |
SECRET_KEY |
Flask session signing key (must match across servers) |