Skip to content
OpsAtlas

From Dockerfile to Kubernetes in Four Steps

By OpsAtlas · · 4 min read

Table of contents

In this tutorial you take a small Node.js API, notes-api, from a first Dockerfile to Kubernetes manifests that pass a security review. Each step uses one OpsAtlas tool and hands its result to the next:

  1. Lint the Dockerfile and apply the safe fixes.
  2. Turn the docker run commands into compose.yaml for local development.
  3. Generate the Kubernetes Deployment and Service.
  4. Validate the manifests before you apply them.

Everything runs in your browser, so you can follow along with your own app instead of the example. The outputs shown below are exactly what the tools produce for the example inputs.

You need: a Node.js app with a package.json and package-lock.json, an image registry you can push to, and a cluster for step 4 if you want to deploy for real.

Step 1: Lint and fix the Dockerfile

Start with the Dockerfile most people write first:

FROM node:22
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD npm start

Paste it into the Dockerfile linter. It scores 79/100 (B) with five findings. Three of them have an Apply fix button:

  • Runs as root: Add USER 10001 before CMD
  • Shell-form CMD: Use exec form: CMD ["npm", "start"]
  • npm install: Use npm ci

Click all three. The score rises to 98/100 (A), and the changed lines are highlighted in the editor. Two findings remain because they need a decision from you:

  • COPY . . before installing dependencies. Every code change reinstalls all packages. Copy package.json and package-lock.json first, install, then copy the rest.
  • No HEALTHCHECK. This one is informational: on Kubernetes the probes from step 3 do the same job.

Make those edits by hand. While you are at it, switch to the smaller node:22-slim base, install production dependencies only, and start Node directly so it receives SIGTERM:

FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
USER 1000
HEALTHCHECK CMD ["node", "healthcheck.js"]
CMD ["node", "server.js"]

This scores 100/100. Notice USER 1000 rather than USER node. Both are the same non-root user in the official Node.js images, but Kubernetes can only check runAsNonRoot against a numeric user. With a user name, the pod in step 3 would fail to start.

Build and push the image as ghcr.io/acme/notes-api:1.0.0 (use your own registry and version).

Step 2: Replace docker run with Compose

Locally, the API needs a Postgres database. If you have been starting both with docker run, paste the commands into the docker run to Compose converter:

docker run -d --name notes-api --network notes -p 3000:3000 \
  -e DATABASE_URL=postgres://notes:notes@db:5432/notes \
  --restart unless-stopped ghcr.io/acme/notes-api:1.0.0

docker run -d --name db --network notes \
  -e POSTGRES_USER=notes -e POSTGRES_PASSWORD=notes \
  -v pgdata:/var/lib/postgresql/data \
  --restart unless-stopped postgres:16

It produces this compose.yaml:

services:
  notes-api:
    image: ghcr.io/acme/notes-api:1.0.0
    container_name: notes-api
    networks:
    - notes
    ports:
    - "3000:3000"
    environment:
    - DATABASE_URL=postgres://notes:notes@db:5432/notes
    restart: unless-stopped
  db:
    image: postgres:16
    container_name: db
    networks:
    - notes
    environment:
    - POSTGRES_USER=notes
    - POSTGRES_PASSWORD=notes
    volumes:
    - pgdata:/var/lib/postgresql/data
    restart: unless-stopped
volumes:
  pgdata: {}
networks:
  notes: {}

The converter also warns that Compose will create the notes network itself. If you created it earlier with docker network create, mark it external: true or delete the old one. Run docker compose up -d, check http://localhost:3000, and the local setup is done. The database password is fine for local development; in the cluster it belongs in a Secret.

Step 3: Generate the Kubernetes manifests

Open the Kubernetes manifest generator and fill in the form:

FieldValue
App namenotes-api
Namespacenotes
Container imageghcr.io/acme/notes-api:1.0.0
Replicas2
Container port3000
Service port80
Environment variablesNODE_ENV=production
Readiness and liveness probeHTTP
Probe path/healthz
Hardened security contexton

Keep the default resources (100m CPU and 128Mi memory requested, 256Mi memory limit) unless you already know what the API needs. The generator writes a Deployment and a Service. The part that matters most is the hardened security context:

    spec:
      automountServiceAccountToken: false
      securityContext:
        runAsNonRoot: true
        seccompProfile:
          type: RuntimeDefault
      containers:
      - name: notes-api
        image: ghcr.io/acme/notes-api:1.0.0
        # … ports, env, resources, probes …
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop:
            - ALL
        volumeMounts:
        - name: tmp
          mountPath: /tmp

The generator shows one warning: hardened mode requires an image that runs as a non-root user. Your image from step 1 already does, with a numeric USER, so runAsNonRoot: true passes. The read-only root filesystem is why the generator mounts an empty /tmp; if your app writes anywhere else, mount a volume there too.

Copy the output to k8s/notes-api.yaml.

Step 4: Validate before you apply

Paste the generated file into the Kubernetes YAML validator. It scores 100/100 (A). The diagram shows the Service selecting the Deployment, and there is one informational finding: no PodDisruptionBudget protects the two replicas during node drains. Add one when the service matters:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: notes-api
  namespace: notes
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: notes-api

Paste it below the other documents and the validator links it to the Deployment. Then apply everything and check that both pods become ready:

kubectl create namespace notes
kubectl apply -f k8s/
kubectl -n notes rollout status deployment/notes-api

What you built

  • A Dockerfile that installs only production dependencies, reuses the dependency cache when code changes, runs as a numeric non-root user and stops cleanly.
  • A compose.yaml that replaces the docker run commands for local development.
  • Kubernetes manifests with probes, resource requests, a hardened security context and a disruption budget, reviewed before they reach the cluster.

To go further, read Dockerfile mistakes that leak secrets and bloat images, and move the database password into a Secret before your first real deploy.

Try it yourself

  • Dockerfile linter and fixer

    Lint a Dockerfile in your browser: find root users, unpinned images, leaked secrets and bloated layers, then apply safe fixes in one click.

    DockerBeta
  • docker run to Docker Compose converter

    Convert docker run commands into a clean compose.yaml: ports, volumes, env, networks, healthchecks and limits. Runs in your browser.

    Docker
  • Kubernetes manifest generator

    Generate a Kubernetes Deployment and Service with resources, probes and a hardened securityContext, plus optional Ingress and HPA.

    Kubernetes
  • Kubernetes YAML validator and visualizer

    Paste Kubernetes manifests to get a resource diagram and a security and best-practice review: privileged pods, root, probes, limits, deprecated APIs.

    KubernetesBeta