2026-08-30 19:34:29 +07:00
2026-08-30 00:23:53 +07:00
2026-08-30 19:34:29 +07:00
2026-08-08 22:23:27 +07:00
2026-08-30 19:34:29 +07:00
2026-08-08 22:23:27 +07:00
2026-08-08 22:23:27 +07:00
2026-08-08 22:23:27 +07:00
2026-08-30 00:06:52 +07:00
2026-08-08 22:23:27 +07:00
2026-08-30 19:34:29 +07:00
2026-08-30 19:34:29 +07:00
2026-08-30 19:11:50 +07:00
2026-08-30 19:11:50 +07:00

kuber

kuber is Astral's internal Docker Compose to Kubernetes translation layer.

It reads a local Compose file, renders Kubernetes resources, applies them to the cluster, and can build images remotely from your working tree without requiring Docker on your machine.

What It Does

  • Reads compose.yml or docker-compose.yml
  • Converts supported Compose services into Kubernetes resources
  • Applies those resources into a namespace named after the current directory
  • Turns env_file entries into Kubernetes Secrets and mounts them through envFrom
  • Translates file mounts into ConfigMaps and directory/volume mounts into PVC-backed volumes
  • Builds images on the remote builder by syncing:
    • committed git state
    • tracked local diffs
    • untracked files
    • ignored .env* files
  • Supports host-based ports syntax that renders Kubernetes Ingress rules
  • Supports managed Postgres claims through special pseudo-volumes such as postgresql:app
  • Supports managed S3 buckets and credentials through pseudo-volumes such as s3:app

Environment Assumptions

This tool is built for Astral's cluster and workstation setup. It is not intended to work unchanged outside that environment.

Expected local setup:

  • Tailscale access to the remote builder and Kubernetes network
  • a working ~/.kube/config
  • ssh available locally

Not required locally:

  • Docker
  • kubectl

Docker is not required because builds happen on the remote builder after kuber syncs your repo state there. kubectl is not required because cluster access is handled through the bundled Kubernetes client.

Next.js Example

example/ contains a documented deployment template for adding kuber to an existing Bun-powered Next.js project without initializing or bundling an application in this repository. It includes a standalone-output Dockerfile, .dockerignore, compose.yml, and the required Next.js configuration.

Running

During development:

bun run index.ts up

Run the dedicated unit suite and type checks:

bun run test
bun run typecheck

Other useful commands:

bun run index.ts ps
bun run index.ts logs
bun run index.ts logs -f
bun run index.ts exec app sh
bun run index.ts start
bun run index.ts stop
bun run index.ts restart
bun run index.ts db ls
bun run index.ts s3 ls
bun run index.ts s3 creds app
bun run index.ts s3 ui app

All commands accept --config to use a configuration file other than .kuberrc.ts:

kuber --config deploy/production.kuberrc.ts up
kuber up --config deploy/production.kuberrc.ts

Shell Completion

Generate and load completions for your shell:

source <(kuber complete zsh)
source <(kuber complete bash)

For a permanent setup, write the generated script to a file and source it from your shell configuration. Fish and PowerShell are also supported through kuber complete fish and kuber complete powershell.

Commands

  • up: build images if needed, render manifests, apply them, restart deployments whose image content changed, and wait for rollout
  • start: same as up but skips image builds
  • stop: scale managed deployments to zero
  • restart: roll out a restart across managed deployments
  • down: delete managed resources while keeping ingress, PVCs, managed databases, and managed S3 storage
  • down -f: also delete ingress, PVCs, managed database and S3 resources, and the namespace
  • ps: list deployments in the current project namespace
  • logs [deployment]: print logs for one deployment or all managed deployments
  • logs -f [deployment]: follow logs continuously
  • exec <deployment> <command...>: execute a command inside a running deployment pod
  • db ls: list managed Postgres claims declared in the current Compose file
  • db creds <service>: print the generated connection details for a managed Postgres claim
  • s3 ls: list managed S3 claims declared in the current Compose file
  • s3 creds <service>: print all generated S3 environment variables for a service
  • s3 ui <service>: print the Garage UI object-browser URL for a service bucket

Configuration

Kuber optionally loads .kuberrc.ts from the working directory. The file must default export an object satisfying the published KuberConfig type:

import type { KuberConfig } from "@dmgnr/kuber";

export default {
  project: "my-app",
  composeFile: "compose.production.yml",
  registry: "registry.example.com",
  builders: {
    amd64: "kuber@amd-builder",
    arm64: "kuber@arm-builder",
    remoteRoot: "kuber-build",
  },
  rolloutTimeoutMs: 10 * 60_000,

  async compose(compose) {
    const app = compose.services?.app;
    if (app && !Array.isArray(app.environment)) {
      app.environment ??= {};
      app.environment.NEXT_PUBLIC_BUILD_ID =
        await Bun.$`git rev-parse --short HEAD`
          .text()
          .then((value) => value.trim());
    }
  },
} satisfies KuberConfig;

Operational defaults:

  • project: Compose top-level name, falling back to the current working directory name
  • composeFile: the first recognized Compose filename in the working directory
  • registry: registry.neko-piranha.ts.net
  • builders.amd64: kuber@astral-th
  • builders.arm64: kuber@astral
  • builders.remoteRoot: kuber-build
  • rolloutTimeoutMs: 300000

Project-name precedence is .kuberrc.ts project, Compose top-level name, then the current working directory name.

Configuration hooks can be synchronous or asynchronous and receive mutable values:

  • compose(compose, context): once after parsing and validation; affects every command that reads Compose
  • preBuild(compose, context): before build eligibility is evaluated when builds are enabled
  • postBuild(result, context): after images are built; result contains built and changed service names
  • postRender(resources, context): after rendering and before reconciliation planning; also runs for export
  • postApply(resources, context): after desired resources are successfully applied

