Create and Verify a Docker Swarm Service from the CLI

docker service create looks like docker run with extra flags, until you realise it asks the whole cluster to keep tasks alive. This creates a two-replica Nginx service on a Swarm manager, confirms the tasks converge, checks the published port, and removes the test service cleanly. Docker Community CLI 29.8.1, packaged here as docker-ce-cli 5:29.8.1-1~ubuntu.24.04~noble.

Allow about fifteen minutes. You need Docker Engine access, an initialised Swarm, and a shell on a manager node. The examples create a real service and may pull an image, so use a test name and, on a shared cluster, a maintenance window. Docker commands normally need no sudo when your account can reach the socket; add it only if your local setup requires it.

1. Check the command and Swarm state

Start with read-only checks. Neither creates a container nor changes cluster state:

$ docker version --format '{{.Client.Version}}'
29.8.1
$ docker service create --help | sed -n '1,12p'
Usage:  docker service create [OPTIONS] IMAGE [COMMAND] [ARG...]

Create a new service
$ docker info --format 'swarm={{.Swarm.LocalNodeState}} control={{.Swarm.ControlAvailable}}'
swarm=active control=true

The last line has to name an active manager. The installed help is explicit about the required shape: options, an image, then an optional command and its arguments. Fix a worker, an inactive Swarm, a stopped daemon or a socket permission error before you go further. Do not run docker swarm init on a machine meant to join an existing cluster: initialising a new Swarm is a separate, disruptive decision that does not belong in this guide.

2. Choose a disposable service definition

Use the public nginx:alpine image, two replicated tasks, and a host port unlikely to be taken. The service name is what every later inspection and removal command will use to find it:

$ SERVICE_NAME='demo-web'
$ HOST_PORT='8080'
$ docker service create \
    --name "$SERVICE_NAME" \
    --replicas 2 \
    --publish published="$HOST_PORT",target=80 \
    nginx:alpine

This is the first state-changing command. It asks the manager to maintain two tasks continuously, not just start two containers once, in the default replicated mode. Port 8080 on every Swarm node now points at port 80 inside the service tasks. Docker prints a service ID, but keeping the name in a shell variable saves you copying that ID through the rest of this walkthrough.

Warning: a published port affects the whole cluster's routing mesh. Stop here if port 8080 is already allocated, or if an external firewall, load balancer or production service already uses it. Change HOST_PORT to an approved unused port before you create the service.

3. Verify the desired service state

List the service by name and check the replica count:

$ docker service ls --filter "name=$SERVICE_NAME"
ID            NAME      MODE        REPLICAS   IMAGE         PORTS
...           demo-web  replicated  2/2        nginx:alpine  *:8080->80/tcp

The ID is shortened here and will differ on your cluster. 2/2 is the checkpoint that matters. A value like 0/2 means the manager has the desired state recorded but has not started both tasks yet: give an image pull or scheduling step time to settle, then inspect the tasks rather than recreating the service out of impatience.

$ docker service ps "$SERVICE_NAME" \
    --format 'table {{.Name}}\t{{.Node}}\t{{.CurrentState}}\t{{.Error}}'
NAME         NODE        CURRENT STATE             ERROR
demo-web.1   manager-1   Running 10 seconds ago
demo-web.2   worker-1    Running 10 seconds ago

Task names, nodes and times will vary. Both should reach Running. If a task keeps getting replaced, read the final column and the full task output: an unavailable image, a port conflict, no node satisfying a constraint, or a command that exits immediately are the usual causes. A service can exist quite happily while its tasks stay pending or failed.

4. Inspect the configuration you actually created

Check the manager's recorded specification rather than trusting what you remember typing:

$ docker service inspect "$SERVICE_NAME" \
    --format 'name={{.Spec.Name}} replicas={{.Spec.Mode.Replicated.Replicas}}'
name=demo-web replicas=2
$ docker service inspect "$SERVICE_NAME" \
    --format '{{range .Endpoint.Ports}}{{.PublishedPort}}->{{.TargetPort}}/{{.Protocol}}{{"\n"}}{{end}}'
8080->80/tcp

These read the service specification and endpoint, not one container's settings. The service uses the virtual IP endpoint mode by default; if you need DNS round-robin instead, create a separate test service with --endpoint-mode dnsrr rather than changing a live one just to check.

Test the published endpoint from a machine that can actually reach the node and port:

$ curl --fail --silent --show-error "http://127.0.0.1:$HOST_PORT/" \
    | sed -n '1,3p'
<!DOCTYPE html>
<html>

The response body varies with the image version. A connection failure alone does not prove the service is unhealthy: check the node address, firewall, routing mesh settings and task states separately before you conclude anything.

5. Remove the test service

Warning: removal is deliberate and irreversible for this service definition. It stops and deletes the tasks and removes the service record. Confirm the name before running it:

$ docker service ls --filter "name=$SERVICE_NAME"
ID            NAME      MODE        REPLICAS   IMAGE         PORTS
...           demo-web  replicated  2/2        nginx:alpine  *:8080->80/tcp
$ docker service rm "$SERVICE_NAME"
demo-web
$ docker service ls --filter "name=$SERVICE_NAME"
ID   NAME   MODE   REPLICAS   IMAGE   PORTS

There is no undo for docker service rm. Recreate the test by rerunning the creation command and waiting for the image and tasks to converge. Removing the service does not touch unrelated images, named volumes, overlay networks or Swarm nodes: inspect and remove those separately if you need to.

Common traps and useful options

Done means