Docker – Ubuntu a refresher

Saksham

Usually I use Centos, but this time I wanted to try out docker swarm and ubuntu was the preferred option due to the options and supporting blogs available.

Do read – https://docs.docker.com/get-started/#containers-and-virtual-machines

docker.com

Step 1

Updating the system and installing required dependencies

sudo apt-get update

sudo apt-get -y install \
  apt-transport-https \
  ca-certificates \
  curl \
  gnupg-agent \
  software-properties-common

The version of Ubuntu I am using is as under

> cat /etc/issue
Ubuntu 18.04.4 LTS \n \l

Add the GPG key and repository. Finally install docker community edition

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

sudo apt-key fingerprint 0EBFCD88

sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"

sudo apt-get update

sudo apt-get install -y docker-ce=5:18.09.5~3-0~ubuntu-bionic docker-ce-cli=5:18.09.5~3-0~ubuntu-bionic containerd.io
docker info
Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 0
Server Version: 18.09.5
Storage Driver: overlay2
 Backing Filesystem: extfs
 Supports d_type: true
 Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
 Volume: local
 Network: bridge host macvlan null overlay
 Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
Swarm: inactive
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 7ad184331fa3e55e52b890ea95e65ba581ae3429
runc version: dc9208a3303feef5b3839f4323d9beb36df0a9dd
init version: fec3683
Security Options:
 apparmor
 seccomp
  Profile: default
Kernel Version: 5.3.0-1023-aws
Operating System: Ubuntu 18.04.4 LTS
OSType: linux
Architecture: x86_64
CPUs: 2
Total Memory: 3.798GiB
Name: samarthya2c.mylabserver.com
ID: R4OG:F2YR:VRTY:XFRM:4QXX:JWN6:Y2YS:OA5M:7YCJ:OMFR:BXP5:HKXO
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): false
Registry: https://index.docker.io/v1/
Labels:
Experimental: false
Insecure Registries:
 127.0.0.0/8
Live Restore Enabled: false
Product License: Community Engine

WARNING: No swap limit support

docker version

Now time to check the version. docker version


Client:
 Version:           18.09.5
 API version:       1.39
 Go version:        go1.10.8
 Git commit:        e8ff056
 Built:             Thu Apr 11 04:43:57 2019
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          18.09.5
  API version:      1.39 (minimum version 1.12)
  Go version:       go1.10.8
  Git commit:       e8ff056
  Built:            Thu Apr 11 04:10:53 2019
  OS/Arch:          linux/amd64
  Experimental:     false

Step 2

Time to install docker swarm.

docker swarm init

On the machine that you use the command above it will be the designated swarm manager.

Options – For Init

docker swarm init --help

Usage:	docker swarm init [OPTIONS]

Initialize a swarm

Options:
      --advertise-addr string                  Advertised address (format: <ip|interface>[:port])
      --autolock                               Enable manager autolocking (requiring an unlock key to start a stopped manager)
      --availability string                    Availability of the node ("active"|"pause"|"drain") (default "active")
      --cert-expiry duration                   Validity period for node certificates (ns|us|ms|s|m|h) (default 2160h0m0s)
      --data-path-addr string                  Address or interface to use for data path traffic (format: <ip|interface>)
      --default-addr-pool ipNetSlice           default address pool in CIDR format (default [])
      --default-addr-pool-mask-length uint32   default address pool subnet mask length (default 24)
      --dispatcher-heartbeat duration          Dispatcher heartbeat period (ns|us|ms|s|m|h) (default 5s)
      --external-ca external-ca                Specifications of one or more certificate signing endpoints
      --force-new-cluster                      Force create a new cluster from current state
      --listen-addr node-addr                  Listen address (format: <ip|interface>[:port]) (default 0.0.0.0:2377)
      --max-snapshots uint                     Number of additional Raft snapshots to retain
      --snapshot-interval uint                 Number of log entries between Raft snapshots (default 10000)
      --task-history-limit int                 Task history retention limit (default 5)

If you are deploying it on a cloud or a hosted VM, you need to be cautious of which ports are accessible to the nodes who will be joining in the swarm.

For my case I have a public and private ip that my machine has, and I will be using the private ip for the option --advertise-addr and that has no restrictions in terms of port.

docker swarm init --advertise-addr 172.31.0.1
Swarm initialized: current node (a0zuhhhc0mjnm322gadntwsrwe) is now a manager.

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

    docker swarm join --token SWMTKN-1-417pmtvy5vtzui4lmf023jhr4q50tekzzgsf8evgy4emtykvs4-exobuyup2mro23r32aursfzxw 172.31.0.1:2377

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

docker info should return some information about the swarm

Swarm: active
 NodeID: a0zuhhhc0mjnm322gadntwsrwe
 Is Manager: true
 ClusterID: uym09dgerjhhkasf
 Managers: 1
 Nodes: 1
 Default Address Pool: 100.0.0.0/8  

Namespace isolation provides a secure environment.

linux

docker node ls is another command that can be used to see the nodes in the swarm.

Once the docker swarm manager is ready you can add slave nodes to the swarm.

In the slave nodes wherever we have deployed docker and is supposed to work as the slave node, you need to fire the command.

docker swarm join --token SWMTKN-1-417pmtvy5vtzui4lmf023jhr4q50tekzzgsf8evgy4emtykvs4-exobuyup2mro23r32aursfzxw 172.31.0.1:2377

If it is successful, the following output will be shown.

This node joined a swarm as a worker

If you run docker node ls again, you will be see the nodes in the swarm

docker node ls
ID                            HOSTNAME                      STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
mauqklyumsaqs     samarthya1c.samarthya.com   Ready               Active                                  18.09.5
kilop2sdhc0m *   samarthya2c.samarthya.com   Ready               Active              Leader              18.09.5
vg890hdgd333     samarthya3c.samarthya.com   Ready               Active                                  18.09.5
csl8igcknj28c     samarthya4c.samarthya.com   Ready               Active                                  18.09.5

You can see the designated leader in the swarm. The leader assigns work to the worker nodes.

You can output the join token in master/manager node using

docker swarm join-token worker
root@master>docker swarm join-token worker
To add a worker to this swarm, run the following command:

    docker swarm join \
    --token SWMTKN-1-21wvlfoixago56zx0dbpznq8la36he9blrdyoyhosm4u02kvzk-bltrqgg1qiyrnr48xs1g5sybn \
    172.31.0.1:2377

Flattening the layers – Docker

In this section we will be using directives instructions used in Dockerfile to create an image and flatten it to a single image instead of multiple.

Docker can build images automatically by reading the instructions from a Dockerfile.

Consider a small docker container created as under

FROM golang:1.13.11 AS builder
WORKDIR /helloworld
COPY goworld.go .
RUN GOOS=linux go build -a -installsuffix cgo -o goworld .

FROM alpine:3.9.3
WORKDIR /root
COPY --from=builder /helloworld/goworld .
CMD ["./goworld"]

It just creates compiles a hello world go lang program and then executes it to print the output

package main
import ("fmt")

func main() {
  fmt.Println(" Hello World!")
}

If I look at the layers of the image

[
    {
        "Id": "sha256:2be60e641422227cd8caeb65894f66c63d08c514019e296dc32aae7f935f3076",
        "RepoTags": [
            "efficient:latest"
        ],
        "RepoDigests": [],
        "Parent": "sha256:192c03c1f3fa07ed20b20b2b4f73a60fcb42dc6d910e47b13b2cd0e060961452",
        "Comment": "",
        "Created": "2020-07-11T18:35:55.800402558Z",
        "Container": "b0a931821260361263ce15240238476813e7a84d3a32b9e773dcab24f956ce3f",
        "ContainerConfig": {
            "Hostname": "b0a931821260",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
            ],
            "Cmd": [
                "/bin/sh",
                "-c",
                "#(nop) ",
                "CMD [\"./goworld\"]"
            ],
            "Image": "sha256:192c03c1f3fa07ed20b20b2b4f73a60fcb42dc6d910e47b13b2cd0e060961452",
            "Volumes": null,
            "WorkingDir": "/root",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": {}
        },
        "DockerVersion": "19.03.12",
        "Author": "",
        "Config": {
            "Hostname": "",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
            ],
            "Cmd": [
                "./goworld"
            ],
            "Image": "sha256:192c03c1f3fa07ed20b20b2b4f73a60fcb42dc6d910e47b13b2cd0e060961452",
            "Volumes": null,
            "WorkingDir": "/root",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": null
        },
        "Architecture": "amd64",
        "Os": "linux",
        "Size": 7541995,
        "VirtualSize": 7541995,
        "GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/2f6641cf733d78bc0ad8d6f0bad7c4436f01bf978bed3f1a9b41d7ec06af0244/diff",
                "MergedDir": "/var/lib/docker/overlay2/3d38a55e5cd0e6dc257dd9ef2aa90b5d2b8a98ae55937a6ce37edf821c893e95/merged",
                "UpperDir": "/var/lib/docker/overlay2/3d38a55e5cd0e6dc257dd9ef2aa90b5d2b8a98ae55937a6ce37edf821c893e95/diff",
                "WorkDir": "/var/lib/docker/overlay2/3d38a55e5cd0e6dc257dd9ef2aa90b5d2b8a98ae55937a6ce37edf821c893e95/work"
            },
            "Name": "overlay2"
        },
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:a464c54f93a9e88fc1d33df1e0e39cca427d60145a360962e8f19a1dbf900da9",
                "sha256:7bf030eb44f162731c1d17a4a3d5af128818f938997709309c13653780c15cd6"
            ]
        },
        "Metadata": {
            "LastTagTime": "2020-07-12T08:28:11.850260882Z"
        }
    }
]

We will flatten it to

[
    {
        "Id": "sha256:b9038e8380fe786207d091bd9e313e867198d44f985d149685bcb6c6fbbf333b",
        "RepoTags": [
            "flat:latest"
        ],
        "RepoDigests": [],
        "Parent": "",
        "Comment": "Imported from -",
        "Created": "2020-07-12T09:04:40.856766368Z",
        "Container": "",
        "ContainerConfig": {
            "Hostname": "",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": null,
            "Cmd": null,
            "Image": "",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": null
        },
        "DockerVersion": "19.03.12",
        "Author": "",
        "Config": {
            "Hostname": "",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": null,
            "Cmd": null,
            "Image": "",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": null
        },
        "Architecture": "amd64",
        "Os": "linux",
        "Size": 7541906,
        "VirtualSize": 7541906,
        "GraphDriver": {
            "Data": {
                "MergedDir": "/var/lib/docker/overlay2/efd29f53d69a47bc7b983c9da58a7985afaf3b354800c13da8fd3ef0e7f8dea6/merged",
                "UpperDir": "/var/lib/docker/overlay2/efd29f53d69a47bc7b983c9da58a7985afaf3b354800c13da8fd3ef0e7f8dea6/diff",
                "WorkDir": "/var/lib/docker/overlay2/efd29f53d69a47bc7b983c9da58a7985afaf3b354800c13da8fd3ef0e7f8dea6/work"
            },
            "Name": "overlay2"
        },
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:3aa6e339ec7bf27f61646f33b8532bff0de532c8072847f7e1d268ad51e0e358"
            ]
        },
        "Metadata": {
            "LastTagTime": "2020-07-12T09:04:40.859989787Z"
        }
    }
]

Docker Service

To deploy an image when docker is in swarm mode you can use docker service. When service is created, you specify which container image to use and which commands to execute inside running containers.

Refer to more information here

Minimum version required to use docker service is 1.24.

docker service create --help

Usage:  docker service create [OPTIONS] IMAGE [COMMAND] [ARG...]

Create a new service

