Skip to main content
01Technical deep-dive

How I built
Build & Ship.
Five layers, no orchestrator.

A walk through the architecture of Build & Ship by Yashesh Bharti, the peer-to-peer deployment platform: framework detection, reliability scoring, three-layer health probing, WireGuard mesh, and the blue-green swap engine. Roughly 15,000 lines of Go, zero third-party orchestrators.
By
Yashesh Bharti
Published
26 May 2026
Read
12 min

The build is twenty seconds. The deploy is two hours. That ratio is the deployment tax, and it is the only reason this project exists. A developer can write a working app with AI in twenty minutes and then spend an afternoon picking a cloud, configuring a project, setting environment variables, learning a vendor's deploy graph, hitting a free-tier ceiling, and finally paying a monthly bill for something that used to be a single binary on a single box. Build & Ship treats that tax as a bug.

The premise: shipping software to your own hardware should be one command. Not one command per provider. One command, full stop. bs deploy takes the project in your current directory and ships it. Auto-detects the framework, builds the container, swaps it in behind a health check, hands you a URL. Average end-to-end time on a warm node is about eight seconds.

That experience sounds like a wrapper around something. It is not. The temptation when building a deployment tool is to lean on Kubernetes, or Terraform, or one of the higher-level orchestrators that already solve scheduling. I considered it for about a day and then rejected it. The rest of this essay is why, and what we built instead.

01The wrong answer

Kubernetes for one

Kubernetes solves scheduling at the scale of large fleets running heterogeneous workloads. It is the right answer for that problem. It is the wrong answer for the problem most of us actually have, which is "run my app on this server and a backup, and put a URL on it." The cognitive overhead of running Kubernetes on a single-digit fleet is not paid back. You inherit a control plane, a CNI, an ingress controller, a cert manager, a Helm tree, a yaml taxonomy, and a fleet of operators, all to serve a deploy graph that fits in a 200-line Go file.

The reflex is to say "but you might scale". Most of the time you do not. And if you do, you scale the deploy primitives, not the orchestrator. Build & Ship's answer is that the orchestrator should be the smallest piece that can do the job and nothing else.

02What we built instead

Five purpose-built layers

Build & Ship is five layers of Go that compose into the deploy experience. Each layer does one thing. No layer reaches across another's boundaries. The whole stack is about 15,000 lines of code.

03Layer 1

Framework detection

The CLI's first job is to figure out what kind of project it is looking at. The detector reads the working directory, fingerprints by file presence and content, and matches against a table of about fifteen framework profiles: Next.js, Remix, Nuxt, SvelteKit, Astro, Vite, static HTML, Express, Django, Flask, FastAPI, Go, Rust, and Docker. Internal accuracy across the projects we have run is about 97%. The remaining 3% are cases where someone's repo layout breaks a convention; we surface a clear error rather than guess.

If the project has no Dockerfile, the detector generates one tuned to the framework. Next.js gets the standalone build with the right port; Django gets gunicorn with the right module path; Go gets a multi-stage build with a static binary. The generated Dockerfile is committed to a hidden working directory, not the user's repo, unless they ask to write it.

The detector deliberately does not try to be exhaustive. We support the long tail by accepting a user-supplied Dockerfile and skipping detection entirely.

04Layer 2

Node reliability scoring

When there are multiple nodes in your swarm, something has to decide which one runs which workload. The scoring layer assigns each node a 100-point reliability score derived from six factors:

  • Uptime, weighted by the most recent 30 days.
  • Heartbeat consistency, measured against the expected 1-Hz tick.
  • Resource pressure at the moment of scoring (CPU, memory, bandwidth).
  • Network reachability to a fixed set of probes.
  • Historical deploy success rate as a node.
  • Geographic proximity to the user's declared region preference.

The placement algorithm sorts candidates by score, then applies a tie-breaker for anti-affinity (do not put both replicas of a service on the same node), and routes the build there. The point of the scoring system is not to be optimal under load; it is to make a sensible default for non-pathological cases and to be obviously interpretable when it picks wrong. Every placement decision is logged with the score breakdown so you can answer "why did this go there".

