paulserban.eu

Writing Edition

Paul Serban

Writing, snippets & book notes

August 07, 2019

Docker Orchestration: Docker Swarm and Kubernetes Explained

A practical comparison of the two dominant container orchestration platforms and when to choose each one

Introduction

Running a single container on a single host is trivial. Running hundreds of containers across dozens of machines, keeping them healthy, routing traffic correctly, and rolling out updates without downtime is a fundamentally different problem. This is the problem that container orchestration solves, and it's why Docker Swarm and Kubernetes exist. Both platforms take a declarative approach: you describe the desired state of your system, and the orchestrator continuously works to make reality match that description. The appeal is obvious once you've felt the pain of manually SSHing into servers to restart a crashed process at 2 a.m.

This article walks through how Docker Swarm and Kubernetes actually work under the hood, where they diverge in design philosophy, and how to reason about choosing between them. Rather than treating this as a simple feature checklist, the goal is to build a mental model of orchestration itself - scheduling, service discovery, health checking, and rolling updates - so that whichever tool you end up using, you understand why it behaves the way it does.

Context and the Problem Orchestration Solves

Before Docker Swarm and Kubernetes existed in their current forms, container deployment at scale was largely a hand-rolled affair. Teams wrote shell scripts and configuration management playbooks (Ansible, Chef, Puppet) to place containers on hosts, and they built their own health-check loops and load balancer configuration updates. This worked, but it was brittle. A host going down meant manual intervention. Scaling a service meant editing inventory files and re-running deployment scripts. There was no single source of truth for "what should be running right now," which made debugging production incidents slower than it needed to be.

Docker Swarm emerged directly from Docker Inc. as a way to extend the existing docker CLI and API into a clustering solution, prioritizing simplicity and a low learning curve for teams already comfortable with Docker Compose. Kubernetes, originally designed at Google and open-sourced in 2014, drew on internal experience running containerized workloads at massive scale (documented in Google's Borg and Omega papers) and was built from the outset as an extensible platform rather than a Docker extension. Both tools solve the same core problems - scheduling, self-healing, service discovery, and rolling updates - but they encode very different assumptions about how much complexity a team is willing to take on in exchange for flexibility.

It's worth being precise about what "orchestration" actually means in this context, because the term gets used loosely. An orchestrator is responsible for four things: deciding which node a workload runs on (scheduling), restarting or replacing workloads that fail (self-healing), letting workloads find each other over the network (service discovery), and updating running workloads without taking the whole service offline (rolling deployments). Everything else - dashboards, package managers, CI/CD integrations - is built on top of these primitives. Keeping this narrow definition in mind makes it much easier to compare Swarm and Kubernetes fairly, since they implement the same four primitives with different levels of configurability.

Deep Technical Explanation

Docker Swarm's architecture is intentionally close to what you already know from docker run and docker-compose. A Swarm cluster consists of manager nodes, which maintain cluster state using a Raft consensus algorithm, and worker nodes, which execute tasks. You define a docker-compose.yml-style stack file, run docker stack deploy, and Swarm's scheduler places containers ("tasks") across available workers based on constraints like resource reservations, placement preferences, or explicit node labels. Service discovery is handled through an embedded DNS server: services can reach each other by service name, and Swarm's routing mesh handles load balancing across replicas automatically, even from any node in the cluster.

Kubernetes takes a more decomposed approach. The control plane is split into distinct components: the API server (the single entry point for all cluster operations), etcd (a distributed key-value store holding all cluster state), the scheduler (which assigns Pods to nodes based on resource requests, affinity rules, and taints/tolerations), and the controller manager (which runs reconciliation loops for objects like Deployments and ReplicaSets). Rather than running containers directly, Kubernetes' smallest deployable unit is the Pod - one or more tightly coupled containers sharing a network namespace and storage volumes. This extra layer of indirection is what allows Kubernetes to support sidecar containers, init containers, and shared-volume patterns that Swarm's simpler model doesn't natively express.

The reconciliation model is the conceptual heart of Kubernetes, and understanding it clarifies a lot of behavior that otherwise seems mysterious. Every controller in Kubernetes runs a loop: read the desired state (from a Deployment or StatefulSet spec stored in etcd), compare it to the observed state (queried from the API server), and issue changes to close the gap. This is why deleting a Pod that belongs to a Deployment doesn't actually remove it for long - the ReplicaSet controller notices the discrepancy between "desired: 3 replicas" and "observed: 2 replicas" and creates a replacement within seconds. Swarm has an analogous reconciliation mechanism for services, but it's simpler and less exposed; you don't get direct access to the intermediate objects the way you do with Kubernetes' ReplicaSets and Deployments.

Networking is another area where the two diverge substantially. Swarm ships with a built-in overlay network driver and ingress routing mesh that works out of the box with minimal configuration. Kubernetes deliberately leaves network implementation to the Container Network Interface (CNI) specification, meaning you choose a plugin - Calico, Cilium, Flannel, or a cloud provider's native CNI - based on your requirements for network policy enforcement, encryption, or performance. This is a recurring theme: Kubernetes gives you a pluggable interface where Swarm gives you an opinionated default.

Implementation and Practical Examples

To make the architectural differences concrete, consider deploying the same simple web service - an API with three replicas, health checks, and a rolling update strategy - on both platforms.

A Docker Swarm stack file for this looks like:

version: "3.8"
services:
  api:
    image: myregistry/api:1.4.0
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
        order: start-first
      restart_policy:
        condition: on-failure
        max_attempts: 3
      resources:
        limits:
          cpus: "0.50"
          memory: 256M
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"]
      interval: 15s
      timeout: 5s
      retries: 3
    networks:
      - backend
networks:
  backend:
    driver: overlay

Deploying it is a single command: docker stack deploy -c stack.yml myapp. Swarm handles placing the three replicas, wiring up the overlay network, and executing the rolling update one task at a time whenever you push a new image tag.

The equivalent Kubernetes manifest requires more explicit objects - typically a Deployment and a Service - because Kubernetes separates "how to run the Pods" from "how traffic reaches them":

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: myregistry/api:1.4.0
          resources:
            limits:
              cpu: "500m"
              memory: "256Mi"
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 15
---
apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  selector:
    app: api
  ports:
    - port: 80
      targetPort: 8080

Applying this with kubectl apply -f deployment.yaml gives you finer control - for instance, maxUnavailable: 0 guarantees the rolling update never drops below full capacity, something Swarm's update_config can approximate but not express as precisely.

For teams automating cluster interactions programmatically, both platforms expose APIs that fit naturally into CI/CD tooling. The following Python example uses the official Kubernetes client to watch a Deployment's rollout status, a pattern commonly used in deployment automation scripts:

from kubernetes import client, config, watch

def wait_for_rollout(namespace: str, deployment_name: str, timeout_seconds: int = 300):
    config.load_kube_config()
    apps_v1 = client.AppsV1Api()
    w = watch.Watch()

    for event in w.stream(
        apps_v1.list_namespaced_deployment,
        namespace=namespace,
        timeout_seconds=timeout_seconds,
    ):
        deployment = event["object"]
        if deployment.metadata.name != deployment_name:
            continue

        status = deployment.status
        desired = deployment.spec.replicas
        ready = status.ready_replicas or 0

        if ready == desired and status.updated_replicas == desired:
            w.stop()
            return True

    raise TimeoutError(f"Rollout of {deployment_name} did not complete in time")

This kind of script is typically invoked at the end of a CI pipeline step, after kubectl apply, to block the pipeline until the new version is confirmed healthy rather than assuming success the moment the manifest is submitted.

Trade-offs and Common Pitfalls

The most consistent trade-off teams run into is operational complexity versus configurability. Kubernetes' component separation - API server, etcd, scheduler, controller manager, kubelet, kube-proxy - means there are more moving parts that can fail independently, and more knobs to misconfigure. Etcd performance degradation, API server certificate rotation, and kubelet resource pressure are all real operational concerns that teams running self-managed Kubernetes clusters have to understand. Swarm's tighter integration with the Docker Engine means fewer components to reason about, but it also means you inherit whatever limitations the Docker Engine itself has, and you have far less room to customize scheduling behavior or network policy.

A common pitfall with Kubernetes adoption is treating YAML manifests as static configuration rather than understanding the reconciliation loops behind them. Engineers new to the platform are often surprised that a kubectl delete pod doesn't "fix" a stuck Pod the way restarting a process would - if the Pod belongs to a Deployment, it comes right back, sometimes with the same underlying problem, because the root cause (a bad image, a missing ConfigMap, an unschedulable resource request) hasn't changed. Debugging Kubernetes effectively requires reading kubectl describe output and controller events, not just Pod logs.

On the Swarm side, the most common pitfall is scaling expectations. Swarm's simplicity is genuinely valuable for small to mid-sized deployments, but its ecosystem has stagnated relative to Kubernetes: fewer third-party tools integrate with it, its addition of features like autoscaling or fine-grained network policy has lagged, and Docker Inc.'s own strategic focus has shifted toward Kubernetes compatibility rather than Swarm feature development. Teams that pick Swarm for its simplicity and later need capabilities like custom resource definitions, advanced autoscaling (Horizontal Pod Autoscaler with custom metrics), or a broad operator ecosystem often find themselves migrating to Kubernetes anyway, at which point the migration cost is higher than if they'd started there.

Best Practices

Regardless of which orchestrator you choose, a handful of practices consistently separate stable production clusters from fragile ones. First, always define resource requests and limits explicitly. Both platforms will schedule workloads without them, but unscheduled resource contention is one of the most common causes of production incidents - a single misbehaving container can starve its neighbors of CPU or memory if limits aren't enforced. Second, treat health checks as a design responsibility of the application, not an afterthought bolted on at deploy time. A /healthz or /readyz endpoint should verify that dependencies (databases, caches, downstream services) are actually reachable, not just that the process is alive - a common failure mode is a "healthy" container that responds to pings but can't actually serve real traffic.

Third, adopt rolling update strategies that fail safely. In Kubernetes, setting maxUnavailable: 0 for critical services guarantees you never drop below full capacity during a deploy, at the cost of requiring slightly more headroom during the rollout. In Swarm, order: start-first in the update configuration achieves a similar effect by starting the replacement task before stopping the old one. Fourth, keep configuration in version control and apply it through a pipeline rather than manual kubectl or docker stack deploy commands run from a developer's laptop - this is the difference between a system with an audit trail and one without.

Finally, invest early in observability that's orchestration-aware, not just application-aware. Metrics like Pod restart counts, scheduling latency, and node resource pressure tell you about the health of the orchestration layer itself, and they often surface problems before application-level metrics do. Tools like Prometheus (with kube-state-metrics for Kubernetes) or Docker's own stats API for Swarm should be part of the initial setup, not something added after the first outage.

Analogies and Mental Models

It helps to think of Docker Swarm as an extension of a shipping company's local delivery fleet - the same trucks and drivers you already have, now coordinated by a dispatcher, but limited to the routes and capabilities the fleet was originally built for. Kubernetes, by contrast, is closer to a full logistics network: it can plug in different transportation modes (the CNI network plugins), different warehouse strategies (StatefulSets versus Deployments), and third-party customs and inspection rules (admission controllers, custom resource definitions) - enormous flexibility, but you need a logistics team that understands the whole network to run it well.

Another useful mental model is the thermostat. A Kubernetes Deployment (or a Swarm service) doesn't execute your deployment as a one-time script - it behaves like a thermostat set to a target temperature. You state the target ("3 replicas of version 1.4.0"), and the controller continuously measures the current state and nudges the system toward the target whenever it drifts, whether that drift is caused by a crashed container, a node failure, or a manual kubectl scale command that got overridden. This model explains almost every "surprising" behavior newcomers encounter, from Pods reappearing after deletion to updates rolling back automatically when health checks fail.

The 80/20 Insight

If you strip away the tooling-specific syntax, roughly twenty percent of orchestration concepts account for the vast majority of real-world operational value: resource requests/limits, health checks (liveness and readiness), rolling update configuration, and basic service discovery. Teams that get these four right rarely experience the catastrophic incidents that orchestration horror stories are made of - cascading failures from unbounded resource usage, traffic routed to unhealthy instances, or downtime during deploys.

Everything beyond that - custom schedulers, admission webhooks, service meshes, operators for stateful workloads - delivers real value at scale, but it's the remaining eighty percent of complexity for a shrinking slice of additional benefit for most teams. A pragmatic path is to master the core four concepts thoroughly on whichever platform you choose before reaching for the more advanced Kubernetes ecosystem tooling (Istio, Helm charts with complex templating, custom operators). Complexity added before it's needed is a common source of the operational fragility both platforms are otherwise designed to eliminate.

Key Takeaways

Conclusion

Docker Swarm and Kubernetes solve the same underlying problem - keeping a distributed set of containers running in the state you declared - but they make different bets about how much complexity is worth trading for flexibility. Swarm optimizes for a short learning curve and tight integration with tools teams already know from Docker Compose. Kubernetes optimizes for extensibility and long-term ecosystem support, at the cost of a steeper operational learning curve and more components to reason about.

Neither choice is objectively correct; it depends on team size, growth trajectory, and how much operational overhead you're willing to absorb in exchange for flexibility you may or may not eventually need. What matters more than the specific platform is understanding the underlying primitives - scheduling, self-healing, service discovery, and rolling updates - well enough that you can reason about failures when they happen, rather than treating the orchestrator as an opaque black box that either works or doesn't.

References

  1. Docker, Inc. - "Swarm mode overview." Docker Documentation. https://docs.docker.com/engine/swarm/
  2. Docker, Inc. - "Deploy services to a swarm." Docker Documentation. https://docs.docker.com/engine/swarm/services/
  3. Kubernetes Authors - "Kubernetes Documentation." The Kubernetes Project. https://kubernetes.io/docs/home/
  4. Kubernetes Authors - "Deployments." Kubernetes Documentation. https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
  5. Kubernetes Authors - "Pods." Kubernetes Documentation. https://kubernetes.io/docs/concepts/workloads/pods/
  6. Burns, B., Grant, B., Oppenheimer, D., Brewer, E., Wilkes, J. - "Borg, Omega, and Kubernetes." Communications of the ACM, 2016. https://queue.acm.org/detail.cfm?id=2898444
  7. Cloud Native Computing Foundation - "Container Network Interface (CNI) Specification." https://github.com/containernetworking/cni
  8. Kubernetes Authors - "kubernetes client library for Python." https://github.com/kubernetes-client/python