Software / DevOps

Docker vs Kubernetes vs Serverless: Deployment Options Compared

/12 min read

Docker, Kubernetes, and serverless are not three competing choices — Docker is a packaging format and runtime, Kubernetes is a container orchestrator, and serverless is an execution model (FaaS or managed containers) that can run Docker images too. They overlap on a Venn diagram rather than a lineup; the real decision is how much infrastructure operation your team wants to own, and the honest answer for most teams is "as little as possible."

The Category Error, Explained

"Should we use Docker or serverless?" is like asking whether you should ship your product in a box or have the post office deliver it. Docker answers how is the code packaged? — as an OCI image with a declared filesystem, entrypoint, and runtime dependencies. Kubernetes answers who runs and schedules it? — placing your containers on machines, restarting crashes, scaling replicas, and routing traffic. Serverless answers what am I billed for and what do I operate? — invocations or vCPU-seconds, with the platform owning the machines, often running your container image under the hood.

That is why the comparison is still useful, though: each option implies a different operational lifestyle, cost curve, and set of constraints. Comparing them on those axes is exactly right — just not as if selecting one forecloses the others. AWS Lambda runs container images. Google Cloud Run is serverless that takes a Dockerfile. Kubernetes can run event-driven functions via Kubeless-style platforms. The lines are product boundaries, not physics.

Docker: Packaging and the Single-Server Sweet Spot

Docker's lasting contribution is the image format. A container image pins your runtime, system libraries, and environment in one immutable artifact, which eliminates the "works on my machine" class of failures and makes the thing you tested the exact thing you deploy. That property is valuable regardless of where it eventually runs.

Docker Compose covers two real deployment stories well. First, local development: one command spins up the app, Postgres, Redis, and a mail sink with healthchecked dependencies — far faster than asking every engineer to hand-install services. Second, small production: a single VPS running docker compose up -d behind Caddy or Traefik for TLS is a legitimate production architecture for side projects, internal tools, and early-stage products. Add a one-line deploy script, automatic restart policies, and offsite backups, and it will carry you further than most advice suggests.

# docker-compose.yml — local dev, or a small single-VPS deploy
services:
  web:
    build: .
    ports: ["8080:3000"]
    environment:
      DATABASE_URL: postgres://db:5432/app
    depends_on:
      db:
        condition: service_healthy
  db:
    image: postgres:17
    volumes: ["pgdata:/var/lib/postgresql/data"]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      retries: 5
volumes:
  pgdata:

Where Docker-alone breaks down is the moment you need more than one machine: manual failover, no autoscaling, no rolling deploys without writing scripts, and every service's port and volume decisions living in one file. That is precisely the operational surface Kubernetes was built to take over — and the point at which most teams should first consider a managed platform instead.

Docker verdict: non-negotiable as a packaging format for nearly everyone; excellent as a full deployment story on one or two machines; insufficient alone beyond that.

Kubernetes: What It Gives You and What It Costs

Kubernetes (K8s) is an open-source platform for automating deployment, scaling, and management of containerized apps. What it actually provides, concretely:

  • Scheduling: Bin-packing containers onto a pool of nodes with resource requests and limits.
  • Self-healing: Restarts failed containers, reschedules dead nodes, replaces pods that fail health checks.
  • Scaling: Horizontal pod autoscaling on CPU, memory, or custom metrics; cluster autoscaling for the node pool.
  • Service discovery and load balancing: A stable virtual IP and DNS name per service, so pods come and go without reconfiguration.
  • Rollouts: Rolling updates and automatic rollback on failed probes.
  • Ingress and cert management: HTTP routing and TLS certificate automation via controllers like ingress-nginx and cert-manager.