Options:
      --config config                      Specify configurations to expose to the service
      --constraint list                    Placement constraints
      --container-label list               Container labels
      --credential-spec credential-spec    Credential spec for managed service account (Windows only)
  -d, --detach                             Exit immediately instead of waiting for the service to converge
      --dns list                           Set custom DNS servers
      --dns-option list                    Set DNS options
      --dns-search list                    Set custom DNS search domains
      --endpoint-mode string               Endpoint mode (vip or dnsrr) (default "vip")
      --entrypoint command                 Overwrite the default ENTRYPOINT of the image
  -e, --env list                           Set environment variables
      --env-file list                      Read in a file of environment variables
      --generic-resource list              User defined resources
      --group list                         Set one or more supplementary user groups for the container
      --health-cmd string                  Command to run to check health
      --health-interval duration           Time between running the check (ms|s|m|h)
      --health-retries int                 Consecutive failures needed to report unhealthy
      --health-start-period duration       Start period for the container to initialize before counting retries towards unstable (ms|s|m|h)
      --health-timeout duration            Maximum time to allow one check to run (ms|s|m|h)
      --host list                          Set one or more custom host-to-IP mappings (host:ip)
      --hostname string                    Container hostname
      --init                               Use an init inside each service container to forward signals and reap processes
      --isolation string                   Service container isolation mode
  -l, --label list                         Service labels
      --limit-cpu decimal                  Limit CPUs
      --limit-memory bytes                 Limit Memory
      --log-driver string                  Logging driver for service
      --log-opt list                       Logging driver options
      --mode string                        Service mode (replicated or global) (default "replicated")
      --mount mount                        Attach a filesystem mount to the service
      --name string                        Service name
      --network network                    Network attachments
      --no-healthcheck                     Disable any container-specified HEALTHCHECK
      --no-resolve-image                   Do not query the registry to resolve image digest and supported platforms
      --placement-pref pref                Add a placement preference
  -p, --publish port                       Publish a port as a node port
  -q, --quiet                              Suppress progress output
      --read-only                          Mount the container's root filesystem as read only
      --replicas uint                      Number of tasks
      --reserve-cpu decimal                Reserve CPUs
      --reserve-memory bytes               Reserve Memory
      --restart-condition string           Restart when condition is met ("none"|"on-failure"|"any") (default "any")
      --restart-delay duration             Delay between restart attempts (ns|us|ms|s|m|h) (default 5s)
      --restart-max-attempts uint          Maximum number of restarts before giving up
      --restart-window duration            Window used to evaluate the restart policy (ns|us|ms|s|m|h)
      --rollback-delay duration            Delay between task rollbacks (ns|us|ms|s|m|h) (default 0s)
      --rollback-failure-action string     Action on rollback failure ("pause"|"continue") (default "pause")
      --rollback-max-failure-ratio float   Failure rate to tolerate during a rollback (default 0)
      --rollback-monitor duration          Duration after each task rollback to monitor for failure (ns|us|ms|s|m|h) (default 5s)
      --rollback-order string              Rollback order ("start-first"|"stop-first") (default "stop-first")
      --rollback-parallelism uint          Maximum number of tasks rolled back simultaneously (0 to roll back all at once) (default 1)
      --secret secret                      Specify secrets to expose to the service
      --stop-grace-period duration         Time to wait before force killing a container (ns|us|ms|s|m|h) (default 10s)
      --stop-signal string                 Signal to stop the container
  -t, --tty                                Allocate a pseudo-TTY
      --update-delay duration              Delay between updates (ns|us|ms|s|m|h) (default 0s)
      --update-failure-action string       Action on update failure ("pause"|"continue"|"rollback") (default "pause")
      --update-max-failure-ratio float     Failure rate to tolerate during an update (default 0)
      --update-monitor duration            Duration after each task update to monitor for failure (ns|us|ms|s|m|h) (default 5s)
      --update-order string                Update order ("start-first"|"stop-first") (default "stop-first")
      --update-parallelism uint            Maximum number of tasks updated simultaneously (0 to update all at once) (default 1)
  -u, --user string                        Username or UID (format: <name|uid>[:<group|gid>])
      --with-registry-auth                 Send registry authentication details to swarm agents
  -w, --workdir string                     Working directory inside the container

Simply using docker service create

docker service create --name mynginx nginx

the parameter --name defines the service name and the last param is the image to be used for the service.

docker service ls
ID                  NAME                  MODE                REPLICAS            IMAGE                                         PORTS
tp2szq0s5hbi        mynginx               replicated          1/1                 nginx:latest                                  

See I have not exposed any port, and replicas are also only one.

How can I scale it?

docker service scale mynginx=3
mynginx scaled to 3
overall progress: 3 out of 3 tasks 
1/3: running   [==================================================>] 
2/3: running   [==================================================>] 
3/3: running   [==================================================>] 
verify: Service converged 

How should I update to expose a port

docker service update --help

Usage:  docker service update [OPTIONS] SERVICE

Update a service

