Pier reference
Self-hosted PaaS for your server fleet.
Pier handles end-to-end app lifecycle on any Linux host: provisioning, builds, deploys, multi-host orchestration, managed databases, and incident response — all driven by a Go agent installed in one curl.
Hosts & the agent
Every Pier deploy lands on a host you registered. The agent is a single Go binary at /opt/deploywatch/agent running as a systemd service. It pulls job descriptions from the control plane and reports back status, logs, and host telemetry.
Provisioning a host
- Admin → Servers → Add server: pick a label and a region. Pier mints a one-time bootstrap token
dwb_*. - Run the rendered curl on the host. The install script hardens with UFW, fail2ban, and unattended-upgrades; downloads + verifies the agent; and exchanges the bootstrap token for a long-lived host token
dwh_*. - The host joins the Wireguard mesh on
10.83.0.0/16. Host-to-host communication happens over this private network — no public ports opened beyond 22/80/443. - Every 30 seconds the agent posts a heartbeat with disk, CPU, RAM, kernel version, Docker version, and the hardening profile version.
Hetzner Cloud integration (optional)
If your team adds a Hetzner API token under Admin → Team → Hetzner, Pier auto-discovers your servers, Volumes, Load Balancers, and Object Storage credentials — no copy-paste between dashboards.
Self-update
Agent binaries are versioned and SHA256-pinned on R2. The control plane stages updates by version; agents poll and replace themselves atomically via /opt/deploywatch/agent → systemd watchdog restarts on failure.
Projects & apps
Projects are namespaces. They group apps that share a slug prefix, a deploy environment, and an RBAC boundary. Each project has at most one Beacon counterpart (same slug) so logs route automatically.
Apps live inside projects. An app is the deployable unit — one Git repo, one build configuration, one set of processes. You can have multiple apps per project (e.g. acme-api, acme-web, acme-worker).
Build modes
- Compose: existing
docker-compose.ymlin the repo. Pier wraps it with health-gating, traffic-swap, and resource limits. - Dockerfile: BuildKit build with persistent cache. One image per app. Best for hand-tuned builds.
- Static: bring a public folder; Pier serves it behind Traefik + Let's Encrypt with cache headers.
- Nixpacks: language auto-detection (Node, Python, Ruby, Go, Rust, etc.) without a Dockerfile.
Processes & scheduled jobs
Each app can run multiple named processes (web, worker, scheduler) on the same image. Health probes are configured per process. Pre-deploy and post-deploy hooks let you run migrations or warmups before traffic shifts.
Crons are systemd timers managed by Pier — define them under App → Crons. Each cron run streams logs to Beacon tagged with the cron name.
Environment variables & secrets
Env vars are managed per app per environment under App → Envs. Variables are stored encrypted at rest (WebCrypto AES with ENV_KEY) and injected at deploy time. Multiple environments (production, staging, preview) inherit from a base set and override per env.
Domains & Cloudflare DNS
An app can bind one or more domains. The first is the primary — used for canonical URLs and OG cards. If your team connects a Cloudflare API token, Pier auto-writes the A record on bind and removes it on unbind.
TLS terminates at Traefik on each host with Let's Encrypt certificates. Certs are issued via HTTP-01 by default; for cf_proxied domains Pier falls back to HTTP-01 challenges that work through Cloudflare's edge.
App middleware
Traefik middleware chains let you toggle internal-only access, Cloudflare-proxy enforcement, basic auth guards, and rate limits per app. Configure under App → Middlewares.
Multi-host placement
- Pin: app runs on exactly one host. Default for apps with local volumes or single-writer state.
- Spread: N replicas across hosts, with placement gated by capacity. Use for stateless workloads.
Choose under App → Settings → Placement. Pier respects per-host capacity overrides so a small staging box doesn't get scheduled prod-sized workloads.
Volumes & storage
Per-app block volumes mount at paths you define. On Hetzner hosts with an attached Hetzner API token, Pier provisions and attaches the underlying Volume automatically. On other providers, the agent manages mount points on local disks.
For object storage, Pier abstracts the backend behind one interface — R2, S3, SFTP, and Hetzner Object Storage all work with the same binding. Credentials are injected into the app as env vars.
Managed databases
- Postgres: single-node and Patroni HA clusters. Extensions enabled per cluster (pgvector, postgis, timescaledb). PgBouncer connection pool managed inline.
- Redis: standalone and Sentinel HA topologies.
- External cluster adoption: bring an existing Postgres or Patroni cluster by URL. List, create, and drop logical DBs from the same UI without migrating data.
Backups & point-in-time recovery
Postgres backups run at 03:00 UTC daily — pg_dump | gzip uploaded to R2 with a 7-day retention default. Configure retention and trigger ad-hoc backups from Service → Backups.
For Patroni clusters and external Postgres, Pier configures pgBackRest repos with point-in-time recovery. Restore a cluster to any timestamp within retention from the same panel.
Volume snapshots
Block volumes get Restic-based snapshots on a configurable schedule. Snapshots are deduplicated and stored in your configured storage backend. Restore from Service → Snapshots.
Incident runbooks
Runbooks are policy-as-code that fan out on three trigger types:
- Error rate — comparing the current revision to a baseline window (e.g. >2% errors for 3 minutes after a deploy).
- Uptime — health probe failures across replicas.
- Resource — CPU, memory, or disk thresholds.
Each runbook triggers one or more actions:
- Slack message to a channel
- Discord webhook
- Email to a list
- Generic HTTP webhook (your own automations)
Runbooks are evaluated every 5 minutes with per-runbook cooldowns so a spike doesn't fire 12 alerts in an hour. Define under App → Runbooks.
# Example runbook
when: error_rate > 2%
for: 3m
and:
deploy.age < 15m
then:
- notify: "#oncall"
- rollback: prev
cooldown: 15mPR previews
Open a pull request and Pier spins up a preview environment at a derived hostname. Previews carry their own database snapshot (or share with main, configurable), expire after 7 days, and cap at 5 concurrent per project.
Cost tracking
Each host carries a monthly_cost_usd attribute. Pier allocates that cost to apps by CPU-share share over a 6-hour window, then sums to a dollar badge per app and per project. No additional billing infra required — just enter what the host costs.
Crash-aware revisions
Every deploy creates a revision (Git SHA + build metadata). Pier deep-links each revision card to Beacon search filtered to that revision, with a red-dot count of unique error groups and an error-rate delta vs the previous revision. Bad deploys identify themselves before users do.
Edge workers for zero-downtime swaps
For apps that absolutely cannot drop a request during deploy, Pier provisions a per-app Cloudflare Worker that holds in-flight traffic up to hold_ms while the blue-green swap completes. A shared cert store on R2 keeps Let's Encrypt consistent across hosts.
Coolify import
If you're migrating from Coolify, the importer is non-destructive — it pulls projects, apps, and managed databases over the Coolify HTTP API. Lenient field parsing, four validation checks before any write, optional DNS takeover at the end.
Audit log & notification channels
Every meaningful action gets an audit event — deploy, lifecycle change, domain bind, cron edit, runbook fire, env-var change. Audit events feed activity panels at the app, project, and account level.
Notification channels (Slack, Discord, email, HTTP webhook) are defined once at the team level and referenced from runbooks and Beacon alerts.
Auth & RBAC
- Sign-in: email OTP + optional TOTP 2FA. TOTP is a custom ~80-LOC WebCrypto implementation — no external auth dependency.
- API tokens:
dwa_*bearer tokens with per-token scopes. Mint under Account → API tokens. - Project access: deny-by-default. Members see only the projects they were invited to. Admins manage membership at the project or workspace level.
- Operate-as banner: super-admins can impersonate another tenant temporarily for support; every action is audited.