None of that is free. The cost of Kubernetes is not the software — it is the platform expertise and the config surface:

  • Cluster operations: Upgrades, node pools, networking (CNI), DNS, storage classes, and the failure modes of each.
  • YAML sprawl: Deployment, Service, Ingress, HPA, ConfigMap, Secret, NetworkPolicy, ServiceAccount, PDB — per service, per environment. Helm or Kustomize mitigates but does not remove the model you must hold in your head.
  • A de-facto platform team: Someone owns the cluster, the add-ons, and the paved road. Below roughly two engineers devoted to platform work, Kubernetes usually consumes more than it returns.
  • Debugging surface: CrashLoopBackOff, OOMKilled, image pull errors, ingress misroutes — each an acronym you learn at 3 a.m.
# deployment.yaml — the minimum you actually maintain per service
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  selector:
    matchLabels: { app: web }
  template:
    metadata:
      labels: { app: web }
    spec:
      containers:
        - name: web
          image: ghcr.io/acme/web@sha256:<digest>
          ports: [{ containerPort: 3000 }]
          resources:
            requests: { cpu: "250m", memory: "256Mi" }
            limits:   { memory: "512Mi" }
          readinessProbe:
            httpGet: { path: /healthz, port: 3000 }
---
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector: { app: web }
  ports: [{ port: 80, targetPort: 3000 }]

That snippet is one service's floor, not its ceiling — and the point is how much ceremony a single web process requires compared to a container registry and a run command.

Managed Kubernetes and the middle ground

Managed control planes — EKS, GKE, AKS — remove perhaps half the operational burden: the API server, etcd, and upgrades are the vendor's problem. You still own node groups, add-ons, networking policy, and everything layered on top (service mesh, GitOps, observability). GKE Autopilot and similar modes push further, managing nodes for you with per-pod pricing.

Below Kubernetes sit platforms that give you containers without cluster operations: Fly.io, Railway, Render, Heroku's successors, and AWS App Runner. You push an image or a repo; they handle placement, TLS, scaling, and rollouts. For most small-to-mid teams these are the correct answer to "we outgrew a VPS."

Serverless: FaaS and Managed Containers

Serverless splits into two sub-categories worth separating. Function-as-a-Service (AWS Lambda, Google Cloud Functions, Azure Functions, Cloudflare Workers) runs short-lived functions triggered by events — HTTP requests, queue messages, storage events, schedules. Managed container services (Google Cloud Run, AWS Fargate, Azure Container Apps) run your container image on infrastructure the platform operates, with request-based or task-based scaling and often scale-to-zero.

The appeal is a genuine elimination of infrastructure work: no OS patching, no cluster upgrades, no capacity planning. Pricing follows usage — per invocation and GB-seconds on Lambda, per vCPU-second while handling requests on Cloud Run — so an idle service can cost single-digit dollars per month. The constraints are equally genuine:

  • Cold starts: An idle function spun up fresh pays initialization time — loading the runtime and your code before the first byte of response. Typically tens to hundreds of milliseconds for small Node/Python functions, seconds for large JVM or .NET packages or big container images. Provisioned concurrency and always-warm minimum instances fix it at a cost.
  • Execution limits: Lambda caps at 15 minutes per invocation; CPU is proportional to configured memory; background work after the response ends gets killed. Long-running jobs belong on a worker service or a job runner, not in a request handler.
  • Vendor lock-in: Event formats, IAM integration, and the surrounding services (Step Functions, EventBridge, Pub/Sub) tie you to a cloud — more deeply than a plain container does.
  • Local development: Weaker fidelity than Docker. Emulators, per-vendor local runtimes, and Cloud Run's local mode help, but testing against real event envelopes takes discipline.
// Cloud Run / Cloud Functions / Lambda-style handler
// No port binding, no server framework boot required
export async function handler(req, res) {
  // Scale-to-zero: this process may be cold — keep init work lazy
  const session = await getSession(req);          // cached client
  const result = await db.orders.list(session.tenantId);
  res.status(200).json(result);
}

