← Back to DevBytes

Migrating from Docker Swarm to Kubernetes: Step-by-Step Guide

Understanding the Docker Swarm to Kubernetes Migration

Docker Swarm and Kubernetes are both container orchestration platforms, but they differ significantly in architecture, scalability, and ecosystem maturity. Docker Swarm offers simplicity and tight integration with the Docker CLI, making it ideal for smaller deployments. Kubernetes, on the other hand, provides a richer feature set, robust auto-scaling, self-healing capabilities, and has become the industry standard for container orchestration at scale. Migrating from Swarm to Kubernetes involves translating service definitions, networking configurations, storage volumes, and deployment workflows into Kubernetes-native resources.

Key Differences Between Docker Swarm and Kubernetes

Before diving into the migration, it's essential to understand the conceptual mapping between the two platforms. Docker Swarm services map to Kubernetes Deployments or StatefulSets. Swarm stacks map to Kubernetes namespaces with multiple resources. Swarm configs and secrets have direct counterparts in Kubernetes ConfigMaps and Secrets. Networking in Swarm uses overlay networks, while Kubernetes uses a flat network model with Services and Ingress resources for routing.

Pre-Migration Assessment and Planning

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

A successful migration begins with a thorough audit of your existing Swarm environment. Document every service, its environment variables, mounted volumes, secret references, network attachments, and deployment constraints. This inventory will serve as your migration blueprint and help you identify potential challenges early.

Auditing Your Swarm Environment

Run the following commands on your Swarm manager node to capture the complete state of your cluster. Save the outputs to files for reference during the migration process.

# List all services with detailed information
docker service ls --format "table {{.Name}}\t{{.Image}}\t{{.Mode}}\t{{.Replicas}}\t{{.Ports}}" > swarm-services.txt

# Export each service's full configuration
for service in $(docker service ls -q); do
  docker service inspect "$service" > "service-${service}.json"
done

# List all stacks
docker stack ls > swarm-stacks.txt

# Export each stack's compose configuration
for stack in $(docker stack ls --format "{{.Name}}"); do
  docker stack services "$stack" --format "table {{.Name}}\t{{.Image}}" > "stack-${stack}-services.txt"
done

# List secrets and configs
docker secret ls > swarm-secrets.txt
docker config ls > swarm-configs.txt

# List networks
docker network ls --filter "scope=swarm" > swarm-networks.txt

# Capture node information
docker node ls --format "table {{.Hostname}}\t{{.Status}}\t{{.Availability}}\t{{.Role}}" > swarm-nodes.txt

Creating a Migration Dependency Map

With the audit data collected, create a dependency map that shows how services connect to each other, which secrets they consume, and which volumes they mount. This map will guide the order of resource creation in Kubernetes and help you identify services that can be migrated together as a group.

Translating Swarm Services to Kubernetes Deployments

The core of the migration involves converting each Docker Swarm service into a Kubernetes Deployment. A Deployment manages a set of replicated pods and provides declarative updates, rollbacks, and scaling—functionality that closely mirrors Swarm services.

Basic Service Translation

Consider a typical Swarm service defined in a docker-compose file. Here's how it translates to Kubernetes YAML. The Swarm service definition uses Docker-native syntax, while the Kubernetes Deployment requires explicit labels, selectors, and a pod template.

Original Docker Swarm Service (docker-compose.yml):

version: "3.8"
services:
  web-app:
    image: nginx:1.25
    ports:
      - "80:80"
      - "443:443"
    environment:
      - NODE_ENV=production
      - LOG_LEVEL=info
    deploy:
      replicas: 3
      restart_policy:
        condition: on-failure
        delay: 5s
      resources:
        limits:
          cpus: "0.5"
          memory: 256M
    volumes:
      - web-data:/usr/share/nginx/html
    networks:
      - frontend

volumes:
  web-data:
    driver: local

networks:
  frontend:
    driver: overlay

Equivalent Kubernetes Deployment and Resources:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
  namespace: production
  labels:
    app: web-app
    tier: frontend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
        tier: frontend
    spec:
      restartPolicy: Always
      containers:
      - name: nginx
        image: nginx:1.25
        ports:
        - containerPort: 80
          name: http
          protocol: TCP
        - containerPort: 443
          name: https
          protocol: TCP
        env:
        - name: NODE_ENV
          value: "production"
        - name: LOG_LEVEL
          value: "info"
        resources:
          limits:
            cpu: "500m"
            memory: "256Mi"
          requests:
            cpu: "250m"
            memory: "128Mi"
        volumeMounts:
        - name: web-data
          mountPath: /usr/share/nginx/html
      volumes:
      - name: web-data
        persistentVolumeClaim:
          claimName: web-data-pvc

