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.
The route
Jump straight to the step you need, or tick off Done means at the end.
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
- Keep the image and command in order.
alpine ping docker.comuses the Alpine filesystem and runsping docker.com; put the command before the image and Docker usually errors out. - Use the right channel for the right data.
--env KEY=valuefor ordinary configuration,--secretfor sensitive values,--configfor non-secret configuration files. Never put a password in a label, environment variable or a visible command example. - Treat --with-registry-auth as a security decision. It sends registry authentication to Swarm agents; use it only when private-image pulls actually require it.
- Separate placement from limits.
--constraintrestricts which nodes can run a task;--reserve-memoryaffects placement,--limit-memoryconstrains the task afterwards. - Know the defaults. Replicated mode is the default,
--replicas-max-per-nodedefaults to unlimited, and the restart condition defaults toanywith a five-second delay. - Pick the right mode.
--mode globalfor one task per node instead of a large replica count;--mode replicated-jobor--mode global-job(with--max-concurrentfor replicated jobs) for a finite operation with different update and completion behaviour.
Done means
- Confirmed Docker CLI 29.8.1 and an active manager node.
- Created a named, two-replica service with an approved published port.
- Checked
docker service lsshows the desired replica count. - Verified
docker service psshows both tasks running, or exposed a specific error. - Matched
docker service inspectagainst the intended replicas and port mapping. - Removed the disposable service and confirmed it no longer appears.