// FaaS execution limits to design around:
//   AWS Lambda        — 15 min max, 10 GB memory, no CPU pinning
//   Google Cloud Run   — request-based, 60 min max (24h beta), CPU per request
//   Cloudflare Workers — CPU-time based, global edge, no persistent sockets

Cloudflare Workers occupies a distinct corner: V8 isolate-based rather than container-based, running at the edge with near-instant cold starts and no traditional container runtime at all — excellent for edge logic, API fronts, and workers that must run close to users, less suited to heavy compute or arbitrary native dependencies.

Head-to-Head Comparison

The practical axes teams actually argue about:

DimensionDocker (Compose/VPS)KubernetesServerless
Ops burdenLow on 1 box; manual failoverHigh — needs platform expertiseMinimal — no infra to run
Cost at low traffic~$5–50/mo VPS, fixedExpensive idle (control plane + nodes)Near zero with scale-to-zero
Cost at high trafficManual scaling ceilingEfficient at scale, per-node pricingPer-invocation can exceed always-on
Cold startNoneNoneReal — tens of ms to seconds
Long-running jobsFineFine (Jobs/CronJob)Awkward — hard time limits
Vendor lock-inLow — portable imagesMedium — portable core, cloud add-onsHigh — event formats + IAM + services
Local dev fidelityExcellent (Compose)Good (kind/minikube/k3d)Weakest — emulators, per-vendor tools
Scaling ceilingOne machine, then DIYVery high with right configEffectively unlimited, per quota
Team skills neededLinux + networking basicsDedicated platform knowledgeApp-level; event-driven design

Reading the table: Docker-on-a-VPS wins on simplicity and low fixed cost until you need a second machine or real availability. Kubernetes wins when traffic is sustained, unpredictable, and large enough that per-node efficiency beats idle control-plane cost — and when you have the team to run it. Serverless wins when traffic is spiky, event-driven, or genuinely near zero, and the workload fits execution limits.

Decision Guide: How to Choose

Walk this sequence honestly before adopting anything:

  1. Do you have any platform team at all? If not, Kubernetes is off the table regardless of traffic. Choose managed: a PaaS (Railway, Render, Fly, App Runner) or Cloud Run.
  2. Is the workload request/response and stateless? Web APIs, sites, and webhooks are ideal for FaaS or managed containers. Long-running websockets, background workers, video processing, and scheduled heavy jobs want a container service or Kubernetes Jobs.
  3. What is the traffic shape? Spiky, event-driven, or near-zero baseline → serverless with scale-to-zero. Steady, high utilization → always-on containers cost less per request than per-invocation pricing.
  4. What is the latency budget? If p99 must stay flat and cold starts are unacceptable, either pay for warm instances or run always-on containers. If you are at the edge, Cloudflare Workers' isolates change the cold-start math entirely.
  5. How strong is the pull toward one cloud? If you are already all-in on AWS with Step Functions and EventBridge, Lambda lock-in is a rounding error. If multi-cloud or portability matters, standard containers on a portable runtime (Fly, plain K8s, Cloud Run) keep options open.
  6. When does Kubernetes become the right call? When you have a dedicated platform team, multi-service complexity, genuine autoscaling needs, strict custom networking or policy requirements, and a workload whose sustained utilization makes idle capacity genuinely wasteful. Two of those signals are not enough; four usually are.

Common Paths Teams Actually Take

The progression repeats often enough to be a pattern:

Stage 1: Docker Compose on a single VPS, or a PaaS from day one. Fixed low cost, one mental model, no cluster.

Stage 2: Managed containers — Cloud Run, Fargate, Railway, Fly. You keep the container image, the platform keeps the machines. Most products live here permanently, and that is not a compromise — it is the efficient equilibrium.

Stage 3: Serverless FaaS for event-driven pieces (webhook handlers, image processing, cron glue) alongside the always-on services. Mixed architectures are normal: the front door scales to zero, the workers do not.

