Docker Swarm: Getting Started with a 3 Node Docker Swarm Cluster with a Scalable App

What are we up to today?

  • Install Docker and Docker Compose on 3 Nodes
  • Initialize the Swarm and Join the Worker Nodes
  • Create a Nginx Service with 2 Replicas
  • Do some Inspection: View some info on the Service
  • Demo: Create a Registry, Flask App with Python that Reports the Hostname, Scale the App, Change the Code, Update the Service, etc.

For detailed information on Docker Swarm, have a look at their Docker's Documentation.

Getting Started:

Bootstrap Docker Swarm Setup with Docker Compose on Ubuntu 16.04:

Installing Docker:

Run the following on all 3 Nodes as the root user:

$ apt-get update && apt-get upgrade -y
$ apt-get remove docker docker-engine -y
$ apt-get install apt-transport-https ca-certificates curl gnupg2 software-properties-common -y

$ curl -fsSL https://download.docker.com/linux/debian/gpg | sudo $ apt-key add -
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable"

$ apt-get update
$ apt-get install docker-ce -y
$ systemctl enable docker
$ systemctl restart docker

$ curl -L https://github.com/docker/compose/releases/download/1.13.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
$ chmod +x /usr/local/bin/docker-compose
$ usermod -aG docker <your-user>

Testing:

Test Docker by running a Test Container

$ docker --version
$ docker-compose --version
$ docker run hello-world

In this example I do not rely on DNS, and for simplicity, I just added the host entry data into the /etc/hosts files on each node:

$ cat /etc/hosts
172.31.18.90    manager
172.31.20.94    worker-1
172.31.21.50    worker-2

Initialize the Swarm:

Now we will initialize the swarm on the manager node and as we have more than one network interface, we will specify the --advertise-addr option:

[manager] $ docker swarm init --advertise-addr 172.31.18.90
Swarm initialized: current node (siqyf3yricsvjkzvej00a9b8h) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join \
    --token SWMTKN-1-0eith07xkcg93lzftuhjmxaxwfa6mbkjsmjzb3d3sx9cobc2zp-97s6xzdt27y2gk3kpm0cgo6y2 \
    172.31.18.90:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

If this is a scenario where we would like to add more than one manager, we would need to run the above command to add more managers.

Join the Worker nodes to the Manager:

Now, to join the worker nodes to the swarm, we will run the docker swarm join command that we received in the swarm initialisation step, like below:

[worker-1] $ docker swarm join --token SWMTKN-1-0eith07xkcg93lzftuhjmxaxwfa6mbkjsmjzb3d3sx9cobc2zp-97s6xzdt27y2gk3kpm0cgo6y2 172.31.18.90:2377
This node joined a swarm as a worker.

And to join the second worker to the swarm:

[worker-2] $ docker swarm join --token SWMTKN-1-0eith07xkcg93lzftuhjmxaxwfa6mbkjsmjzb3d3sx9cobc2zp-97s6xzdt27y2gk3kpm0cgo6y2 172.31.18.90:2377
This node joined a swarm as a worker.

To see the node status, so that we can determine if the nodes are active/available etc, from the manager node, list all the nodes in the swarm:

[manager] $ docker node ls
ID                           HOSTNAME  STATUS  AVAILABILITY  MANAGER STATUS
j14mte3v1jhtbm3pb2qrpgwp6    worker-1  Ready   Active
siqyf3yricsvjkzvej00a9b8h *  master    Ready   Active        Leader
srl5yzme5hxnzxal2t1efmwje    worker-2  Ready   Active

If at any time, you lost your join token, it can be retrieved by running the following for the manager token:

$ docker swarm join-token manager -q
SWMTKN-1-67chzvi4epx28ii18gizcia8idfar5hokojz660igeavnrltf0-09ijujbnnh4v960b8xel58pmj

And the following to retrieve the worker token:

$ docker swarm join-token worker -q
SWMTKN-1-67chzvi4epx28ii18gizcia8idfar5hokojz660igeavnrltf0-acs21nn28v17uwhw0oqg5ibwx

At this moment, we will see that we have no services running in our swarm:

[manager] $ docker service ls
ID  NAME  MODE  REPLICAS  IMAGE

Deploying our First Service:

Let's create a nginx service with 2 replicas, which means that there will be 2 containers of nginx running in our swarm.

If any of these containers fail, they will be spawned again to have the desired number that we set on the replica option:

[manager] $ docker service create --name my-web --publish 8080:80 --replicas 2 nginx

Let's have a look at our nginx service:

[manager] $ docker service ls
ID            NAME    MODE        REPLICAS  IMAGE
1okycpshfusq  my-web  replicated  2/2       nginx:latest

After we see that the replica count is 2/2 our service is ready.

To see on what nodes our containers are running that makes up our service:

[manager] $ docker service ps my-web
ID            NAME      IMAGE         NODE      DESIRED STATE  CURRENT STATE           ERROR  PORTS
k0qqrh8s0c2d  my-web.1  nginx:latest  worker-1  Running        Running 30 seconds ago
nku9wer6tmll  my-web.2  nginx:latest  worker-2  Running        Running 30 seconds ago

We can also retrieve more information of our service by using the inspect option:

[manager] $ docker service inspect my-web
[
    {
        "ID": "1okycpshfusqos8023gtvdf11",
        "Version": {
            "Index": 22
        },
        "CreatedAt": "2017-06-21T07:58:15.177547236Z",
        "UpdatedAt": "2017-06-21T07:58:15.178864919Z",
        "Spec": {
            "Name": "my-web",
            "TaskTemplate": {
                "ContainerSpec": {
                    "Image": "nginx:latest@sha256:41ad9967ea448d7c2b203c699b429abe1ed5af331cd92533900c6d77490e0268",
                    "DNSConfig": {}
                },
                "Resources": {
                    "Limits": {},
                    "Reservations": {}
                },
                "RestartPolicy": {
                    "Condition": "any",
                    "MaxAttempts": 0
                },
                "Placement": {},
                "ForceUpdate": 0
            },
            "Mode": {
                "Replicated": {
                    "Replicas": 2
                }
            },
            "UpdateConfig": {
                "Parallelism": 1,
                "FailureAction": "pause",
                "MaxFailureRatio": 0
            },
            "EndpointSpec": {
                "Mode": "vip",
                "Ports": [
                    {
                        "Protocol": "tcp",
                        "TargetPort": 80,
                        "PublishedPort": 8080,
                        "PublishMode": "ingress"
                    }
                ]
            }
        },
        "Endpoint": {
            "Spec": {
                "Mode": "vip",
                "Ports": [
                    {
                        "Protocol": "tcp",
                        "TargetPort": 80,
                        "PublishedPort": 8080,
                        "PublishMode": "ingress"
                    }
                ]
            },
            "Ports": [
                {
                    "Protocol": "tcp",
                    "TargetPort": 80,
                    "PublishedPort": 8080,
                    "PublishMode": "ingress"
                }
            ],
            "VirtualIPs": [
                {
                    "NetworkID": "wvim41tbf7tsmi7z49g56bisa",
                    "Addr": "10.255.0.6/16"
                }
            ]
        },
        "UpdateStatus": {
            "StartedAt": "0001-01-01T00:00:00Z",
            "CompletedAt": "0001-01-01T00:00:00Z"
        }
    }
]

We can get the Endpoint Port info by using inspect and using the --format parameter to filter the output:

[manager] $ docker service inspect --format="{{json .Endpoint.Ports}}" my-web  | python -m json.tool

From the output we will find the PublishedPort is the Port that we Expose, which will be the listener. Our TargetPort will be the port that is listening on the container:

[
    {
        "Protocol": "tcp",
        "PublishMode": "ingress",
        "PublishedPort": 8080,
        "TargetPort": 80
    }
]

To get the Virtual IP Info:

[manager] $ docker service inspect --format="{{json .Endpoint.VirtualIPs}}" my-web  | python -m json.tool
[
    {
        "Addr": "10.255.0.6/16",
        "NetworkID": "wvim41tbf7tsmi7z49g56bisa"
    }
]

To only get the Published Port:

[manager] $ docker service inspect --format="{{range .Endpoint.Ports}}{{.PublishedPort}}{{end}}" my-web
8080

We can also list our networks:

[manager] $ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
1cb24ee7e385        bridge              bridge              local
503b2f0eda49        docker_gwbridge     bridge              local
fe679d82d502        host                host                local
wvim41tbf7ts        ingress             overlay             swarm
310b9219ec0c        none                null                local