---
# service.yaml - Exposing the deployment
apiVersion: v1
kind: Service
metadata:
  name: web-app-service
  namespace: production
  labels:
    app: web-app
spec:
  type: ClusterIP
  selector:
    app: web-app
  ports:
  - name: http
    port: 80
    targetPort: 80
    protocol: TCP
  - name: https
    port: 443
    targetPort: 443
    protocol: TCP

---
# persistentvolumeclaim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: web-data-pvc
  namespace: production
spec:
  accessModes:
  - ReadWriteMany
  storageClassName: standard
  resources:
    requests:
      storage: 10Gi

Handling Swarm Replicated vs Global Services

Docker Swarm supports two service modes: replicated (a specified number of instances) and global (one instance per node). Kubernetes handles these differently. Replicated services map directly to Deployments with a replica count. Global services map to DaemonSets, which ensure a pod runs on every node in the cluster.

# DaemonSet for a global service (e.g., monitoring agent)
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-exporter
  namespace: monitoring
spec:
  selector:
    matchLabels:
      name: node-exporter
  template:
    metadata:
      labels:
        name: node-exporter
    spec:
      containers:
      - name: node-exporter
        image: prom/node-exporter:latest
        ports:
        - containerPort: 9100
          hostPort: 9100
          protocol: TCP
        resources:
          limits:
            cpu: "200m"
            memory: "128Mi"

Migrating Swarm Networking to Kubernetes Services and Ingress

Networking is one of the most significant conceptual shifts when moving from Swarm to Kubernetes. Swarm's overlay networks provide automatic service discovery through DNS using the service name. Kubernetes achieves the same through its built-in DNS service (CoreDNS) but requires explicit Service resources to enable internal routing.

Internal Service Discovery

In Swarm, containers on the same overlay network can reach each other simply by using the service name. In Kubernetes, a Service object with a ClusterIP type enables internal DNS resolution. Every Service gets a DNS record in the format <service-name>.<namespace>.svc.cluster.local.

# Internal service for backend API
apiVersion: v1
kind: Service
metadata:
  name: api-backend
  namespace: production
spec:
  type: ClusterIP
  selector:
    app: api-backend
  ports:
  - port: 8080
    targetPort: 8080
    protocol: TCP
---
# Another service can now reach this via:
# http://api-backend.production.svc.cluster.local:8080

Migrating Swarm Routing Mesh to Kubernetes Ingress

Docker Swarm's routing mesh automatically exposes published ports across all nodes in the cluster, even if a node isn't running the service. Kubernetes uses Ingress controllers (like NGINX Ingress or Traefik) to achieve similar functionality with far more routing capabilities, including host-based and path-based routing, TLS termination, and load balancing.

# ingress.yaml - Replaces Swarm routing mesh
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-ingress
  namespace: production
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - www.example.com
    secretName: tls-secret
  rules:
  - host: www.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: web-app-service
            port:
              number: 80
      - path: /api
        pathType: Prefix
        backend:
          service:
            name: api-backend
            port:
              number: 8080

Translating Swarm Secrets and Configs to Kubernetes

Both platforms support secrets and configuration management, but the implementation differs. Swarm secrets are encrypted at rest and mounted as files at /run/secrets/<secret-name>. Kubernetes Secrets are base64-encoded objects that can be mounted as files or exposed as environment variables. Swarm configs map directly to Kubernetes ConfigMaps.

Creating Kubernetes Secrets from Swarm Secrets

First, extract each secret's plaintext value from Swarm. Then create a Kubernetes Secret object. Note that Kubernetes Secrets are namespaced resources, unlike Swarm secrets which are cluster-wide.

# On the Swarm manager, retrieve a secret's value
docker secret inspect db-password --format "{{.Spec.Name}}" > secret-name.txt
# The actual value requires creating a temporary service to extract it

# Create the Kubernetes Secret declaratively
# secret-db-password.yaml
apiVersion: v1
kind: Secret
metadata:
  name: db-password
  namespace: production
type: Opaque
data:
  password: 

---
# Alternatively, create it imperatively from a file
echo -n "supersecretpassword" | base64 > password-encoded.txt
kubectl create secret generic db-password \
  --namespace production \
  --from-file=password=password-encoded.txt

Mounting Secrets in Pods

Reference the Secret in your Deployment pod template to mount it as a file or inject it as an environment variable, mirroring Swarm's behavior.

# Pod template excerpt showing secret mounting
spec:
  containers:
  - name: database
    image: postgres:16
    env:
    - name: POSTGRES_PASSWORD
      valueFrom:
        secretKeyRef:
          name: db-password
          key: password
    volumeMounts:
    - name: secret-volume
      mountPath: /etc/secrets
      readOnly: true
  volumes:
  - name: secret-volume
    secret:
      secretName: db-password
      items:
      - key: password
        path: db-password.txt

Converting Swarm Configs to ConfigMaps

Swarm configs store non-sensitive configuration data. The Kubernetes equivalent is a ConfigMap, which can hold entire configuration files, key-value pairs, or complex YAML/JSON documents.

# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-config
  namespace: production
data:
  nginx.conf: |
    server {
        listen 80;
        server_name localhost;
        location / {
            root /usr/share/nginx/html;
            index index.html;
        }
        location /health {
            return 200 'ok';
        }
    }
  site-parameters.conf: |
    max_clients=200
    timeout=30

---
# Reference in Deployment
spec:
  containers:
  - name: nginx
    image: nginx:1.25
    volumeMounts:
    - name: config-volume
      mountPath: /etc/nginx/conf.d
  volumes:
  - name: config-volume
    configMap:
      name: nginx-config
      items:
      - key: nginx.conf
        path: default.conf

Migrating Persistent Storage

Docker Swarm volumes are typically bound to a specific node unless using shared storage drivers. Kubernetes abstracts storage through PersistentVolumes (PV) and PersistentVolumeClaims (PVC), allowing pods to request storage without being tied to a specific node's filesystem. This is a critical improvement for stateful workloads.

Converting Swarm Volumes to PersistentVolumeClaims

For each Swarm volume, create a PVC that matches the storage requirements. If your Swarm volumes contain important data, you'll need to migrate that data separately using tools like rsync or backup/restore procedures.

# PVC for a stateful application like a database
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-data
  namespace: production
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: fast-ssd
  resources:
    requests:
      storage: 50Gi

---
# Deployment referencing the PVC
spec:
  containers:
  - name: postgres
    image: postgres:16
    volumeMounts:
    - name: postgres-storage
      mountPath: /var/lib/postgresql/data
      subPath: pgdata
  volumes:
  - name: postgres-storage
    persistentVolumeClaim:
      claimName: postgres-data

Handling StatefulSets for Stateful Applications

For stateful services that require stable network identities and ordered deployment (like databases or message queues), use a StatefulSet instead of a Deployment. This provides each pod with a unique, stable hostname and preserves data across rescheduling.

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres-cluster
  namespace: production
spec:
  serviceName: postgres-headless
  replicas: 3
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:16
        ports:
        - containerPort: 5432
          name: postgresql
        volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes:
      - ReadWriteOnce
      storageClassName: fast-ssd
      resources:
        requests:
          storage: 50Gi

---
# Headless service for StatefulSet peer discovery
apiVersion: v1
kind: Service
metadata:
  name: postgres-headless
  namespace: production
spec:
  clusterIP: None
  selector:
    app: postgres
  ports:
  - port: 5432
    name: postgresql

Migrating Swarm Stacks to Kubernetes Namespaces and Helm Charts

Docker Swarm stacks group related services, networks, and volumes under a single deployable unit using docker-compose files. In Kubernetes, the equivalent organizational unit is a Namespace, which provides a logical boundary for resources. For complex multi-service stacks, Helm charts offer templated, reusable packaging that surpasses Swarm stacks in flexibility.

Creating a Namespace for Each Stack

Start by creating a dedicated namespace for each Swarm stack you're migrating. This maintains the logical separation and makes resource management cleaner.

# Create namespaces matching your Swarm stacks
kubectl create namespace production-frontend
kubectl create namespace production-backend
kubectl create namespace monitoring
kubectl create namespace logging

# Label namespaces for organization
kubectl label namespace production-frontend environment=production tier=frontend
kubectl label namespace production-backend environment=production tier=backend

Converting a Complete Stack to Kubernetes Resources

Here's a full Swarm stack converted to Kubernetes resources within a single namespace. This demonstrates how multiple interdependent services translate together.

Original Swarm Stack (docker-compose.yml):

version: "3.8"
services:
  redis:
    image: redis:7-alpine
    networks:
      - backend
    deploy:
      replicas: 1
    volumes:
      - redis-data:/data

  worker:
    image: myapp/worker:latest
    networks:
      - backend
    environment:
      - REDIS_URL=redis://redis:6379
    deploy:
      replicas: 2

  web:
    image: myapp/web:latest
    ports:
      - "80:3000"
    networks:
      - backend
      - frontend
    environment:
      - REDIS_URL=redis://redis:6379
    deploy:
      replicas: 3

networks:
  backend:
    driver: overlay
  frontend:
    driver: overlay

volumes:
  redis-data:

Converted Kubernetes Resources (all in namespace "app-stack"):

# 00-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: app-stack
  labels:
    app: myapp
    managed-by: kubernetes

---
# 01-redis-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
  namespace: app-stack
  labels:
    app: redis
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: redis:7-alpine
        ports:
        - containerPort: 6379
          name: redis
        volumeMounts:
        - name: redis-data
          mountPath: /data
      volumes:
      - name: redis-data
        persistentVolumeClaim:
          claimName: redis-data-pvc

---
# 02-redis-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: redis
  namespace: app-stack
spec:
  selector:
    app: redis
  ports:
  - port: 6379
    targetPort: 6379

---
# 03-redis-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: redis-data-pvc
  namespace: app-stack
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi

---
# 04-worker-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: worker
  namespace: app-stack
spec:
  replicas: 2
  selector:
    matchLabels:
      app: worker
  template:
    metadata:
      labels:
        app: worker
    spec:
      containers:
      - name: worker
        image: myapp/worker:latest
        env:
        - name: REDIS_URL
          value: "redis://redis.app-stack.svc.cluster.local:6379"

---
# 05-web-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: app-stack
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: web
        image: myapp/web:latest
        ports:
        - containerPort: 3000
          name: http
        env:
        - name: REDIS_URL
          value: "redis://redis.app-stack.svc.cluster.local:6379"

---
# 06-web-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: web
  namespace: app-stack
spec:
  type: LoadBalancer
  selector:
    app: web
  ports:
  - port: 80
    targetPort: 3000

Handling Swarm Placement Constraints with Kubernetes Scheduling

Docker Swarm uses placement constraints to control which nodes run specific services (e.g., node labels like node.role==worker or custom labels). Kubernetes achieves the same through node affinity, pod anti-affinity, taints, and tolerations, providing much finer-grained control over pod scheduling.

Node Affinity Rules

# Pod specification with node affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: node-role
            operator: In
            values:
            - worker
          - key: disk-type
            operator: In
            values:
            - ssd
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 80
        preference:
          matchExpressions:
          - key: zone
            operator: In
            values:
            - us-east-1a

Pod Anti-Affinity for High Availability

Use pod anti-affinity to spread replicas across nodes, mimicking Swarm's placement: spread directive but with more explicit control.

spec:
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchLabels:
            app: web
        topologyKey: kubernetes.io/hostname

Implementing Resource Limits and Health Checks

Kubernetes offers more sophisticated health checking than Swarm. While Swarm relies on simple process-level health checks or custom HTTP checks, Kubernetes provides liveness, readiness, and startup probes that can execute commands, make HTTP requests, or check TCP connections.

Comprehensive Probe Configuration

spec:
  containers:
  - name: api-server
    image: myapp/api:v2.1
    ports:
    - containerPort: 8080
    resources:
      limits:
        cpu: "1"
        memory: "512Mi"
      requests:
        cpu: "500m"
        memory: "256Mi"
    startupProbe:
      httpGet:
        path: /health/startup
        port: 8080
      initialDelaySeconds: 10
      periodSeconds: 5
      failureThreshold: 30
    livenessProbe:
      httpGet:
        path: /health/live
        port: 8080
      initialDelaySeconds: 0
      periodSeconds: 15
      timeoutSeconds: 3
    readinessProbe:
      httpGet:
        path: /health/ready
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 10