Stage 4: Kubernetes, adopted deliberately when the platform team exists and the workload justifies it — often driven by a dozen services, GPU scheduling needs, or regulated isolation requirements, not by the desire to have Kubernetes on the résumé.

Skipping stages happens (startups adopting K8s at three engineers), and it usually costs more in attention than it returns in capability for the first two years.

Conclusion

Docker won the packaging war and now runs underneath both of its supposed rivals. Kubernetes is the right tool for a specific, demanding job — orchestrating many containers at scale with a team that can operate the platform — and the wrong reflex for everything else. Serverless, in both its FaaS and managed-container forms, is the highest-leverage default for new services: minimal ops, usage-based cost, real constraints you can design around.

The verdict: start managed — Cloud Run, Railway, Render, Fly, or Fargate. Adopt Kubernetes only when you have a platform team and a concrete, articulable need it uniquely satisfies. Reach for FaaS for event-driven spikes and glue code. And keep everything in portable container images, so the decision remains reversible.

Key Takeaways

  • Docker is a packaging format, Kubernetes an orchestrator, serverless an execution model — they overlap rather than fully substitute, and serverless often runs your Docker image.
  • Compose on a single VPS or a PaaS is legitimate production for most early products; graduate to managed containers before considering a cluster.
  • Kubernetes pays off only with a dedicated platform team, sustained high utilization, and multi-service complexity — otherwise it is operational tax.
  • Serverless FaaS removes infrastructure work and scales to zero, but imposes cold starts, execution time limits, and deeper vendor coupling.
  • Match traffic shape to pricing model: spiky and idle-prone favors scale-to-zero; steady and saturated favors always-on containers.

FAQ

Is Docker obsolete because of Kubernetes and serverless?

No — the opposite. Docker's container image format is the standard packaging unit that Kubernetes schedules and that serverless platforms like Cloud Run, AWS Lambda (container images), and Fargate execute. Docker is obsolete only as a production runtime orchestrator; as a build and packaging tool it is more central than ever.

When should a small team choose Kubernetes?

Rarely. A small team should reach for Kubernetes only with a concrete trigger: GPU scheduling, strict multi-tenant isolation, or dozens of services with autoscaling needs that managed platforms cannot express. Even then, start with a managed control plane (GKE Autopilot, EKS Auto Mode) rather than self-hosting.

Are cold starts a dealbreaker for serverless?

For most web APIs they are manageable: small Node, Python, or Go functions typically initialize in tens to low hundreds of milliseconds, and provisioned concurrency or minimum warm instances eliminate the tail for latency-critical paths. Cold starts become a real problem for large JVM runtimes, big container images on FaaS, or user-facing interactions where every 200ms is felt.

What is cheaper at scale: serverless or Kubernetes?

It depends on utilization. Per-invocation FaaS pricing is excellent for spiky, intermittent, or near-zero traffic but grows expensive under sustained load. Always-on containers (Kubernetes or managed) cost more when idle but less per request at high, steady utilization. Model both against your traffic curve — the crossover usually arrives when baseline utilization passes roughly 30–50%.

Is Cloud Run serverless or containers?

Both. Cloud Run is a managed container platform — you deploy a Docker image or buildpack and it runs on infrastructure Google operates — with serverless characteristics: request-based autoscaling down to zero, per-second billing while active, and no cluster to manage. It is the clearest example of the category boundary dissolving.

References

  1. Google Cloud. "Cloud Run: Run containers on a fully managed serverless platform." cloud.google.com/run
  2. Amazon Web Services. "What is Kubernetes?" AWS. aws.amazon.com/what-is/kubernetes
  3. AWS Lambda. "Lambda quotas and limits." docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html
  4. Kubernetes. "Production-grade container scheduling and management." kubernetes.io
  5. Cloudflare. "Workers: Serverless execution on the edge." workers.cloudflare.com
  6. Microsoft Azure. "What is Azure Container Apps?" learn.microsoft.com/azure/container-apps/overview
Summarize with AI
Page