And also inspecting our network for more information:

[manager] $ docker network inspect ingress
[
    {
        "Name": "ingress",
        "Id": "wvim41tbf7tsmi7z49g56bisa",
        "Created": "2017-06-21T07:50:44.14232108Z",
        "Scope": "swarm",
        "Driver": "overlay",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "10.255.0.0/16",
                    "Gateway": "10.255.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Containers": {
            "ingress-sbox": {
                "Name": "ingress-endpoint",
                "EndpointID": "b6b2a8f3f233cfc77806718a9c47daf02ad128ba81c96d6382ff1d3799c3b5c1",
                "MacAddress": "02:42:0a:ff:00:03",
                "IPv4Address": "10.255.0.3/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.driver.overlay.vxlanid_list": "4096"
        },
        "Labels": {},
        "Peers": [
            {
                "Name": "master-59041a33bebc",
                "IP": "172.31.18.90"
            },
            {
                "Name": "worker-1-cfee817ddb5f",
                "IP": "172.31.20.94"
            },
            {
                "Name": "worker-2-40891fb1fa3f",
                "IP": "172.31.21.50"
            }
        ]
    }
]

To get the IP of container in a swarm cluster:

[manager] $ docker service ps my-web
ID            NAME      IMAGE         NODE      DESIRED STATE  CURRENT STATE           ERROR  PORTS
k0qqrh8s0c2d  my-web.1  nginx:latest  worker-1  Running        Running 31 minutes ago
nku9wer6tmll  my-web.2  nginx:latest  worker-2  Running        Running 31 minutes ago

For example, we would like to determine the IP for our my-web.1 nginx service on worker-1:

[worker-1] $ docker ps
CONTAINER ID        IMAGE                                                                           COMMAND                  CREATED             STATUS              PORTS               NAMES
72e05af4e654

Then inspect the container to get the IP Address:

[worker-1] $ docker inspect 72e05af4e654 --format "{{json .NetworkSettings.Networks.ingress.IPAddress}}"
"10.255.0.8"

or:

[worker-1] $ docker inspect 72e05af4e654 --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}"
10.255.0.8

Creating the Demo Web Application Service Example:

In this example, we will create a Python Flask Example that prints out the hostname of the container (containerid) and also prints a UUID.

So when we scale the service we can see that the round-robin load balancing algorithm, reports a different hostname on each HTTP Request.

Creating a Private Registry to Save our Images:

We will deploy a private registry server as a service into our swarm that will act as a private registry that will host our images that we will be pushing to:

[manager] $ docker service create --name registry --publish "5000:5000" registry:2

Create a Network:

We will create a network, which will use the overlay driver which spans multiple docker hosts, that makes up the overlay network and specify the subnet of choice:

[manager] $ docker network create --driver overlay --subnet 10.24.90.0/24 mynet

Now that our network is created, let's inspect the network to determine the detail under the hood:

$ docker network inspect mynet
[
    {
        "Name": "mynet",
        "Id": "byzlzl4r2mwv4j9jisu7ellhs",
        "Created": "0001-01-01T00:00:00Z",
        "Scope": "swarm",
        "Driver": "overlay",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "10.24.90.0/24",
                    "Gateway": "10.24.90.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Containers": null,
        "Options": {
            "com.docker.network.driver.overlay.vxlanid_list": "4103"
        },
        "Labels": null
    }
]

Also, when service is associated with this network, in terms of dns resolution, the service name will respond to the virtual ip (vip) of the service.

Create the Local Directory, Code, Dockerfile and docker-compose config:

Create the Directory where our Code will be Stored Locally:

$ mkdir flask-demo
$ cd flask-demo

Set the Python Dependency in the requirements.txt file:

$ cat requirements.txt
flask

Creating a Python Flask App (app.py) to Show the Container Name and Random uuid String:

$ cat app.py
from flask import Flask
import os
import uuid

app = Flask(__name__)

@app.route('/')
def index():
    hostname = os.uname()[1]
    randomid = uuid.uuid4()
    return 'Container Hostname: ' + hostname + ' , ' + 'UUID: ' + str(randomid)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5098)

Create the Dockerfile that will act as our instruction on how to build our image:

$ cat Dockerfile
FROM python:3.4-alpine
ADD . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["python", "app.py"]

Create the Docker Compose file, that will build our image using our Dockerfile, specifying the exposed ports, the image registry and we will also specify to use our existing mynet network:

$ cat docker-compose.yml
version: '3'

services:
  web:
    image: master:5000/flask-app
    build: .
    ports:
      - "80:5098"

networks:
  default:
    external:
      name: mynet

Testing locally on the host:

$ docker-compose up
 # do some testing
$ docker-compose down

Build the Image, the push to the private registry that is specified in the docker-compose.yml file:

$ docker-compose build
$ docker-compose push
Pushing web (master:5000/flask-app:latest)...
The push refers to a repository [master:5000/flask-app]
a8deddc4e222: Pushed
2916dd76ba80: Pushed
e03c530e5673: Layer already exists
0342f4a51cd7: Layer already exists
5e94921eba94: Layer already exists
ed06208397d5: Layer already exists
5accac14015f: Layer already exists
latest: digest: sha256:47b26870a38e3e468f44f81f4bf91ab9aca0637714f5b8795fccf2a85e29c8e1 size: 1786

Now that we have our image saved in our private registry, create the service from that image and the published port 80 that will translate to port 5098 on the containers that is set in the code.

Note that we set only 1 replica, which means that every time we make a HTTP Request, we will be returned with the same hostname as we only have 1 container in our swarm.

We also set --update-delay to 5 seconds so this means, when we update our service, each task (container) will be updated with 5 seconds delay between each one.

Creating the Flask-Demo Service:

[manager] $ docker service create --name flask-demo --network mynet --update-delay 5s --publish 80:5098 --replicas 1 master:5000/flask-app

Listing our Services in our Swarm:

List the services in our swarm and check if the replica number is the same as the desired count that was set when the service was created:

[manager] $ docker service ls
ID            NAME        MODE        REPLICAS  IMAGE
5y520y6fau5j  flask-demo  replicated  1/1       master:5000/flask-app:latest
xfk2z5s2ybcg  registry    replicated  1/1       registry:2

After we can see that the desired number is set, we can also run a ps to see the state and running time, as well as on which node the container lives:

$ docker service ps flask-demo
ID            NAME          IMAGE                         NODE      DESIRED STATE  CURRENT STATE           ERROR  PORTS
u7ab0xhwrlzh  flask-demo.1  master:5000/flask-app:latest  worker-1  Running        Running 11 seconds ago

Double checking, logging onto worker-1 and determine if the container lives on the mentioned node:

[worker-1] $ docker ps --filter "name=flask-demo" --format "table {{.ID}}\t{{.Names}}"
CONTAINER ID        NAMES
4e6bd7a8a2bf        flask-demo.1.u7ab0xhwrlzh3r8xme08ievop

For just some verification, lets have a look at the exposed port:

$ docker service inspect --format="{{range .Endpoint.Ports}}{{.PublishedPort}}{{end}}" flask-demo
80

The awesome thing here is that docker swarm uses the ingress routing mesh, so that all the nodes that is part of the swarm will answer requests on the exposed port, even if that container does not reside on that node.

For example, we can see that our container resides on worker-1 so we will get the Public IP of our manager node and our worker node, then make HTTP Requests on port 80 against the public IP's and you will see that the requests gets served.

Testing our Service:

On our Manager Node:

$ curl ip.ruanbekker.com
52.214.150.151

On our Worker Node:

[worker-1] $ curl ip.ruanbekker.com
34.253.158.130
```<p>

Now from a client instance outside our swarm, we will make an HTTP Request to our Manager and Worker Node IP:

```bash
[client] $ curl -XGET 52.214.150.151
Container Hostname: 4e6bd7a8a2bf , UUID: 771cc6bd-b669-4f68-83c6-cbb30ad01380[client] $

[client] $ curl -XGET 34.253.158.130
Container Hostname: 4e6bd7a8a2bf , UUID: c4f4115b-e848-4941-a399-b703afed249d[client] $

We can see we need to change the code so that it ends with a new line character

Updating our Application Code:

$ vi app.py

Change the code so that we add a new line character after our UUID:

from

return 'Container Hostname: ' + hostname + ' , ' + 'UUID: ' + str(randomid)

to:

return 'Container Hostname: ' + hostname + ' , ' + 'UUID: ' + str(randomid) + '\n'

Build the Image from the Updated Code, Push the Image to our Private Registry:

Now that our app is updated, we should build the image and push to our registry. We can apply a tag to our image like flask-app:v1 but sine we don't specify a tag, it will use the latest tag.

If you use docker-compose the tag should be set via the docker-compose.yml file, and if docker-compose is not used, you should set the tag via docker build option:

$ docker-compose build
$ docker-compose push
Pushing web (master:5000/flask-app:latest)...
The push refers to a repository [master:5000/flask-app]

Optionally, we can also build an image without docker-compose:

$ docker build -t flask-app .
$ docker tag flask-app master:5000/flask-app
$ docker push master:5000/flask-app
The push refers to a repository [master:5000/flask-app]

Lets make a Request to our API on our Private Registry to make sure we can see our repository:

$ curl master:5000/v2/_catalog
{"repositories":["flask-app"]}

Verify, if we can see the updated Image from our Private Repository:

$ docker images master:5000/flask-app
REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
master:5000/flask-app   latest              3253d71edfa4        17 minutes ago      93.3 MB
master:5000/flask-app   <none>              b7fd5b5e7892        51 minutes ago      93.3 MB

Update the Service:

Apply a rolling update to a service so that our application can be updated:

$ docker service update --image master:5000/flask-app:latest flask-demo

Make sure the service is in service:

$ docker service ls
ID            NAME        MODE        REPLICAS  IMAGE
5y520y6fau5j  flask-demo  replicated  1/1       master:5000/flask-app:latest
xfk2z5s2ybcg  registry    replicated  1/1       registry:2

If we run a docker service ps on our service name, we should see that the previous container was shut down, and a new container with the updated code has been spawned:

$ docker service ps flask-demo
ID            NAME              IMAGE                         NODE      DESIRED STATE  CURRENT STATE           ERROR  PORTS
ltvenwz4jidh  flask-demo.1      master:5000/flask-app:latest  worker-2  Running        Running 2 minutes ago
u7ab0xhwrlzh   \_ flask-demo.1  master:5000/flask-app:latest  worker-1  Shutdown       Shutdown 2 minutes ago

(Optionally:) Once again, you could logon to worker-2 to make sure we can see the container-id:

[worker-2] $ docker ps --filter "name=flask-demo" --format "table {{.ID}}\t{{.Names}}"
CONTAINER ID        NAMES
499b2a76da67        flask-demo.1.ltvenwz4jidhj7esc89ee6gzp

Testing our Updated Web Application:

Now let's make those same HTTP Requests, to see if the service was updated with the latest image:

[client] $ curl -XGET 52.214.150.151
Container Hostname: 499b2a76da67 , UUID: b1d534eb-e737-46ce-91e2-5800bf24a72e

[client] $ curl -XGET 52.214.150.151
Container Hostname: 499b2a76da67 , UUID: 6c47b0c5-7ffa-4501-a522-c430e420b12c

We can see its working, as the task got updated, running on a new container.

Scaling our Application:

Lets scale our application to 10 containers:

$ docker service scale flask-demo=10
flask-demo scaled to 10

Let's have a look at our replica count:

$ docker service ls
ID            NAME        MODE        REPLICAS  IMAGE
5y520y6fau5j  flask-demo  replicated  10/10     master:5000/flask-app:latest
xfk2z5s2ybcg  registry    replicated  1/1       registry:2

We can see our tasks is replicated and we could further see on which node they are running:

$ docker service ps flask-demo
ID            NAME              IMAGE                         NODE      DESIRED STATE  CURRENT STATE           ERROR  PORTS
ltvenwz4jidh  flask-demo.1      master:5000/flask-app:latest  worker-2  Running        Running 2 minutes ago
xyxi33keqdny   \_ flask-demo.1  master:5000/flask-app:latest  worker-1  Shutdown       Shutdown 2 minutes ago
u7ab0xhwrlzh   \_ flask-demo.1  master:5000/flask-app:latest  worker-1  Shutdown       Shutdown 6 minutes ago
yhnf8wyrimvb  flask-demo.2      master:5000/flask-app:latest  master    Running        Running 39 seconds ago
vxghtq86537m  flask-demo.3      master:5000/flask-app:latest  worker-1  Running        Running 37 seconds ago
f3p8ym23r4ir  flask-demo.4      master:5000/flask-app:latest  worker-1  Running        Running 37 seconds ago
q9hgb8z58ybd  flask-demo.5      master:5000/flask-app:latest  master    Running        Running 39 seconds ago
qdxef5d2bspt  flask-demo.6      master:5000/flask-app:latest  worker-1  Running        Running 37 seconds ago
a7wjn1lo9jbz  flask-demo.7      master:5000/flask-app:latest  worker-2  Running        Running 41 seconds ago
776oi10gyo32  flask-demo.8      master:5000/flask-app:latest  worker-1  Running        Running 37 seconds ago
u2mpr2t7aoj5  flask-demo.9      master:5000/flask-app:latest  master    Running        Running 39 seconds ago
kypazgcch5aa  flask-demo.10     master:5000/flask-app:latest  worker-2  Running        Running 41 seconds ago

More verification, viewing the containers per node where the containers are running on:

[master] $ docker ps --filter "name=flask-demo" --format "table {{.ID}}\t{{.Names}}"
CONTAINER ID        NAMES
9e6b28ba711f        flask-demo.9.u2mpr2t7aoj5c5nggr2k3dvjf
4c3edef5c69b        flask-demo.2.yhnf8wyrimvbg937dt6tkjwb0
5a052e37ef73        flask-demo.5.q9hgb8z58ybd07cztmiqnqogy

And on the worker-1 node:

[worker-1] $ docker ps --filter "name=flask-demo" --format "table {{.ID}}\t{{.Names}}"
CONTAINER ID        NAMES
499b2a76da67        flask-demo.6.qdxef5d2bspt2dtga4pdj6ab1
3bd92ade8242        flask-demo.4.f3p8ym23r4iredo7xqyt1yt7h
bd64090dc3f5        flask-demo.3.vxghtq86537mpxpo5t31zm1i4
3a5effab8827        flask-demo.8.776oi10gyo32h76czx7jqwkgf

Further, on the worker-2 node:

[worker-2] $ docker ps --filter "name=flask-demo" --format "table {{.ID}}\t{{.Names}}"
CONTAINER ID        NAMES
20e8148ab72f        flask-demo.10.kypazgcch5aaw4l04ha8r4elv
217c86d234de        flask-demo.7.a7wjn1lo9jbzv4p5hbgierp7g
a186ed70646c        flask-demo.1.ltvenwz4jidhj7esc89ee6gzp

As the service is replicated to multiple containers, our application should now serve the requests from different containers, as we can see is expected from the below output:

[client] $ curl -XGET 52.214.150.151
Container Hostname: 9e6b28ba711f , UUID: 16780535-8c5f-445a-a1dd-fdb10e07bc59

[client] $ curl -XGET 52.214.150.151
Container Hostname: 4c3edef5c69b , UUID: ea496a8f-b49d-470b-a0f4-e695810374aa

Let's make 10 requests, to see the hostname reporting from the containers:

$ for x in {1..10}; do curl -XGET 52.214.150.151; done

Container Hostname: 3a5effab8827 , UUID: 00e3454e-4d0c-48d9-96ec-e8875c893b36
Container Hostname: 9e6b28ba711f , UUID: bf6da863-94a3-445d-b386-25e290004ed2
Container Hostname: 4c3edef5c69b , UUID: 1a611f14-0cc6-4495-b246-5bb4db65a002
Container Hostname: 5a052e37ef73 , UUID: 684f00dc-a17c-418a-bfb7-d2bac50d4c6e
Container Hostname: 20e8148ab72f , UUID: 85696153-d3d5-4f7a-b663-eb4e0c199555
Container Hostname: 217c86d234de , UUID: df7d2bd8-4cae-4150-a825-754e84709724
Container Hostname: a186ed70646c , UUID: 88cc84fe-44f7-4d4d-9f67-9479d52688d7
Container Hostname: 499b2a76da67 , UUID: e4d643e0-10e6-452f-abc7-f712fdf07cdd
Container Hostname: 3bd92ade8242 , UUID: ce23f3b2-c55d-4533-b164-5f60b8bf3aa0
Container Hostname: bd64090dc3f5 , UUID: 9e2bb8c2-848e-49ff-99d8-dff47644b74b

