VM → Docker/K8s Migration: Containerization with Guarantee

Moving an application from virtual machines to containers often brings unexpected issues: legacy code, hidden dependencies, and an environment that 'only worked on the server.' We help you navigate this path without pain—we audit, containerize services, and deploy them in Docker or Kubernetes. Our team delivers the project turnkey, from VM inventory to orchestrated launch, ensuring stable operation and ongoing support.

Our competencies:

Frequently Asked Questions

Latest works

  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1365
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1330
  • B2B Advance company logo design
    B2B Advance company logo design
    776
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1077
  • AIDER company logo development
    AIDER company logo development
    1022
  • CRM development for Chasseurs
    CRM development for Chasseurs
    1113

Migration from Virtual Machines to Containers (VM → Docker/K8s)

Moving an application from a VM to containers is a task every team faces sooner or later. In our practice, we've seen how legacy applications that ran for years on dedicated servers broke in containers due to non-obvious dependencies. Our engineers have helped dozens of clients cut deployment time by 3x and eliminate environment issues. Below is a step-by-step methodology proven on projects with 50+ microservices.

Why Containerization Beats VMs

A VM requires OS installation, repository configuration, kernel updates—done manually or via Ansible. A container is built once and runs anywhere. Based on our data, development on containers accelerates CI/CD cycles by 40%. In Kubernetes, you get autoscaling (HPA/VPA), self-healing, and rolling updates out of the box. Plus, OS license savings add up to a 30–50% reduction in infrastructure costs.

Starting with VM Inventory

Before writing the first Dockerfile, you need to understand exactly what's running on the VM. Often, a single production machine hosts: nginx as a reverse proxy, the application itself (Node.js), PostgreSQL, Redis, a cron job for backups, and a local Postfix mail agent. That's seven separate containers if done right.

# What is running on the VM
systemctl list-units --type=service --state=running
ps auxf | grep -v grep

# Which ports are listening
ss -tlnp

# Cron jobs
crontab -l
ls /etc/cron.d/ /etc/cron.daily/

# Application dependencies (example for Node.js)
cat package.json | jq '.dependencies | keys[]'
node --version; npm --version

The result of inventory is a list of everything to be migrated, with an understanding of dependencies between components.

Typical Pitfalls in Dockerfile

Logging. The application writes to /var/log/app/access.log. In a container, logs must go to stdout/stderr—otherwise Docker daemon, Fluentd, or Loki won't pick them up. One option: add a symlink ln -sf /dev/stdout /var/log/app/access.log in the Dockerfile. A better option: fix the application's logging configuration itself.

Configuration via environment variables. A hardcoded path /etc/app/config.yaml must be replaced with ENV variables or a volume mount. The Twelve-Factor App is not just a trendy concept—it's a requirement for normal operation in K8s.

Users and permissions. The application runs as root on the VM. In a container, that's a security risk. Add an unprivileged user:

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:20-alpine
RUN addgroup -g 1001 appgroup && adduser -u 1001 -G appgroup -s /bin/sh -D appuser
WORKDIR /app
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --chown=appuser:appgroup . .
USER appuser
EXPOSE 3000
CMD ["node", "server.js"]

Init system. The application uses supervisord or spawns child processes directly. In a container, PID 1 must correctly handle SIGTERM and forward signals to child processes. If the application cannot, use tini as init:

RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]

Stateful data. The application writes uploaded files to /var/uploads/. In a container, this must be moved to a PersistentVolume (K8s) or mounted via -v (Docker). Data must not live inside the container.

Typical Kubernetes Manifest

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-backend
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1001
        fsGroup: 1001
      containers:
        - name: backend
          image: registry.internal/app-backend:1.2.3
          ports:
            - containerPort: 3000
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: app-secrets
                  key: database-url
            - name: REDIS_URL
              value: "redis://redis-service:6379"
          resources:
            requests:
              cpu: 200m
              memory: 256Mi
            limits:
              cpu: 1000m
              memory: 512Mi
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 5
          volumeMounts:
            - name: uploads
              mountPath: /app/uploads
      volumes:
        - name: uploads
          persistentVolumeClaim:
            claimName: app-uploads-pvc

Missing resources is one of the most common mistakes during the first deployment to K8s. Without limits, a pod can consume all memory on the node; without requests, the scheduler cannot distribute pods correctly.

Cron Jobs → CronJob

System cron is migrated to a K8s CronJob:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: db-backup
spec:
  schedule: "0 2 * * *"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: backup
              image: registry.internal/db-backup:latest
              env:
                - name: DATABASE_URL
                  valueFrom:
                    secretKeyRef:
                      name: app-secrets
                      key: database-url

concurrencyPolicy: Forbid prevents a new job from starting if the previous one is still running.

Ensuring Reliability During Migration

We guarantee that after the transition all key SLO metrics will be under control. Setting up Prometheus+Grafana monitoring is part of the standard package. All configs are versioned in Git—this eliminates the risk of lost changes. Our team consists of certified Kubernetes administrators and cloud engineers with 10+ years of experience. We have completed over 30 successful migrations for fintech, e-commerce, and SaaS products.

Criterion VM Containers (Docker/K8s)
Boot time 30–60 sec 1–2 sec
Resource usage Heavy OS, hypervisor isolation Lightweight process, shared kernel
Cost (TCO) Higher due to licenses and over-provisioning 30–50% lower via compact placement

What Our Migration Service Includes

  • Audit of VM infrastructure and dependency map
  • Dockerfile for each microservice with multi-stage build
  • Helm charts for K8s deployment
  • CI/CD setup (GitLab CI / GitHub Actions)
  • Monitoring and alerting (Prometheus, Loki, Grafana)
  • Operations documentation and runbook
  • Knowledge transfer to the client's team (1–2 days training)

Which Migration Strategy to Choose?

The correct sequence for a production application:

  1. Containerization — write Dockerfile, test locally
  2. CI for the image — build and push to registry on every merge to main
  3. Deploy to staging on K8s, run tests
  4. Parallel run — old VM and new pods run simultaneously, traffic stays on VM
  5. Traffic switch — Ingress or DNS points to K8s Service
  6. Observation — one week in production, compare metrics with baseline
  7. Decommission VM — after confirming stability

Never switch traffic and shut down the VM at the same time. It seems obvious, but in the stress of a deadline, it's easy to forget.

Ready to start the migration? Get a consultation with our engineer.

Timelines

Application Complexity Containerization Time K8s Migration Time
Simple (1 process, no state) 1–2 days 2–3 days
Medium (2–3 components, stateful) 3–5 days 1 week
Complex (5+ components, legacy dependencies) 1–2 weeks 2–4 weeks

Initial VM infrastructure audit and estimate take 1–2 days. Final timelines are determined after that. Order an audit of your VM infrastructure—we'll prepare a migration plan in 1–2 days.