The answer is at the end
I'm developing a web server in Rust, and it works fine using just Docker and defining two services in compose. As follows:
x-common_auth_service: &common_auth_service
container_name: auth
build:
context: ./auth
dockerfile: Dockerfile
network_mode: host
restart: always
deploy:
resources:
limits:
memory: 40M
reservations:
memory: 20M
depends_on:
- redis
services:
auth:
<<: *common_auth_service
container_name: auth
environment:
APP_PORT: 3002 //specific port
auth2:
<<: *common_auth_service
container_name: auth2
environment:
APP_PORT: 3003 //specific port
But now I need to implement these services using Docker Swarm.
Here I came across the first problem when using replicas.
compose.yml
auth_service:
image: service/auth_app_rust
build:
context: ./auth
dockerfile: Dockerfile
networks:
- network_overlay //I was previously a host, hence the problem
ports:
- "5000-5001:3002"
deploy:
mode: replicated
replicas: 2
nginx:
image: nginx:latest
volumes:
- ./nginx/auth.conf:/etc/nginx/nginx.conf
networks:
- host
depends_on:
- auth_service
nginx.conf
upstream auth_server {
server 127.0.0.1:5000;
server 127.0.0.1:5001;
keepalive 200;
}
server {
listen 9999;
location / {
proxy_buffering off;
proxy_set_header Connection "";
proxy_http_version 1.1;
proxy_set_header Keep-Alive "";
proxy_set_header Proxy-Connection "keep-alive";
proxy_pass http://auth_server;
}
}
Services that previously defined ports manually used the same port across replicas, so I decided to use "port range mapping".
And at first it seemed to work fine, but I noticed that Docker Swarm doesn't assign each port to the container, it actually balances between the replicas.
In other words, if I run:
curl http://localhost:5000
It won't necessarily run in container 1.
Although I have found it very convenient to scale services using replicas, and the model also makes it easy to upgrade services, how do I solve this problem?
Because I would still like to use replicas, instead of defining several services, but I would not like to use the Docker Swarm Load Balancer, as I already use it through Nginx.
In short, I want to scale the number of containers running the web server in Rust, without having to create several of them in docker-compose, and also not use the load balancer, as the load balancer alignment would be bad.
EDIT
@Chris Becke provided insight on how to resolve the issue in the overlay network scenario.
Here is how I solved it if host mode was used.
Basically we use the placement "{{.Task.Slot}}" to determine the ports of the services, and not generate conflicts.
In addition to modifying the "update_config" so that when the containers were updated, there would be no port conflicts.
update_config:
parallelism: 1
order: stop-first
compose.yml - host network - Improve performance
auth_service:
image: service/auth_app_rust
hostname: auth-service-{{.Task.Slot}}
build:
context: ./auth
dockerfile: Dockerfile
networks: # on 300*
- host
environment:
REDIS_HOST: localhost
APP_PORT: "300{{.Task.Slot}}" # this way we specify the service port based on the container identifier
METRIC_PORT: "301{{.Task.Slot}}"
deploy:
mode: replicated
replicas: 2
update_config:
parallelism: 1 # !
order: stop-first # !
failure_action: rollback
delay: 5s
resources:
limits:
memory: 40M
reservations:
memory: 20M
depends_on:
- redis
- rust_service_builder
healthcheck:
test: wget --no-verbose --tries=1 --spider http://127.0.0.1:3002/health
retries: 5
interval: 3s
timeout: 5s
networks:
host:
name: host
external: true
Its not clear where nginx is running. Given you are using 127.0.0.1 rather than docker.host.local it seems that nginx is NOT running in a container itself. You also talk about using docker swarm.
This means you ARE, definitionally, using the ingress overlay network which loadbalances to service containers. Just use "127.0.0.1:5000" and let docker deal with finding the correct container.
Alternatively, if you are runnning on a multi node swarm, then change the auth service:
services:
auth_service:
...
ports:
- target: 3000
publish: 5000
mode: host
deploy:
mode: global
And then in the nginx.conf
upstream auth_server {
server server1-ip:5000;
server server2-ip:5000;
keepalive 200;
}
Finally, if nginx is actually running as a container, then drop the publish directives entirely and define a network that will connect nginx and the auth server. Use dockers service template syntax to give each instance a unique hostname - which will be published to containers attached to the same networks:
networks:
proxy:
name: nginx
attachable: true
services:
auth_service:
hostname: auth-service-{{.Task.Slot}}
networks:
- proxy
Attach nginx to the same network and use this config.
upstream auth_server {
server auth-server-1.nginx:3002;
server auth-server-2.nginx:3002;
keepalive 200;
}
This approach will make nginx senstive to the service running as nginx starts. to solve this you need to ensure nginx can resolve hosts at runtime:
You need to include a resolver: 127.0.0.11
directive and use set $backend1 "http://auth-server-1:3002"
to make the hostnames variables which forces nginx to resolve them at runtime. I don't know how to combine that with upstream auth_server however so ymmv.