Let's make 12 requests, so in theory, we should see 10 unique hostnames:

$ for x in {1..12}; do curl -XGET 52.214.150.151; done

Container Hostname: 3a5effab8827 , UUID: 84a874b9-3767-4b07-82da-ba2ede0e2240
Container Hostname: 9e6b28ba711f , UUID: 0e1a7736-e89b-4c30-9739-97ef7b8a203a
Container Hostname: 4c3edef5c69b , UUID: 455df8e9-b465-4b58-865e-56d708148c26
Container Hostname: 5a052e37ef73 , UUID: 7478f449-ec48-4378-8cd1-fa0ae3465f78
Container Hostname: 20e8148ab72f , UUID: 25dd11fa-c09d-437a-b4d8-a12b7933a245
Container Hostname: 217c86d234de , UUID: b4e26634-037a-4694-b849-896c0bc89946
Container Hostname: a186ed70646c , UUID: 3af5619d-bfa9-4ac7-8190-0f11876b33b1
Container Hostname: 499b2a76da67 , UUID: e38bd20d-c9e7-4b1e-9f1f-a25574207558
Container Hostname: 3bd92ade8242 , UUID: 3caec655-a4f8-4956-a5b0-00a68db99233
Container Hostname: bd64090dc3f5 , UUID: 2c8cfe74-42ff-4463-b069-03fb3be5f290
Container Hostname: 3a5effab8827 , UUID: bb84adea-1dee-4b57-aea5-e086e0a979ce
Container Hostname: 9e6b28ba711f , UUID: 72706b15-544b-4a3f-b66c-5e1d3a363cc7

Some bash scripting to count any duplicates:

$ for x in {1..10}; do curl -XGET 52.214.150.151; done >> run1.txt

$ for x in {1..12}; do curl -XGET 52.214.150.151; done >> run2.txt

Our first run (10 Requests):

[client] $ cat run1.txt | awk '{print $3}' | sort | uniq -c
      1 20e8148ab72f
      1 217c86d234de
      1 3a5effab8827
      1 3bd92ade8242
      1 499b2a76da67
      1 4c3edef5c69b
      1 5a052e37ef73
      1 9e6b28ba711f
      1 a186ed70646c
      1 bd64090dc3f5

Our second run (12 Requests):

[client] $ cat run2.txt | awk '{print $3}' | sort | uniq -c
      1 20e8148ab72f
      1 217c86d234de
      2 3a5effab8827
      1 3bd92ade8242
      1 499b2a76da67
      1 4c3edef5c69b
      1 5a052e37ef73
      2 9e6b28ba711f
      1 a186ed70646c
      1 bd64090dc3f5

Python Script to Randomly select one of the 3 docker hosts to do the HTTP GET Request, and return the Docker Host IP and the Container ID that was serving the response:

>>> import time
>>> import random
>>> import requests
>>> for x in xrange(20):
...     time.sleep(0.125)
...     docker_host = random.choice(['34.253.158.130', '34.251.150.142', '52.214.150.151'])
...     get_request = requests.get('http://' + str(docker_host)).content.split(' ')[2]
...     print("Host: {0}, Container: {1}".format(docker_host, get_request))
...
Host: 52.214.150.151, Container: 217c86d234de
Host: 52.214.150.151, Container: a186ed70646c
Host: 34.251.150.142, Container: bd64090dc3f5
Host: 34.251.150.142, Container: 3a5effab8827
Host: 52.214.150.151, Container: 499b2a76da67
Host: 52.214.150.151, Container: 3bd92ade8242
Host: 52.214.150.151, Container: bd64090dc3f5
Host: 52.214.150.151, Container: 3a5effab8827
Host: 34.253.158.130, Container: 9e6b28ba711f
Host: 52.214.150.151, Container: 9e6b28ba711f
Host: 52.214.150.151, Container: 4c3edef5c69b
Host: 34.251.150.142, Container: 4c3edef5c69b
Host: 34.251.150.142, Container: 9e6b28ba711f
Host: 34.253.158.130, Container: 4c3edef5c69b
Host: 34.253.158.130, Container: 5a052e37ef73
Host: 34.253.158.130, Container: 20e8148ab72f
Host: 34.251.150.142, Container: 5a052e37ef73
Host: 34.253.158.130, Container: 217c86d234de
Host: 52.214.150.151, Container: 5a052e37ef73
Host: 34.251.150.142, Container: 217c86d234de