Options:
      --args command                       Service command args
      --config-add config                  Add or update a config file on a service
      --config-rm list                     Remove a configuration file
      --constraint-add list                Add or update a placement constraint
      --constraint-rm list                 Remove a constraint
      --container-label-add list           Add or update a container label
      --container-label-rm list            Remove a container label by its key
      --credential-spec credential-spec    Credential spec for managed service account (Windows only)
  -d, --detach                             Exit immediately instead of waiting for the service to converge
      --dns-add list                       Add or update a custom DNS server
      --dns-option-add list                Add or update a DNS option
      --dns-option-rm list                 Remove a DNS option
      --dns-rm list                        Remove a custom DNS server
      --dns-search-add list                Add or update a custom DNS search domain
      --dns-search-rm list                 Remove a DNS search domain
      --endpoint-mode string               Endpoint mode (vip or dnsrr)
      --entrypoint command                 Overwrite the default ENTRYPOINT of the image
      --env-add list                       Add or update an environment variable
      --env-rm list                        Remove an environment variable
      --force                              Force update even if no changes require it
      --generic-resource-add list          Add a Generic resource
      --generic-resource-rm list           Remove a Generic resource
      --group-add list                     Add an additional supplementary user group to the container
      --group-rm list                      Remove a previously added supplementary user group from the container
      --health-cmd string                  Command to run to check health
      --health-interval duration           Time between running the check (ms|s|m|h)
      --health-retries int                 Consecutive failures needed to report unhealthy
      --health-start-period duration       Start period for the container to initialize before counting retries towards unstable (ms|s|m|h)
      --health-timeout duration            Maximum time to allow one check to run (ms|s|m|h)
      --host-add list                      Add a custom host-to-IP mapping (host:ip)
      --host-rm list                       Remove a custom host-to-IP mapping (host:ip)
      --hostname string                    Container hostname
      --image string                       Service image tag
      --init                               Use an init inside each service container to forward signals and reap processes
      --isolation string                   Service container isolation mode
      --label-add list                     Add or update a service label
      --label-rm list                      Remove a label by its key
      --limit-cpu decimal                  Limit CPUs
      --limit-memory bytes                 Limit Memory
      --log-driver string                  Logging driver for service
      --log-opt list                       Logging driver options
      --mount-add mount                    Add or update a mount on a service
      --mount-rm list                      Remove a mount by its target path
      --network-add network                Add a network
      --network-rm list                    Remove a network
      --no-healthcheck                     Disable any container-specified HEALTHCHECK
      --no-resolve-image                   Do not query the registry to resolve image digest and supported platforms
      --placement-pref-add pref            Add a placement preference
      --placement-pref-rm pref             Remove a placement preference
      --publish-add port                   Add or update a published port
      --publish-rm port                    Remove a published port by its target port
  -q, --quiet                              Suppress progress output
      --read-only                          Mount the container's root filesystem as read only
      --replicas uint                      Number of tasks
      --reserve-cpu decimal                Reserve CPUs
      --reserve-memory bytes               Reserve Memory
      --restart-condition string           Restart when condition is met ("none"|"on-failure"|"any")
      --restart-delay duration             Delay between restart attempts (ns|us|ms|s|m|h)
      --restart-max-attempts uint          Maximum number of restarts before giving up
      --restart-window duration            Window used to evaluate the restart policy (ns|us|ms|s|m|h)
      --rollback                           Rollback to previous specification
      --rollback-delay duration            Delay between task rollbacks (ns|us|ms|s|m|h)
      --rollback-failure-action string     Action on rollback failure ("pause"|"continue")
      --rollback-max-failure-ratio float   Failure rate to tolerate during a rollback
      --rollback-monitor duration          Duration after each task rollback to monitor for failure (ns|us|ms|s|m|h)
      --rollback-order string              Rollback order ("start-first"|"stop-first")
      --rollback-parallelism uint          Maximum number of tasks rolled back simultaneously (0 to roll back all at once)
      --secret-add secret                  Add or update a secret on a service
      --secret-rm list                     Remove a secret
      --stop-grace-period duration         Time to wait before force killing a container (ns|us|ms|s|m|h)
      --stop-signal string                 Signal to stop the container
  -t, --tty                                Allocate a pseudo-TTY
      --update-delay duration              Delay between updates (ns|us|ms|s|m|h)
      --update-failure-action string       Action on update failure ("pause"|"continue"|"rollback")
      --update-max-failure-ratio float     Failure rate to tolerate during an update
      --update-monitor duration            Duration after each task update to monitor for failure (ns|us|ms|s|m|h)
      --update-order string                Update order ("start-first"|"stop-first")
      --update-parallelism uint            Maximum number of tasks updated simultaneously (0 to update all at once)
  -u, --user string                        Username or UID (format: <name|uid>[:<group|gid>])
      --with-registry-auth                 Send registry authentication details to swarm agents
  -w, --workdir string                     Working directory inside the container

Adding 8082 as a published port to expose the port 80 of nginx.

docker service update mynginx --publish-add 8082:80
mynginx
overall progress: 3 out of 3 tasks 
1/3: running   [==================================================>] 
2/3: running   [==================================================>] 
3/3: running   [==================================================>] 
verify: Service converged 
docker service ls
ID                  NAME                  MODE                REPLICAS            IMAGE                                         PORTS
tp2szq0s5hbi        mynginx               replicated          3/3                 nginx:latest                                  *:8082->80/tcp

Simple check

curl localhost:8082

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

When running Docker Engine in swarm mode, you can use docker stack deploy to deploy a complete application stack to the swarm. It accepts instructions form a compose file.

The Compose file is a YAML file defining servicesnetworks and volumes

Supports version 3 and upwards.

Docker services allows replication of a single service across nodes in a docker swarm, and Docker stack allows more deployment of more complex application of interrelated services and to be scaled as a unit.

Helpful link

Next, I might look into Helm.

Helm is the package manager for the Kubernetes  & in this approach, Kubernetes could be considered as an operating system.