Skaffold’s debug mode doesn’t just start your container with a debugger attached; it actually attaches a debugger to a running container, and the magic is how it handles the ephemeral nature of Kubernetes.
Let’s see this in action. Imagine you have a simple Node.js app you want to debug.
main.js:
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
console.log('Received a request!');
res.send('Hello from a debuggable container!');
});
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
And a Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY main.js ./
EXPOSE 3000
CMD ["node", "main.js"]
Your skaffold.yaml might look like this:
apiVersion: skaffold/v2beta28
kind: Config
metadata:
name: debug-example
build:
artifacts:
- image: my-debug-app
docker: {}
deploy:
kubectl:
manifests:
- k8s/deployment.yaml
profiles:
- name: debug
activation:
- kubeContext: docker-desktop # Or your K8s context
patches:
- op: add
path: /spec/template/spec/containers/0/ports
value:
- containerPort: 9229 # Node.js default debugger port
protocol: TCP
- op: replace
path: /spec/template/spec/containers/0/command
value: ["node", "--inspect=0.0.0.0:9229", "main.js"]
- op: replace
path: /spec/template/spec/containers/0/command
value: ["node", "--inspect=0.0.0.0:9229", "main.js"]
And your Kubernetes deployment k8s/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: debug-app-deployment
spec:
selector:
matchLabels:
app: debug-app
replicas: 1
template:
metadata:
labels:
app: debug-app
spec:
containers:
- name: debug-app
image: my-debug-app # This will be replaced by Skaffold
ports:
- containerPort: 3000
Now, to start debugging, you’d run:
skaffold dev -p debug
Skaffold will build your image, deploy it, and then, crucially, it will find the running container and attach a debugger. It automatically forwards the debugger port (9229 in this case) to your local machine. Your IDE (like VS Code) will then connect to localhost:9229.
The core problem Skaffold solves here is twofold:
- Ephemeral Containers: Kubernetes containers are often short-lived. If you just manually
kubectl execinto a pod and try to start a debugger, the pod might restart, and your debugging session is lost. Skaffold manages the deployment lifecycle. - Port Forwarding: Debugger ports aren’t usually exposed externally. Skaffold automates the
kubectl port-forwardprocess for you, making the debugger accessible locally.
When you run skaffold dev -p debug, here’s what’s happening under the hood:
- Build: Skaffold builds your Docker image (
my-debug-app). - Deploy: It applies your Kubernetes manifests. The
debugprofile modifies the deployment to:- Add a
containerPort: 9229to expose the debugger. - Override the
CMDtonode --inspect=0.0.0.0:9229 main.js. This starts the Node.js process with the inspector enabled, listening on all interfaces (0.0.0.0) on port9229.
- Add a
- Attach: Skaffold then watches for your application’s pod to become ready. Once it’s running, Skaffold automatically initiates a port-forward from the remote debugger port (
9229on the pod) to a local port on your machine (usually also9229by default). - IDE Connect: You configure your IDE to connect to
localhost:9229. When you hit a breakpoint, execution pauses, and you can inspect variables, step through code, etc.
The activation block in the profile tells Skaffold when to apply these debug-specific changes. Here, it’s active when your kubeContext is docker-desktop. This is useful for having separate configurations for local development versus CI/CD.
The patches are where the real transformation happens. Skaffold uses strategic merge patches to modify your existing Kubernetes manifest just for this profile. It adds the necessary port and changes the command to enable the Node.js inspector.
What most people don’t realize is that Skaffold doesn’t just modify the initial deployment. If your pod crashes and Kubernetes restarts it, Skaffold detects the new pod and re-establishes the port-forwarding. It’s a continuous debugging experience that survives pod restarts.
Once you’ve got debugging working, the next logical step is to explore how Skaffold handles hot-reloading your code while the debugger is attached.