Show HN: Kaupang – a push-based deploy CLI, now with a drag-and-drop builder Kaupang, a push-based deploy CLI for Docker Compose, Swarm, and Kubernetes, has been released with a drag-and-drop builder. The tool allows developers to define environments in TypeScript and deploy the same configuration locally and in CI, with immutable, auditable deploys and rollback support. It targets software teams seeking a simple, imperative deployment workflow without a control loop. kaupang KOW-pang β€” Old Norse for a Viking-agetrading hub, fromkaup"trade" +vangr"field, place." A kaupang was where goods from every craft were gathered, packed, and loaded onto ships bound for distant shores. This tool is that hub for your software: it gathers your services, pins them into a sealed, portable cargo, and dispatches them anywhere β€” your laptop, a CI agent, a remote swarm, or an airgapped site across the water. πŸ›Ά Spin up environments on Docker Compose, Docker Swarm, or Kubernetes from a single TypeScript config β€” the same command on your laptop and in CI. kaupang is an imperative, push-based deploy tool β€” an environment maker , not a reconciler. You run it, it makes a target match your config, it records exactly what it did, and you can replay or roll back. No control loop, no cluster agent. kaupang up api --target staging build the plan, resolve images, deploy kaupang up api --dry-run show the dependency graph + commands, run nothing kaupang rollback api --target prod re-apply the previous good deployment kaupang down api tear it back down One tool, laptop to pipeline. kaupang up api --target staging is byte-for-byte the same locally and on an agent. Only the target context, env, backend differs. Config is code. Environments are TypeScript, so image tags and env values can come straight from process.env / pipeline variables β€” no templating language. Immutable, auditable deploys. Every deploy resolves image tags to a digest, pins it, and records the whole artifact in a ledger. rollback replays a known-good snapshot. Promotion is trivial. Staging validates a digest; prod redeploys that exact digest. "What I tested is what I ship." Multi-backend. Swarm staging and Kubernetes prod from the same environment definition. Quick start quick-start Config config Environments & services environments--services Catalog shared presets catalog-shared-service-presets Targets staging vs prod targets-staging-vs-prod Solutions & bundles solutions--bundles Pipelines pipelines Single-service repo using-kaupang-in-a-single-service-repo-python-c-anything Versioning: resolve, pin, record, roll back versioning-resolve-pin-record-roll-back Secrets secrets Pull behavior pull-behavior Build & hooks build--hooks Dependency resolution dependency-resolution Backends backends Using kaupang in Azure DevOps using-kaupang-in-azure-devops Command reference command-reference Develop develop Roadmap roadmap npm install -g @kaupang/cli or use via npx in CI Create a kaupang.config.ts at your repo root and an environments/ folder: js // kaupang.config.ts import { defineConfig } from "@kaupang/core"; export default defineConfig { environments: "./environments", project: "longhall", defaultBackend: "compose", } ; js // environments/api.ts import { defineEnvironment } from "@kaupang/core"; export default defineEnvironment { services: { web: { image: "ghcr.io/midgard/longhall-api:latest", ports: "8080:3000" }, }, } ; kaupang up api deploys locally with docker compose kaupang up api --dry-run preview the plan first js import { defineConfig } from "@kaupang/core"; export default defineConfig { environments: "./environments", // folder of environment files project: "longhall", // namespaces stacks / projects / namespaces defaultBackend: "compose", // compose | swarm | kubernetes dockerRepository: "ghcr.io/midgard", // prefix applied to bare image names defaultPull: "missing", // always | missing | never cacheDir: ".kaupang", // where generated files + the ledger live globalEnv: { TZ: "UTC" }, // injected into every service everywhere catalog: { sources: { type: "file", path: "./catalog.json" } }, targets: { / see Targets / }, } ; Env var precedence low β†’ high : globalEnv < environment env < target env < service env . Not a TypeScript repo?You don't need one. kaupang loads its config from kaupang.config.ts , .mjs , .js ,or and .json environments/ in any of those β€” no tsconfig , no typescript dependency, no build step. Pick what fits: β€” full type-checking + autocomplete the extension is transpiled at runtime . .ts β€” plain JavaScript objects, with comments and the .js / .mjs secret / use helpers. Add / @type {import 'kaupang' .KaupangConfig} / for checking.β€” pure data, ideal for machine-generated configs. The helpers become literal tags: .json secret "X" β†’ { "$secret": "X" } , use "postgres" β†’ { "$catalog": "postgres" } . No comments, and you write the tags by hand. The define helpers are identity functions, so a plain object β€” or a JSON file β€” is all they ever produce. An environment is a docker-compose -like definition, but in TypeScript. The ergonomic surface is deliberately small: js import { defineEnvironment, use } from "@kaupang/core"; export default defineEnvironment { dependsOn: "postgres", // string or string β€” other environments services: { db: use "postgres" , // a whole service from the catalog web: "longhall-api:latest", // string shorthand β†’ { image: "longhall-api:latest" } worker: { image: "longhall-api", dependsOn: "web" }, }, } ; - A service can be a plain string the image , a full object , or a catalog reference via. use dependsOn accepts a string or an array, at both the environment and service level.- With dockerRepository set, bare names longhall-api get prefixed ghcr.io/midgard/longhall-api . Fully-qualified names anything with a / and catalog presets are left alone, so public images stay public. Because the file is TypeScript, dynamic values just work: web: { image: longhall-api:${process.env.LONGHALL API TAG ?? "latest"} } That single line is what lets a pipeline pin a build by setting one env var. Reusable service definitions that live outside your repo and are pulled in at load time β€” so a brand-new environment is a few use calls plus your own service. A catalog is a manifest of named presets: { "services": { "postgres": { "image": "postgres:16-alpine", "ports": "5432:5432" , "healthcheck": { "test": "CMD-SHELL", "pg isready" } }, "redis": { "image": "redis:7-alpine", "ports": "6379:6379" } } } Reference presets with use "name", overrides? . Sources are pluggable src/catalog/source.ts implements CatalogSource ; earlier sources win: | source | status | use it for | |---|---|---| file | shipped | a manifest committed to a shared infra repo / vendored | http | shipped | a static manifest served by an internal endpoint | service | shipped | a running internal catalog service kaupang queries | oci | shipped | a versioned catalog published as an OCI artifact ORAS | The service source a long-running internal catalog β€” e.g. a container your platform team owns just needs to answer a GET with JSON in this shape; both keys are optional, so one service can serve presets and solutions together: // GET https://catalog.internal/ β†’ { "services": { "postgres": { "image": "postgres:16-alpine", "ports": "5432:5432" } }, "solutions": { "hedeby": { "version": "2.3.1", "environments": "api" , "pins": { "api.web": "longhall-api@sha256:…" } } } } catalog: { sources: { type: "service", url: "https://catalog.internal", headers: { Authorization: "Bearer …" } } } That single service is the "container that handles all things" β€” your shared service presets and the solution recipes teams compose on the fly. If you'd rather version the catalog as an immutable artifact in a registry you already run ACR, Harbor, ghcr, ECR … , publish it with ORAS https://oras.land and reference it by ref . No new infrastructure β€” it reuses your image registry. 1. Author the manifest services and/or solutions . cat catalog.json <<'JSON' { "services": { "postgres": { "image": "postgres:16-alpine", "ports": "5432:5432" } }, "solutions": { "hedeby": { "version": "2.3.1", "environments": "api" } } } JSON 2. Authenticate to the registry or docker login β€” oras reuses it . oras login midgardregistry.azurecr.io -u