diff --git a/src/content/blog/manifest.ts b/src/content/blog/manifest.ts index ac2519d..655e05f 100644 --- a/src/content/blog/manifest.ts +++ b/src/content/blog/manifest.ts @@ -14,6 +14,16 @@ export interface PostMeta { } export const POSTS: PostMeta[] = [ + { + slug: "why-shipdeck-replaced-docker", + title: "Why ShipDeck Replaced Docker at DashCaddy: One Command to Deploy", + description: + "ShipDeck is the deployer DashCaddy uses for its own services — a single Go binary that replaces Docker, Docker Compose, and handwritten deploy scripts with one command. Here's what it does, why Docker wasn't the right fit for fleet deployments, and what makes ShipDeck better.", + date: "2026-10-08", + author: "DashCaddy Team", + tags: ["shipdeck", "docker alternative", "deployment tool", "fleet deployer", "systemd deployment", "self-hosted deployment"], + readingMinutes: 7, + }, { slug: "getting-started-with-dashcaddy", title: "Getting Started with DashCaddy: From Blank Server to a Running App", diff --git a/src/content/blog/why-shipdeck-replaced-docker.md b/src/content/blog/why-shipdeck-replaced-docker.md new file mode 100644 index 0000000..2497edf --- /dev/null +++ b/src/content/blog/why-shipdeck-replaced-docker.md @@ -0,0 +1,153 @@ +--- +title: "Why ShipDeck Replaced Docker at DashCaddy: One Command to Deploy" +description: "ShipDeck is the deployer DashCaddy uses for its own services — a single Go binary that replaces Docker, Docker Compose, and handwritten deploy scripts with one command. Here's what it does, why Docker wasn't the right fit for fleet deployments, and what makes ShipDeck better." +date: "2026-10-08" +author: "DashCaddy Team" +tags: ["shipdeck", "docker alternative", "deployment tool", "fleet deployer", "systemd deployment", "self-hosted deployment"] +readingMinutes: 7 +--- + +When you visit dashcaddy.net, or the DashCaddy dashboard hits its license server, or a monitoring check pings one of our backend services — most of those reached production through ShipDeck. It is the deployer DashCaddy uses for its own fleet services, and it replaced Docker for those deployments. + +ShipDeck is a single Go binary. It takes a source repo and a TOML config file, then runs the full pipeline: build → package → SSH transfer → systemd install → Caddy reverse proxy → DNS record → live verification — one command, and the prior release is preserved so rollback is one command away after the first successful deploy. + +This is the story of why we built it and why Docker wasn't the right tool for that job. + +## What DashCaddy needed from a deployer + +DashCaddy runs as a constellation of services across multiple hosts. There's the product dashboard itself, a license server, monitoring agents, and auxiliary tools that talk to each other over Tailscale. Each service needs: + +- A build step that produces a binary or static bundle +- A way to land that artifact on the right host +- A systemd unit so it stays running after a reboot +- A Caddy reverse-proxy block with a proper TLS certificate +- A DNS record that resolves in public and over the tailnet +- Health checks that confirm the deploy worked before it is marked done + +That is six steps that have to happen in order, and every step can fail. Before ShipDeck, each of these was a hand-run script or a Docker Compose file that covered maybe three of the six. + +## Why Docker wasn't the right tool + +Docker is excellent at running isolated processes. It solves the "works on my machine" problem by shipping the entire filesystem along with the process. For a homelab running third-party apps — Vaultwarden, Immich, a media server — that isolation is genuinely useful. Those apps ship as Docker images and updating them is a `docker pull` and a restart. + +But for your own software, which you already build from source, the container adds overhead that may not be worth the trade-off. + +**The Docker daemon is another moving part.** Every Docker deploy requires the daemon to be running, healthy, and configured the same way on every host. It consumes memory even when idle. A systemd unit needs only systemd — which is already running on every Linux host and has been for a decade. + +**Docker images can be harder to debug.** When something goes wrong inside a container, the debugging path is `docker exec` into a filesystem that may not have the tools you need. A bare binary under systemd gives you `journalctl`, `coredumpctl`, and your own tooling — no extra layer. + +**Docker Compose has no built-in rollback.** If you push a bad image and the container fails to start, Compose leaves you to fix it by hand. ShipDeck's blue-green release directories mean the previous working release is still sitting right there, and `shipdeck rollback ` swaps back to it — provided a prior successful deployment is recorded in the journal. + +**Docker has no DNS layer.** ShipDeck's DNS step calls an operator-configured hook (`SHIPDECK_DNS_CMD` — in DashCaddy's fleet, this is wired to `fleet-dns`) to register the service record and gates on the resolver confirming it resolves before marking the deploy done. Containers just expose ports; connecting a name to them is always a separate step. + +**Docker needs separate tooling for TLS.** ShipDeck writes and validates a Caddy block inline in the same deploy, with atomic swap and automatic rollback if the reload fails. With containers, you wire up Traefik or Caddy separately and hope the labels match. + +## What ShipDeck does instead + +A ShipDeck deploy runs these steps in order, stopping on the first failure: + +1. **Build** — runs the build command you defined in the `Shipdeckfile`, producing a binary +2. **Package** — bundles the binary and any assets into a release directory +3. **Transfer** — copies the release over SSH to the target host under `/opt//releases//` +4. **Install** — symlinks `current` → the new release, writes (or validates) the systemd unit, and restarts the service +5. **Caddy** — writes a candidate Caddy block, validates it with `caddy validate`, then atomically swaps it in and reloads +6. **DNS** — calls the operator-configured DNS hook (in our fleet, `fleet-dns add`), then gates on the DNS resolver confirming the record resolves +7. **Verify** — probes the HTTP health endpoint over the tailnet (or public URL) and confirms a `200` + +If the build, package, or transfer step fails, the active installation is unchanged. If a later step (install, Caddy, DNS, or verify) fails, the deploy stops but the prior release directory is preserved on disk. `shipdeck rollback ` swaps `current` back to it and restarts the service — provided a prior successful deployment is recorded in the journal. Caddy's last-good config is automatically restored from the pre-deploy snapshot on validation or reload failure — that part is unattended. Successful deploys append a completion row to `/var/lib/shipdeck/journal.jsonl` recording the sanitized repository URL and exact commit — `status` and `rollback` reference the journal, so neither needs the original checkout. + +## Reproducible deploys with commit pinning + +One feature that makes ShipDeck fundamentally different from Docker workflows: it pins Git commits. + +When you deploy from a Git source, the Shipdeckfile declares an exact 40-character lowercase commit hash. ShipDeck fetches that ref, checks out that exact commit, verifies `HEAD` matches, and builds from that tree. No ambiguity about what code is running in production. No "the image tag says `latest` but nobody remembers which commit built it." This is table stakes for fleet operations; Docker Compose offers no equivalent without a separate registry and tag discipline. + +## Blue-green with instant rollback + +Every deploy lands in a timestamped release directory: + +``` +/opt/myservice/releases/20261008T120000Z/ +/opt/myservice/releases/20261007T090000Z/ ← previous +/opt/myservice/current → releases/20261008T120000Z +``` + +The `current` symlink is the atomic switch. If the new release fails verification, `shipdeck rollback myservice` re-points it to the previous directory and restarts — provided a prior successful deployment is recorded in the journal. That previous release is never touched by the deploy, so the rollback is instant — no pulling old images, no rebuilding, no guessing what the last working version was. + +## No daemon. No registry. No layered filesystems. + +ShipDeck is a CLI tool, not a daemon. You run `shipdeck deploy .` from a checkout; it does the work and exits. There is no persistent shipdeck process to monitor, upgrade, or debug. The journal is a flat JSONL file. The host registry is a TOML file at `/etc/shipdeck/hosts.toml`. + +This means ShipDeck can be called from CI, from a Makefile, from a cron job, or from another CLI tool (DashCaddy's Deploys panel calls ShipDeck as its engine). Docker, by contrast, needs the daemon running on every host that builds or runs containers — and if the daemon is down, the pipeline is dead. + +## When to keep using Docker + +None of this means Docker is bad. Docker is the right choice for: + +- Running third-party apps that already ship as Docker images +- Services that need filesystem isolation from the host +- Development environments where containerization is part of the workflow + +DashCaddy itself still uses Docker for the apps it deploys — the 77 templates in the catalog all run as Docker containers. ShipDeck replaces Docker for *DashCaddy's own services*: the dashboard, the license server, the monitoring stack. Software we build from source and deploy to our own fleet. + +If you self-host apps with DashCaddy, Docker is still your runtime. If you want to deploy your own Go or Node services across your fleet the way DashCaddy does, ShipDeck is the tool. + +## Getting started with ShipDeck + +ShipDeck ships as a single binary. The target host needs an SSH server and `systemd`; the invoking machine needs the SSH client. Caddy and the DNS hook (`SHIPDECK_DNS_CMD`) are configured per-deployment — ShipDeck works without either for local-mode deploys, but in fleet operation both are typically wired in. Build-time tools (like Go for Go projects) run on the machine that invokes ShipDeck. There is no persistent daemon or agent on either side. + +```bash +# Register a fleet host +shipdeck host add edge-1 --address 100.64.1.10 --user root --key ~/.ssh/edge-1 + +# Scaffold from a template +shipdeck init --template=go + +# Fill in the Shipdeckfile: +# [service] +# name = "myservice" +# repo = "https://git.example.com/me/myservice.git" +# ref = "main" +# commit = "0123456789abcdef0123456789abcdef01234567" +# build_cmd = "go build -buildvcs=false -o bin/myservice ./cmd/myservice" +# binary = "bin/myservice" +# +# [deploy] +# host = "edge-1" +# systemd_unit = "myservice.service" +# port = 8901 +# +# [caddy] +# block = \"\"\" +# myservice.sami { +# reverse_proxy 127.0.0.1:8901 +# } +# \"\"\" +# tailnet_only = true +# +# [dns] +# zone = "sami" +# record = "myservice.sami" +# target = "100.64.1.10" +# +# [verify] +# http = "https://myservice.sami/health" +# timeout = 30 + +# Set the DNS hook in your environment +export SHIPDECK_DNS_CMD="fleet-dns" + +# Dry-run, then deploy +shipdeck plan . +shipdeck deploy . +``` + +Omitting `[caddy]` and `[dns]` selects local mode — the service runs on the target host without TLS or DNS registration. + +After a successful deploy, the service is live with the configured Caddy TLS block and DNS record. `shipdeck status myservice` re-probes everything and reports health. `shipdeck rollback myservice` swaps to a prior successful deployment — provided one is recorded in the journal. + +## One command, not six + +The thing that made Docker the wrong tool for DashCaddy's fleet wasn't any single missing feature. It was the gap between "I built this binary" and "it is running behind TLS on every host with a DNS record and health checks." Docker closed some of that gap. ShipDeck closes all of it in one command. + +More importantly, it closes it in a way that can be rolled back, verified, and replayed — which is what fleet operations actually needs. \ No newline at end of file