Skip to content

Architecture overview

SimpleDeploy is a single Go binary. There is no agent, no sidecar, no separate proxy process. Caddy runs as an in-process library, not a child process. SQLite is the only datastore. Everything else is a goroutine inside one PID.

cmd/simpledeploy/ CLI entrypoints (cobra), wires every subsystem
internal/
api/ REST + WebSocket handlers, middleware, routes
auth/ bcrypt, JWT, API keys, rate limit, AES-GCM crypto
alerts/ rule evaluator + webhook dispatch (SSRF-guarded)
backup/ Strategy + Target interfaces, cron scheduler
client/ HTTP client used by the CLI
compose/ Compose YAML parser + label extraction
config/ YAML config loader
deployer/ shells out to `docker compose` via CommandRunner
docker/ Docker SDK wrapper + MockClient
logbuf/ ring buffer io.Writer + WS fan-out
metrics/ Docker stats + gopsutil collector + rollup
proxy/ Caddy embedding, route builder, custom modules
reconciler/ fsnotify watcher + diff loop
store/ SQLite (WAL), embedded migrations
ui/ Svelte SPA, served from embedded fs at /
flowchart TB
subgraph Process[simpledeploy single process]
direction TB
cli[cobra CLI / main]
api[REST + WS API]
rec[Reconciler]
dep[Deployer]
cad[Caddy embedded]
met[Metrics collector]
mw[Metrics writer]
ag[Rollup manager]
al[Alert evaluator]
wb[Webhook dispatcher]
bk[Backup scheduler]
lb[logbuf ring]
db[(SQLite WAL)]
end
user([Operator]) --> cli
cli --> api
cli --> rec
api --> db
rec --> dep --> docker[(Docker daemon)]
rec --> cad
met -- chan MetricPoint --> mw --> db
ag --> db
db --> al --> wb --> internet([Webhook endpoint])
bk --> db
bk --> docker
cad --> upstreams[(Containers)]
dep -. os.Pipe .-> lb
lb --> api

Embedded Caddy. Running Caddy as a library means there is one process to supervise, one binary to ship, and route reloads happen with caddy.Load(JSON) instead of HUP signals or socket reloads. Caddy is configured purely via JSON; there is no Caddyfile anywhere. See /internal/proxy/proxy.go.

Custom Caddy modules. Three modules are registered in the proxy package’s init(): simpledeploy_metrics (records request stats into a channel), simpledeploy_ratelimit (per-domain token bucket), and simpledeploy_ipaccess (CIDR/IP allowlist). They sit in front of reverse_proxy in every route’s handler chain.

SQLite with WAL. Single-writer, many-reader is exactly the workload: reconciler + metrics writer write, the API reads constantly. WAL avoids reader-blocks-writer. SetMaxOpenConns(4) lets multiple read connections proceed in parallel. Migrations are embedded with go:embed and run on Open(). See /internal/store/store.go.

Channel-based metrics. The collector samples every interval and pushes MetricPoint values into a buffered channel. A separate writer goroutine batches them and calls InsertMetrics once per flush window. This decouples sampling cadence from DB write latency. The rollup manager runs on its own ticker and aggregates raw -> 1m -> 5m -> 1h -> 1d every 60 seconds.

Interfaces for testing. Every external dependency has an interface: docker.Client (with MockClient), deployer.CommandRunner (with MockRunner), backup.Strategy and backup.Target, store.* subset interfaces declared in the consumer package. Tests do not need Docker or a network.

  • Request path: TCP -> Caddy -> handler chain -> upstream container.
  • Deploy path: file write -> fsnotify -> reconciler -> deployer -> docker compose -> Docker.
  • Observability path: Docker stats -> collector -> channel -> writer -> SQLite -> rollup -> alert evaluator -> webhook.
  • Backup path: cron tick -> scheduler -> Strategy (docker exec) -> Target (local FS or S3).
  • Auth path: request -> middleware -> JWT cookie or Authorization: Bearer sd_... -> store lookup -> RBAC -> handler.