Developing

Codebase

shed is one Go module with a React dashboard inside it. Each package under internal/ is a self-contained piece, and cmd/shed is the only place that knows how they fit together.

Repository layout

repository
cmd/shed/          entry point: flags, config, logging, wiring, shutdown
internal/
  config/          koanf → Config
  store/           SQLite, embedded migrations, CRUD on plain structs
  docker/          Docker Engine wrapper (moby client)
  build/           clone a commit, build an image (Dockerfile or Railpack)
  proxy/           embedded Caddy, Apply(routes)
  github/          GitHub App: manifest, tokens, repos, CI status, OAuth, webhooks
  vars/            ${{ KEY }} / ${{ service.KEY }} reference resolution
  catalog/         database templates
  deploy/          deployment pipeline, per-service workers, reconcile on boot
  host/            host resource usage from procfs, sysfs, and statfs
  logtail/         in-memory tail of shed's own log
  metrics/         container and host resource sampling, time series
  s3/              S3-compatible object storage client
  backup/          backups and restores of service data and shed.db
  update/          release checks, verified downloads, replacing the binary
  auth/            sessions, GitHub sign-in, middleware, OAuth server for agents
  control/         operations and rules behind every interface
  mcp/             read-only MCP server for agents, mounted at /mcp
  api/             HTTP API, SSE logs, webhook, SPA serving
web/               dashboard (Vite+, React, TanStack Router + Query, CSS modules)
  embed.go         embeds web/dist into the Go binary
  site/            public site: home page and docs, prerendered to static HTML
deploy/            systemd unit and example config
docs/design.md     data model, deploy pipeline, HTTP API contract

docs/design.md is the source of truth for the data model, the deploy pipeline, and the HTTP API contract between Go and the dashboard. When you change any of those, update it in the same change. web/src/api/types.ts mirrors the types in it, so those two files change together too.

Packages

The last column is the real import graph, taken from go list. Leaf packages import nothing from internal/.

PackageResponsibilityImports from internal/
cmd/shedFlags, config, logging, wiring, signals, shutdown.everything
configLoads TOML and SHED_* env into a Config with defaults and validation.none
storeSQLite (modernc), embedded migrations, CRUD on plain structs.none
dockerDocker Engine operations through the moby client.none
buildClones a commit and builds an image from a Dockerfile or with Railpack.none
proxyEmbedded Caddy. Apply(routes) swaps the active route table.none
githubGitHub App: manifest, JWT, installation tokens, repos, CI status, OAuth, webhooks.none
varsResolves ${{ KEY }} and ${{ service.KEY }} references.none
catalogDatabase templates: image, port, volume path, default variables.none
hostHost CPU, memory, network, disk I/O, and filesystem usage.none
logtailIn-memory tail of shed's own log, followed over SSE.none
s3S3-compatible object storage client for off-site backups.none
updateRelease checks, verified downloads, replacing the shed binary.none
deployDeployment pipeline, per-service workers, reconcile on boot.build, catalog, docker, github, proxy, store, vars
metricsContainer and host sampling, per-service and host time series.docker, host, store
backupDumps, archives, zstd, age, schedule, retention, upload, restore.docker, store
authSessions, GitHub sign-in, middleware, OAuth 2.1 server and bearer middleware.github
controlOperations on projects, services, deployments, backups; their validation rules.backup, build, catalog, deploy, github, logtail, metrics, store, update
mcpRead-only MCP server for agents: tools, compact results, instructions.control, github, metrics, update
apiJSON API, SSE logs, webhook endpoint, SPA serving, /mcp mount. Calls control.auth, backup, control, deploy, github, metrics, store, update

web is a Go package too, but only to embed the built dashboard. Its Dist() returns the files as an fs.FS.

cmd/shedbuilds each piece and injects the real implementationsapiHTTP · SSE · webhookcontroloperations and rulesmcpread-only agent toolsauthgithubdeploy7 leaf packagesmetricsdocker host storebackupdocker storeleaf packages · import nothing from internal/storedockerbuildproxygithubvarscataloghosts3configlogtailupdateapi, control, and mcp also import store, github, update, and other leaf packages directly.
Arrows point from a package to what it imports. cmd/shed imports all of them and is not drawn with arrows.
Who imports whom
Imports
buildcatalogdockergithubproxystorevars
Imported by
cmd/shedapicontrol

deploy declares small interfaces for what it needs, and cmd/shed passes in the real implementations.

Most imports from a consumer are for plain types, such as store.Service. The behavior a consumer needs from another package goes through an interface it declares itself, as the next sections describe.

Design principles

  • Leaf packages import nothing from internal/. That is what lets you understand, test, and replace one on its own. Keep it that way when you add code.
  • Interfaces belong to the consumer. deploy.Docker, backup.Remote, auth.Store, and control.Deployer are small and name only what that package calls. Tests pass fakes.
  • Dependencies are passed in. No package-level state, no init side effects, no global loggers. Each constructor takes a Config struct and a *slog.Logger.
  • Caddy is the one exception. Caddy keeps process-global state, so internal/proxy contains it, and nothing else touches Caddy.
  • APIs stay narrow. Export only what callers use. Prefer plain structs and functions over frameworks.
  • New code goes where it belongs. Put it in the package that owns the concern, or in a new leaf package if it is separable. There is no util package.

Wiring in cmd/shed

cmd/shed/main.go builds every piece in dependency order and hands each one the real implementations of the interfaces it declared. The order is: config, logger, data directories, store, Docker client, proxy, deployer, backups, metrics collector, updater, auth, control plane, MCP server, API server. Then it recovers interrupted restores, reconciles services, starts the background loops, and serves HTTP until the process gets a signal.

Most real types satisfy the consumer's interface directly: the one *docker.Client is passed to deploy, backup, and metrics, each of which sees only its own small interface. Where the shapes differ, main.go uses a small adapter:

  • backupServices adapts the *deploy.Deployer to backup.Services, which lets a backup hold a service while it dumps data.
  • api.AuthStore(st) adapts the store to auth.Store.
  • A NewRemote closure converts backup.S3Config to s3.Config, so backup never imports s3.
  • A GitHub closure returns the client from api.GitHubHolder, or false before the GitHub App is set up. deploy, auth, and control call it each time instead of holding a client.
  • deploy.Proxy is left nil when proxy.enabled is false, which turns routing off.
cmd/shed/main.go (abridged)
var routes deploy.Proxy // nil disables routing
if cfg.Proxy.Enabled {
    routes = proxy.New(proxy.Config{ /* ports, ACME email, storage */ })
}
 
deployer := deploy.New(deploy.Config{
    Store: st, Docker: dc, Builder: &build.Builder{ /* ... */ },
    Proxy: routes, GitHub: githubFromHolder, Log: log,
})
backups := backup.New(backup.Config{
    Store: st, Docker: dc, Services: backupServices{deployer},
    NewRemote: func(c backup.S3Config) (backup.Remote, error) {
        return s3.New(s3.Config(c))
    },
})

To add a new capability, write it as a package with its own interfaces, then add the wiring here. If it needs to be visible in the dashboard, add the operation to internal/control, a thin handler for it to internal/api, and describe it in docs/design.md. For how the larger pieces work inside, read Internals.