Kubernetes Deploy
Deploy a service straight to a Kubernetes cluster as part of devx up. devx renders your manifests, applies them to the target cluster, and gates the rest of the DAG on the workload becoming ready. This is the skaffold-class deploy path: declare runtime: kubernetes on a service and devx owns the render → apply → readiness → teardown lifecycle.
Deploy vs. Spawn vs. Bridge
- Zero-Config Kubernetes (
devx k8s spawn) gives you a cluster. - Kubernetes Deploy (
runtime: kubernetes, this page) deploys your app onto a cluster. - Hybrid Bridge (
runtime: bridge) connects your local process to a remote cluster.
They compose: spawn a local cluster, deploy your app onto it, and bridge a remote dependency — all from one devx.yaml.
Overview
A runtime: kubernetes service points at a directory of manifests and a renderer. On devx up, devx:
- Validates
kubectland your cluster access (kubeconfig + context). - Ensures the target namespace exists (created idempotently if missing).
- Applies the rendered manifests with
kubectl apply—-kfor kustomize,-ffor raw. - Readiness-gates the DAG: blocks until the namespace's Deployments report
Available(kubectl wait --for=condition=Available deployment --all). The generic HTTP/TCP healthcheck is skipped — in-cluster readiness needs no port-forward. - Prunes what it applied on shutdown (
Ctrl+C/ cleanup).
Because devx shells out to kubectl, kustomize is built in — no extra tooling to install.
Architecture & Execution Flow
Below are the architectural component structure and the step-by-step execution flow of runtime: kubernetes.
Component Diagram (C4 Level 2)
Execution Lifecycle Flowchart
Prerequisites
kubectlinstalled and on your PATH- A reachable cluster and a kubeconfig/context with apply permissions
Any cluster works: a devx k8s spawn local cluster, a lima/k3s VM, kind/k3d, or a remote dev cluster. Point at it with context / kubeconfig (defaults to your current context).
Target the right cluster
runtime: kubernetes applies real manifests to whatever context it resolves. Pin context: explicitly when your kubeconfig holds production contexts, so devx up can never deploy to the wrong cluster.
Quick Start
Configuration in devx.yaml
services:
- name: payments-api
runtime: kubernetes
depends_on:
- name: postgres
condition: service_healthy
kubernetes:
manifests: ./deploy/k8s/overlays/local # kustomize dir (default), raw file/dir, or helm chart
renderer: kustomize # kustomize (default) | raw | helm
namespace: payments-dev # default: "default" (created if missing)
context: "" # default: current kube context
kubeconfig: "" # default: $KUBECONFIG, then ~/.kube/configRun
devx up📋 Starting tier 1: payments-api
☸️ Deploying payments-api (kustomize) ./deploy/k8s/overlays/local → ns/payments-dev
⏳ Waiting for payments-api deployments to become Available...
✅ payments-api deployed
✅ payments-api is healthy (deployment available)
✅ All services are running and healthy.Renderers
| Renderer | kubectl flag | Notes |
|---|---|---|
kustomize (default) | apply -k | manifests is a directory containing kustomization.yaml. Uses kubectl's built-in kustomize — no separate binary. |
raw | apply -f | manifests is a plain YAML file, or a directory of manifests. |
helm | — (uses helm) | Deploys via helm upgrade --install; manifests is a chart directory. Supports release + values (see below). Requires the helm CLI on your PATH. |
Helm
For renderer: helm, manifests points at a chart directory. devx runs helm upgrade --install (idempotent — re-running upgrades the release) and tears it down with helm uninstall on shutdown. The release name defaults to the service name (override with release); values lists values files (resolved against the service directory):
services:
- name: payments-api
runtime: kubernetes
kubernetes:
renderer: helm
manifests: ./charts/payments-api # chart directory
namespace: payments-dev
release: payments-api # optional (default: the service name)
values: # optional values files
- values-local.yamlRequires the helm CLI on your PATH. The readiness gate still applies — devx waits for the release's Deployments to become Available.
Cluster & Namespace Targeting
| Field | Default | Notes |
|---|---|---|
kubeconfig | $KUBECONFIG, then ~/.kube/config | An explicit path wins. |
context | current context | Pin this when your kubeconfig holds multiple (or production) clusters. |
namespace | default | Created idempotently if it doesn't exist. |
Relative manifests paths resolve against the service's directory, so multirepo includes work correctly.
Profiles
runtime: kubernetes participates in Environment Profiles. A common pattern is a base config with a lightweight local runtime and a k8s profile that flips a service onto a cluster:
services:
- name: payments-api
runtime: host
command: ["go", "run", "./cmd/api"]
port: 8080
profiles:
k8s:
services:
- name: payments-api
runtime: kubernetes
kubernetes:
manifests: ./deploy/k8s/overlays/local
namespace: payments-devdevx up --profile k8sThe profile's kubernetes: block is merged onto the base service (profile wins).
Building & Loading Images
devx can build your service images and load them into the cluster before applying, so runtime: kubernetes works with locally-built code — not just pre-published images. devx stands up a small in-cluster registry (a registry:2 Deployment on a fixed NodePort, in namespace devx-registry), builds each image with your configured container provider, and pushes it there. Reference the image in your manifests as localhost:30500/<name>:<tag> (with imagePullPolicy: Always) — every node pulls it from its own NodePort, so no per-node registry configuration is required.
services:
- name: payments-api
runtime: kubernetes
kubernetes:
manifests: ./deploy/k8s/overlays/local
images:
- name: payments-api # → pushed as localhost:30500/payments-api:dev
context: ./payments-api # build context (default ".")
dockerfile: Dockerfile # relative to context (default "Dockerfile")
tag: dev # default "dev"
# platforms: [linux/amd64, linux/arm64] # multi-arch (default: the builder's arch)Reference the built image in your manifests:
spec:
containers:
- name: payments-api
image: localhost:30500/payments-api:dev
imagePullPolicy: AlwaysOn devx up, devx builds and pushes before applying:
🗄️ in-cluster registry ready (push 100.x.y.z:30500 · ref localhost:30500)
🔨 building payments-api (lima)...
📦 loaded payments-api — reference it in manifests as localhost:30500/payments-api:devMixed-architecture clusters
If your cluster mixes amd64 and arm64 nodes, a single-arch image fails to pull on the other architecture (no matching manifest for ...). Build multi-arch with platforms: [linux/amd64, linux/arm64], or pin pods to a matching arch via a kubernetes.io/arch nodeSelector.
Building requires a working container engine for your devx provider (podman, docker, or a Lima/Colima VM with nerdctl).
Live-reload (file sync)
kubernetes.sync keeps local source in sync with the running pod for hot-reload: after deploy, devx copies each src into the matching pod and re-copies it on change (polled ~1s), so an in-pod watch process (tsx watch, nodemon, air, …) reloads. The watchers stop on shutdown.
services:
- name: payments-api
runtime: kubernetes
kubernetes:
manifests: ./deploy/k8s/overlays/local
sync:
- src: ./src # local dir (relative to the service dir)
dest: /app/src # path inside the pod
selector: app=payments-api # pod label selector
# container: payments-api # optional (default: the pod's first container)The pod's image needs tar (used by kubectl cp); node_modules, .git, dist, build, etc. are skipped; and the in-pod process is responsible for reloading on the synced changes (run it in watch mode).
Port-forwarding
Set port_forward: true and devx automatically discovers the Services in the namespace after deploy and runs kubectl port-forward for each — mapping every Service port to a (conflict-resolved) local port, with no need to enumerate them. Each mapping is printed (localhost:<port> → svc/<name>:<port>), and the forwards are torn down on shutdown.
services:
- name: payments-api
runtime: kubernetes
kubernetes:
manifests: ./deploy/k8s/overlays/local
port_forward: trueBridge vs. auto port-forward
This is the simple "reach my just-deployed Services locally" case. devx's bridge remains the tool for connecting to remote/existing cluster services and for traffic interception (steal), which is beyond port-forwarding.
Requires socat on the nodes
kubectl port-forward needs socat on the cluster nodes to carry traffic. Without it the forward binds locally but connections fail (socat not found / lost connection to pod). This applies to bridge too.
For devx-provisioned clusters, socat is part of the node baseline and is installed automatically — new clusters get it during provisioning, and existing clusters can adopt it (without a rebuild) by running devx cluster reconcile. On clusters devx didn't provision, install socat on each node yourself.
Lifecycle & Cleanup
The deploy is a first-class DAG node: it respects depends_on ordering, gates dependents on readiness, and is torn down in reverse order on shutdown. devx records exactly what it applied (resolved manifests path, renderer, namespace, context) and runs kubectl delete … --ignore-not-found on exit. The namespace itself is left in place.
Limitations & Roadmap
- Readiness gate waits on
Deployments in the target namespace. Workloads that ship onlyStatefulSet/Job/CronJob, or manifests that hardcode a different namespace, aren't covered by the gate yet. - Live-reload uses ~1s polling +
kubectl cp(whole-directory) — fsnotify-based instant sync and per-file copies are a future optimization.