05Layer 3

Three-layer health probing

A deploy is only as good as the moment it goes live. That moment is governed by health probes. Build & Ship runs three of them, in parallel, and aborts the swap if any fails for more than fifteen seconds:

  • WebSocket heartbeat from the node agent. If the node stops ticking, the swap pauses.
  • HTTP probe against the new container's declared health endpoint, or against / if none is declared. 200-class response required.
  • Docker inspect on the running container. Has to be in running state, not restarting or exited.

All three have to be green before traffic shifts. If any one degrades during the swap window, the swap reverses and the old container keeps serving. The user sees a single error, not a half-deployed app.

06Layer 4

WireGuard mesh

Nodes talk to each other and to the relay over WireGuard. There is no separate VPN to install. The first time a node joins the swarm, it provisions a keypair, registers its public key with the relay, and accepts a config that wires it into the mesh. From that point on, every byte between the relay, the CLI, and the node agent rides inside the WireGuard tunnel.

The relay is intentionally dumb. It holds short-lived routing tokens, knows where each node is reachable, and forwards control messages. It cannot decrypt application traffic and it does not hold customer secrets, customer code, or customer logs. If the relay disappears tomorrow, the user's app keeps serving traffic; only new deploys break. We can replace the relay without touching the data plane.

This shape is what makes "free forever" honest. We are not subsidising compute. The user is providing the compute. We are running the smallest possible control plane.

07Layer 5

Blue-green swap engine

The swap engine is the part users actually see. It runs the new container on the same node next to the old one, waits for the three health probes to all go green, atomically flips the upstream in the local reverse proxy, drains existing connections for a configurable grace window (default 15 seconds), and stops the old container. Blue-green as Martin Fowler originally described it, with two running production environments and a router flip, but at the single-node granularity that fits this product.

If the swap engine catches a failure inside the grace window, it reverses. No traffic was ever cut over, no clients were affected, the old container is still warm. The failure is logged and surfaced to the CLI as a single error with the actionable reason.

The hard problems in a swap engine are not the happy path. They are the partial-failure cases: a probe goes green, then orange, then green again; a container fails its health check after the swap completed but before the grace window ended. Those cases are easy to make safe and hard to make obvious. The swap engine logs every state transition with timestamps so "what happened during this deploy" is always answerable from the audit log.

08Why Go

A language choice that earned itself

Go was the right call for four reasons. First, the cross-compilation story: a single Go toolchain builds a Linux node agent, a macOS CLI, and a Windows binary from one machine, with no runtime to install on the target. Second, the concurrency model: every layer above is full of long-running goroutines (probes, heartbeats, swap state machines) that compose cleanly with channels. Third, the static binary: bs is 8 MB and installs with a curl pipe. Fourth, the standard library: the things you need most (http server, json, crypto, exec) are all in there.

About 15,000 lines of Go covers the full stack: detector, scheduler, swap engine, node agent, relay, web dashboard, and CLI. The dashboard is a Next.js app served by a separate process, but everything else is the same binary running in different modes.

09Surface design

One binary, three surfaces

Build & Ship deliberately ships three surfaces over the same engine. bs deploy from the terminal for engineers. A VS Code extension that wraps the same call for editors. A macOS Finder right-click for non-engineers on the same team. Same engine, same audit log, same outcome. The product's shape is the shape of its smallest piece, repeated across the three places it has to live.

10What I would do again

And what I would not

Again: custom layers, Go, three surfaces over one engine, audit-everything by default. The clarity of a five-layer architecture pays back every time something goes wrong, because each layer's failure mode is contained.

Not again: trying to make the framework detector cover every long tail. We spent a real amount of effort on edge cases that would have been better handled by accepting a user-supplied Dockerfile from day one. The 80% case rewards depth; the long tail rewards a clean escape hatch.

More on this stack: Build & Ship case study. Related reading: software engineering work by Yashesh Bharti and PulseProof technical architecture.
Published · Last updated
← Back to indexWriting / by Yashesh Bharti