CI/CD Pipeline Adaptation

Your deployment pipeline must be updated to work with Kubernetes. Swarm's docker stack deploy command with a compose file becomes kubectl apply with Kubernetes manifests. For more advanced workflows, adopt Helm for templating and deployment management.

Basic kubectl-Based Deployment Script

#!/bin/bash
# deploy.sh - Replaces 'docker stack deploy'
set -euo pipefail

NAMESPACE="production"
KUBE_CONTEXT="production-cluster"

echo "Applying namespace configuration..."
kubectl apply -f 00-namespace.yaml --context "$KUBE_CONTEXT"

echo "Deploying secrets and configs..."
kubectl apply -f secrets/ --context "$KUBE_CONTEXT" -n "$NAMESPACE"
kubectl apply -f configmaps/ --context "$KUBE_CONTEXT" -n "$NAMESPACE"

echo "Deploying persistent volume claims..."
kubectl apply -f storage/ --context "$KUBE_CONTEXT" -n "$NAMESPACE"

echo "Deploying backend services..."
kubectl apply -f backend/ --context "$KUBE_CONTEXT" -n "$NAMESPACE"

echo "Waiting for backend deployments to be ready..."
kubectl wait --for=condition=available deployment/redis deployment/worker \
  --context "$KUBE_CONTEXT" -n "$NAMESPACE" --timeout=120s

echo "Deploying frontend services..."
kubectl apply -f frontend/ --context "$KUBE_CONTEXT" -n "$NAMESPACE"

echo "Deploying ingress..."
kubectl apply -f ingress.yaml --context "$KUBE_CONTEXT" -n "$NAMESPACE"

echo "Deployment complete! Checking rollout status..."
kubectl rollout status deployment/web --context "$KUBE_CONTEXT" -n "$NAMESPACE"

Helm Chart Migration Example

For complex stacks, converting to a Helm chart provides reusability and parameterization. Here's a basic Helm chart structure for the stack migrated earlier.

# Chart.yaml
apiVersion: v2
name: myapp-stack
description: Migrated application stack from Docker Swarm
type: application
version: 1.0.0
appVersion: "latest"

# values.yaml
replicaCount:
  web: 3
  worker: 2
  redis: 1

image:
  web: myapp/web:latest
  worker: myapp/worker:latest
  redis: redis:7-alpine

service:
  web:
    type: LoadBalancer
    port: 80

persistence:
  redis:
    enabled: true
    size: 5Gi

# templates/deployment-web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: {{ .Values.replicaCount.web }}
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: web
        image: {{ .Values.image.web }}
        ports:
        - containerPort: 3000
        env:
        - name: REDIS_URL
          value: "redis://redis.{{ .Release.Namespace }}.svc.cluster.local:6379"

Testing and Validation Strategy

Before decommissioning your Swarm cluster, implement a thorough testing phase. Run both clusters in parallel during the cutover period to validate functionality, performance, and reliability.

Smoke Testing Deployed Services

#!/bin/bash
# smoke-test.sh - Validate migrated services

KUBE_CONTEXT="production-cluster"
NAMESPACE="production"

echo "=== Running Smoke Tests ==="