Hook context contains the resolved cwd, project, composeFile, and optional configFile. A hook error aborts the command and is reported by the normal CLI error handler.

Compose Conventions

kuber supports a few project-specific Compose conventions on top of normal service translation.

Host-Based Ports

If a ports entry uses a hostname instead of a numeric published port, kuber treats it as an ingress host and routes traffic to the target container port.

Example:

services:
  app:
    ports:
      - somedomain.astrxl.dev:3000

That produces a Kubernetes Ingress rule for somedomain.astrxl.dev pointing at the service port for container port 3000.

Single-level wildcard subdomains are supported. Quote wildcard entries so YAML does not treat the leading * as an alias:

services:
  app:
    ports:
      - "*.astrxl.dev:3000"
      - "*.secure.astrxl.dev:3001:protected"

Protected routes use the kuber dialect and render Traefik IngressRoute resources instead of plain Kubernetes Ingress:

services:
  app:
    ports:
      - db.astrxl.dev:4984:protected
      - status.astrxl.dev:3001:protected(/dashboard,/socket.io)

Translation rules:

  • host:port -> Kubernetes Ingress
  • host:port:protected -> Traefik IngressRoute with middleware routing/cf-auth and host-wide matching
  • host:port:protected(path1,path2,...) -> Traefik IngressRoute with middleware routing/cf-auth and explicit PathPrefix(...) matches only

Managed Postgres

You can declare a managed Postgres database with a pseudo-volume:

services:
  app:
    volumes:
      - postgresql:app

Or with an explicit username and database name:

services:
  app:
    volumes:
      - postgresql:user/database

This creates or reuses the managed CNPG role secret, reconciles the database resource, and injects DATABASE_URL into the generated app secret in Kubernetes.

Managed S3

Declare a Garage S3 bucket and access key with a pseudo-volume:

services:
  app:
    volumes:
      - s3:app

This creates GarageBucket/app and GarageKey/app in garage-system. To use different key and bucket names:

services:
  app:
    volumes:
      - s3:app-key/shared-assets

The Garage operator generates the credentials. kuber reads its generated Secret and injects these values into the service's <service>-env Secret:

  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • AWS_ENDPOINT_URL_S3
  • AWS_REGION
  • S3_BUCKET

One service can declare both postgresql:... and s3:...; all generated values are merged into the same service Secret. Managed Garage buckets and keys are retained by normal down and deleted by down -f.

Inspect a claim, print its generated credentials, or get its Garage UI URL:

kuber s3 ls
kuber s3 creds app
kuber s3 ui app

s3 creds prints AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_ENDPOINT_URL_S3, AWS_REGION, and S3_BUCKET as shell-style environment assignments. s3 ui only prints the URL; it does not open a browser.

Environment Files

env_file entries are read locally and turned into a Kubernetes Secret named <service>-env. Deployments then consume that secret through envFrom.

This is also where generated values such as DATABASE_URL and the managed S3 environment are injected.

Volumes

kuber treats different volume shapes differently:

  • file bind mounts become ConfigMaps
  • directory bind mounts become PVC-backed mounts
  • named volumes become PVC-backed mounts
  • tmpfs becomes emptyDir with memory backing
  • postgresql:... is treated as a managed database claim, not as a filesystem mount
  • s3:... is treated as a managed object-storage claim, not as a filesystem mount

Named volumes also support kuber-specific storage hints.

Default behavior:

# compose
services:
  app:
    volumes:
      - myvolume:/data

# effective kuber interpretation
services:
  app:
    volumes:
      - myvolume(1Gi on 2 fast):/data

Short syntax:

services:
  app:
    volumes:
      - data(20Gi):/data
      - archive(200Gi on archive):/archive
      - cache(10Gi on 1 fast):/cache

Meaning:

  • name(20Gi):/path -> PVC size 20Gi
  • name(20Gi on archive):/path -> PVC size 20Gi, diskTag: ["archive"], dataLocality: "none"
  • name(20Gi on 1 archive):/path -> PVC size 20Gi, replicaCount: 1, diskTag: ["archive"], dataLocality: "none"

Explicit extensions are also supported.

Top-level named volume:

volumes:
  data:
    x-size: 20Gi
    x-diskTag: [archive]
    x-replicaCount: 1
    x-dataLocality: none

Long-form service mount:

services:
  app:
    volumes:
      - type: volume
        source: data
        target: /data
        volume:
          x-size: 20Gi
          x-diskTag: [archive]
          x-replicaCount: 1
          x-dataLocality: none

Precedence:

  • short syntax like data(20Gi on 1 archive):/data
  • long-form volume.x-*
  • top-level volumes.<name>.x-*
  • fallback default 1Gi on 2 fast

Building

Image builds and image-digest checks run through the selected SSH builder. If a pushed image has the same content fingerprint as the existing registry image, up does not restart that deployment.

To build distributable binaries:

bun run build.ts

If you only want a plain JavaScript bundle for quick local use in another workspace, build index.ts directly:

bun build index.ts --target bun --minify --sourcemap --outdir dist

Notes

  • Resource names and namespaces are derived from the current working directory.
  • The remote build flow is optimized for local iteration, not for producing a perfectly clean export of the repository.
  • Managed database support is Kubernetes-only. It injects DATABASE_URL into the generated app secret and does not rewrite local .env files.
  • kuber operates on managed resources in the namespace matching the current directory name.
S
Description
Docker Compose -> K8s translation layer
Readme
2.2 MiB
Languages
TypeScript 100%