Some Issues I Ran Into:

"server gave HTTP response to HTTPS client" issue:
- https://github.com/docker/distribution/issues/1874

To fix this issue, on each node create a /etc/docker/daemon.json file and populate the file with the following info:

cat /etc/docker/daemon.json
{ "insecure-registries":["master:5000"] }

Manager Scheduling Only:

We can set our manager to only serve requests and do scheduling, by draining the node, all containers will be moved from this node to the other nodes in the swarm:

$ docker node ls
ID                           HOSTNAME  STATUS  AVAILABILITY  MANAGER STATUS
j14mte3v1jhtbm3pb2qrpgwp6    worker-1  Ready   Active
siqyf3yricsvjkzvej00a9b8h *  master    Ready   Active        Leader
srl5yzme5hxnzxal2t1efmwje    worker-2  Ready   Active

After viewing our nodes in the swarm, we can select which node we want to drain

$ docker node update --availability drain manager
manager

To verify our change:

$ docker node ls
ID                           HOSTNAME  STATUS  AVAILABILITY  MANAGER STATUS
j14mte3v1jhtbm3pb2qrpgwp6    worker-1  Ready   Active
siqyf3yricsvjkzvej00a9b8h *  master    Ready   Drain         Leader
srl5yzme5hxnzxal2t1efmwje    worker-2  Ready   Active

Inspecting our Manager Node:

$ docker node inspect --pretty manager
ID:                     siqyf3yricsvjkzvej00a9b8h
Hostname:               master
Joined at:              2017-06-21 07:50:43.525020216 +0000 utc
Status:
 State:                 Ready
 Availability:          Drain
 Address:               172.31.18.90
Manager Status:
 Address:               172.31.18.90:2377
 Raft Status:           Reachable
 Leader:                Yes
Platform:
 Operating System:      linux
 Architecture:          x86_64
Resources:
 CPUs:                  1
 Memory:                3.673 GiB
Plugins:
  Network:              bridge, host, macvlan, null, overlay
  Volume:               local
Engine Version:         17.03.1-ce

And now we can see that our containers is only running on the worker nodes:

$ docker service ps flask-demo | grep Runn
lig9wj70c5m8  flask-demo.1      master:5000/newapp:v1      worker-1  Running        Running 5 minutes ago
zmmzcm6xxzzo  flask-demo.2      master:5000/newapp:v1      worker-2  Running        Running 25 seconds ago
a292huwkzr23  flask-demo.3      master:5000/newapp:v1      worker-2  Running        Running 5 minutes ago
uuhbswsdvziq  flask-demo.4      master:5000/newapp:v1      worker-1  Running        Running 4 minutes ago
x4vrrs6fgpqz  flask-demo.5      master:5000/newapp:v1      worker-2  Running        Running 25 seconds ago
...

Deleting the Service:

$ docker service rm flask-demo

Deleting the Network:

$ docker network rm mynet

Leaving the Swarm:

To remove nodes from the swarm, run the following from the nodes:

[worker-1] $ docker swarm leave
[worker-2] $ docker swarm leave

Then we can list our nodes to get the nodes that we want to remove from the manager:

[master] $ docker node ls
ID                           HOSTNAME  STATUS  AVAILABILITY  MANAGER STATUS
j14mte3v1jhtbm3pb2qrpgwp6    worker-1  Down    Active
siqyf3yricsvjkzvej00a9b8h *  master    Ready   Drain         Leader
srl5yzme5hxnzxal2t1efmwje    worker-2  Down    Active

And removing the nodes from the managers list:

$ docker node rm worker-1
worker-1

$ docker node rm worker-2
worker-2

$ docker node ls
ID                           HOSTNAME  STATUS  AVAILABILITY  MANAGER STATUS
siqyf3yricsvjkzvej00a9b8h *  master    Ready   Drain         Leader

Resources:

I hope this was useful, and I will continue writing some more content on Docker Swarm :D