Skip to content

Architecture

┌──────────────────────────────────┐
│ Pier (single binary) │
│ │
Browser ───────▶ │ Axum ──▶ API routes (100+) │
│ │ │
│ ├──▶ MiniJinja ──▶ HTML (HTMX) │
│ ├──▶ Bollard ──▶ Docker Engine │
│ ├──▶ rusqlite ──▶ SQLite │
│ └──▶ reqwest ──▶ Remote Agents │
└──────────────────────────────────┘
┌───────────────┴────────────────┐
│ Traefik (reverse proxy) │
│ Let's Encrypt · Auto-routing │
└────────────────────────────────┘
LayerCrateRole
HTTP + WebSocketaxumAPI endpoints and SSE/WebSocket streams for logs & metrics
DockerbollardAll container, volume, and network operations
DatabaserusqliteEmbedded SQLite, WAL mode for concurrent reads
TemplatesminijinjaServer-rendered HTML for the dashboard
Authaxum-login + totp-rsSession-based login; TOTP on the roadmap
SSHrusshRemote server provisioning
GitgixClone and pull for Git-to-deploy
MetricssysinfoSystem and container metrics
CachemokaIn-memory cache for Docker stats
RuntimetokioAsync I/O

The UI is server-rendered with MiniJinja. HTMX handles partial updates; Alpine.js covers client-side state for small interactions (modals, toggles). Total client-side JS is around 30 KB. Everything ships embedded in the binary — no CDN, no external fonts, no bundler at runtime.

/opt/pier/
├── bin/pier # the binary
├── bin/pier.old # previous version (for rollback)
├── .env # PIER_SECRET — AES-256 key for env vars
└── data/
├── pier.db # SQLite main database
├── pier.db-wal # WAL file
├── backups/ # automated DB + .env backups
└── traefik/ # dynamic config files per domain

Every environment variable stored in the database is encrypted with AES-256-GCM. The key lives in /opt/pier/.env as PIER_SECRET (32 random bytes, base64-encoded). The database alone is useless without the key — the split is intentional so that a leaked SQL dump does not leak secrets.

Encryption is backward-compatible: rows written before the feature shipped are readable as plain text; new writes carry an ENC: prefix and are always encrypted.