# Test service endpoints
echo "Testing web service..."
WEB_POD=$(kubectl get pods -n "$NAMESPACE" -l app=web \
  --context "$KUBE_CONTEXT" -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n "$NAMESPACE" "$WEB_POD" --context "$KUBE_CONTEXT" -- \
  curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/health

# Test DNS resolution
echo "Testing internal DNS resolution..."
kubectl run dns-test --rm -i --restart=Never --image=busybox:latest \
  -n "$NAMESPACE" --context "$KUBE_CONTEXT" -- \
  nslookup redis.app-stack.svc.cluster.local

# Test secret mounting
echo "Verifying secret mounting..."
kubectl exec -n "$NAMESPACE" deployment/db --context "$KUBE_CONTEXT" -- \
  ls -la /etc/secrets/

# Test persistent storage
echo "Testing persistent volume writes..."
kubectl exec -n "$NAMESPACE" deployment/redis --context "$KUBE_CONTEXT" -- \
  redis-cli set migration_test "$(date +%s)"

echo "=== Smoke tests completed ==="

Best Practices for a Smooth Migration

1. Adopt an Incremental Migration Approach

Never attempt a big-bang migration. Move services incrementally, starting with stateless, non-critical services. Validate each migrated service thoroughly before moving to the next. Use a canary deployment pattern where a small percentage of traffic routes to the Kubernetes cluster while the rest remains on Swarm.

2. Implement Comprehensive Monitoring Early

Deploy Prometheus and Grafana on your Kubernetes cluster before migrating workloads. Instrument your applications with metrics exporters. This visibility will help you identify performance regressions, resource bottlenecks, and unexpected behaviors during the transition.

# Deploy Prometheus monitoring stack before migration
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install prometheus prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --create-namespace \
  --set prometheus.serviceMonitorSelectorNilUsesHelmValues=false

3. Establish a GitOps Workflow

Store all Kubernetes manifests in a Git repository and use tools like ArgoCD or Flux to automatically sync your cluster state. This replaces ad-hoc docker stack deploy commands with a declarative, version-controlled, and auditable deployment process.

# Example ArgoCD application for a migrated stack
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp-stack
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/myapp-k8s-manifests
    targetRevision: main
    path: overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

4. Implement Resource Quotas and Network Policies

Kubernetes provides namespace-level resource quotas and fine-grained network policies that didn't exist in Swarm. Implement these early to prevent resource contention and enhance security.

# Resource quota for a namespace
apiVersion: v1
kind: ResourceQuota
metadata:
  name: production-quota
  namespace: production
spec:
  hard:
    requests.cpu: "10"
    requests.memory: "20Gi"
    limits.cpu: "20"
    limits.memory: "40Gi"
    persistentvolumeclaims: "10"
    services: "15"

---
# Network policy restricting pod communication
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-isolation
  namespace: production
spec:
  podSelector:
    matchLabels:
      tier: backend
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          tier: frontend
    ports:
    - port: 8080
      protocol: TCP
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: redis
    ports:
    - port: 6379
      protocol: TCP

5. Plan Your Rollback Strategy

Maintain the Swarm cluster in a ready state throughout the migration. Document a clear rollback procedure for each service. Kubernetes rollbacks are handled via kubectl rollout undo, but a full platform rollback to Swarm requires keeping the original compose files and infrastructure intact.

# Quick rollback of a Kubernetes deployment
kubectl rollout undo deployment/web-app -n production

# Check rollback history
kubectl rollout history deployment/web-app -n production

# Rollback to a specific revision
kubectl rollout undo deployment/web-app -n production --to-revision=3

6. Train Your Team on Kubernetes Fundamentals

The operational model differs significantly between Swarm and Kubernetes. Ensure your team understands pod lifecycle, debugging with kubectl, reading logs, and handling common failure scenarios before the migration begins. Invest in hands-on labs using tools like Minikube or Kind for local practice.

Data Migration for Stateful Workloads

For databases and stateful services, data migration requires careful planning. You cannot simply redeploy a stateful workload and point it at new storage. Use the following approach for zero-downtime or minimal-downtime data migration.

Database Migration Using Logical Replication

# Step 1: Deploy new PostgreSQL instance in Kubernetes
kubectl apply -f postgres-statefulset.yaml

# Step 2: Export data from Swarm PostgreSQL
pg_dump -h swarm-postgres-host -U postgres -d myapp_db \
  --no-owner --no-privileges -F c -f dump_file.dump

# Step 3: Restore to Kubernetes PostgreSQL
PGPOD=$(kubectl get pods -n production -l app=postgres \
  -o jsonpath='{.items[0].metadata.name}')
kubectl cp dump_file.dump "$PGPOD":/tmp/dump_file.dump -n production
kubectl exec -n production "$PGPOD" -- \
  pg_restore -U postgres -d myapp_db /tmp/dump_file.dump

# Step 4: Validate data integrity
kubectl exec -n production "$PGPOD" -- \
  psql -U postgres -d myapp_db -c "SELECT count(*) FROM users;"

Volume Data Migration with rsync

# For file-based volumes, use an intermediate pod to rsync data
apiVersion: v1
kind: Pod
metadata:
  name: data-migration-helper
  namespace: production
spec:
  containers:
  - name: migrator
    image: alpine:3.19
    command: ["sleep", "3600"]
    volumeMounts:
    - name: source-data
      mountPath: /

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles