Choose the right container orchestration. Covers Kubernetes vs ECS vs Nomad, when not to use K8s, Docker Compose for simple workloads, and right-sizing your orchestration choice.
This page is for engineers who need to run containers in production without a Kubernetes control plane. It matters when you’re on constrained hardware, evaluating edge deployments, migrating off an existing platform, or simply avoiding the operational overhead of a full K8s stack. The patterns below are not tied to any specific vendor’s implementation; they describe the mechanics you’ll find across runtimes that expose a container orchestration interface, and the concrete commands you can run today with tools that ship out of the box.
Most “beyond Kubernetes” orchestration starts with either a built‑in mode in the container engine or a lightweight scheduler. Docker Engine, when started with the experimental flag or on newer releases, includes Swarm mode. Other runtimes rely on a CRI‑compatible client or a standalone scheduler.
Docker Swarm initialization is a single command that turns a host into a manager and exposes the Docker API for cluster‑wide operations:
docker swarm init --advertise-addr 10.0.1.42
The output gives a join token that other nodes can use:
docker swarm join --token <manager-token> 10.0.1.42:2377
Once the cluster is up, docker commands are automatically routed to the manager. If you prefer a non‑Docker scheduler, crictl works against any CRI‑compatible runtime (containerd, CRI‑O, etc.) and provides a kubectl‑like CLI for inspecting and scheduling containers:
crictl ps -a
crictl pull docker.io/library/nginx:latest
For completely raw deployments, you can script docker run loops over a list of hosts, but you’ll quickly re‑implement the basics that Swarm or a scheduler gives you for free: desired‑state tracking, restart handling, and network scoping.
The core workflow in any orchestration system is moving from a container image to a running service with a specified replica count, then updating that service without downtime.
In Docker Swarm, a service is the desired‑state abstraction:
docker service create \
--name web \
--replicas 4 \
--network mynet \
--publish 80:8080 \
docker.io/library/nginx:stable
The --replicas flag sets the target number of tasks. The orchestrator spreads them across healthy nodes, respects placement constraints, and restarts failed tasks according to the --restart-policy (default: on-failure).
Rolling updates replace the container image while preserving availability. The following command upgrades the service to a new tag, pausing between batches to let health checks pass:
docker service update \
--image docker.io/library/nginx:new \
--update-parallelism 2 \
--update-delay 30s \
web
If something goes wrong, the rollback is immediate:
docker service update --rollback web
A common silent failure occurs when a service has no restart policy and a container exits with a non‑zero code: the task disappears from docker ps output, and the orchestrator does not recreate it. Another frequent confusion is between docker service scale and docker service update --replicas. Both adjust the replica count, but scale takes a bare number while update --replicas accepts the N=M syntax and is the preferred method in scripts.
Orchestrated services need to find each other without hard‑coding IP addresses. Swarm provides an embedded DNS server (docker‑engine‑dns) that resolves service names to the virtual IP of the task mesh. A service named db becomes resolvable as db.mynet from any other task in the same overlay network.
Creating an overlay network that spans the swarm:
docker network create --driver overlay --subnet 10.0.5.0/24 mynet
If you’re not using Swarm, you’ll typically fall back to environment‑variable injection or a sidecar DNS resolver. Many runtimes expose the container’s IP via cgroups or crictl:
crictl inspecti <container-id> | jq '.[0].NetworkSettings.IPAddress'
Port mapping in Swarm is published at the swarm‑manager level; incoming traffic is distributed across replicas using a round‑robin virtual IP. Outside the overlay, you can attach a host‑port publish (--publish 80:8080) and rely on the underlying load balancer or cloud NAT to route traffic.
A quietly failing pattern is relying on Docker’s legacy --link flag for inter‑container communication. It has been deprecated since Docker 1.10 and is removed in recent releases. The recommended pattern is user‑defined networks with explicit DNS resolution.
Without K8s’ built‑in log aggregation, you must decide how stdout/stderr from container tasks gets collected. Docker’s --log-driver flag directs output to a local file, journald, or a remote syslog endpoint:
docker service create \
--log-driver json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
--name collector \
alpine:latest sh -c "while true; do echo \"$(date) hello\"; sleep 1; done"
For structured querying, docker logs <service>/<task> streams the captured output, and crictl logs <container> does the same against a CRI runtime.
Metrics follow a similar pattern. The --metrics-store flag is not a universal Docker flag, but third‑party exporters (cAdvisor, Prometheus‑exporters) can be run as services and scraped by whatever metric collector you have. A common edge case: when max-size rotation occurs, in‑flight log lines may be truncated or lost if the container writes directly to the file descriptor without flushing. Always pair log rotation with a graceful shutdown hook or ensure the process calls fflush(stdout) periodically.
Orchestration does not solve storage by itself. Docker volumes survive task recreation when named at service creation time:
docker volume create pgdata
docker service create \
--name postgres \
--mount type=volume,source=pgdata,destination=/var/lib/postgresql/data \
postgres:15
If the volume driver is not specified, Docker uses the host’s default driver (usually local). In multi‑node swarms, local volumes are not shared; you’ll need a distributed driver like Rexray, Portworx, or a cloud‑native CSI integration.
For databases or stateful workloads that cannot run in‑memory, the pattern is to externalize the state: run the database on a dedicated host or managed service, and mount its network‑exported path as a read‑only or read‑write volume depending on the access pattern. A silent failure mode is assuming that a volume created on the manager node is accessible on a worker node without an explicit driver configuration—tasks will fail to mount, and the service will restart in a crash loop.
Several pitfalls reveal themselves only after a production incident:
--restart-policy on-failure:5 will stop retrying after five restarts within a rolling window. If the underlying issue persists, the task stays stopped and docker service ps shows Failed. There is no automatic escalation; you must manually intervene or adjust the policy.docker info output. The node appears in docker node ls with Status: Down and remains unreachable until the join command is re‑run with the correct CA certificate.--with-registry-auth or a local cache (Harbor, Artifactory), the registry may rate‑limit, causing tasks to fall back to ImagePullBackOff. The orchestrator will keep retrying, but the event log will flood with pull attempt entries, masking the real bandwidth constraint.--with-registry-auth combined with environment‑variable injection (-e), the order of precedence matters. Docker resolves env vars before injecting secrets defined in a swarm config, but if both define the same key, the env var wins silently, potentially exposing credentials or breaking feature flags.--limit-memory 512M --limit-cpu 0.5 to the docker service create call is essential for multi‑tenant workloads.When any of these occur, the first diagnostic step is docker service ps to see task states, docker logs <task> for recent output, and docker events for real‑time orchestration events. Pairing these with systemctl status docker and checking daemon logs at /var/log/docker/ usually surfaces the root cause within minutes.
Orchestration beyond Kubernetes is a pragmatic choice for many fleets, but there comes a point where the lack of a declarative API, limited rollout strategies, and the absence of a rich ecosystem of operators become blockers. Signs that a migration toward a full K8s distribution (or a managed service) is warranted include:
Moving away from K8s does not mean abandoning container semantics. The primitives—desired state, declarative image references, restart policies, network isolation—remain the same. What changes is the control plane that enforces those semantics across a cluster. The commands and flags shown here are your footing while you evaluate whether staying on a lightweight runtime or investing in a full orchestration platform aligns with your operational capacity and product roadmap.
This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy