When developers look for a self-hosted alternative to Vercel, Netlify, or Heroku, they almost always stumble into a familiar trap: the sprawling microservice distributed system disguised as a single app.
To run a βsimpleβ self-hosted PaaS on a $10 Hetzner or DigitalOcean droplet, you are suddenly asked to spin up:
- An external PostgreSQL or MySQL instance just to store route metadata.
- A Redis or RabbitMQ queue just to pass build status messages.
- Background workers, Celery daemons, and node processes consuming 1.5GB of idle RAM.
- Brittle shell wrapper scripts (
exec.Command("sh", "-c", "docker run ...")) that leak zombie processes and break when image tags contain special characters. - Ingress daemons that require rewriting static
Caddyfileornginx.conffiles to disk and triggering disruptive process reloads (kill -HUP).
At 2 AM, after watching a simple route update drop active WebSocket connections during a configuration reload, we asked a fundamental question:
What if an entire PaaS control plane compiled into a single static ~14MB Go binary, required zero external daemons, and executed sub-15ms ingress route mutations with zero dropped TCP connections?
That became pikpik.
The 4 Non-Negotiable Invariants
To keep the architecture ruthless, deterministic, and resilient, we established four non-negotiable engineering invariants at day one:
[ Invariant 1: Zero Shelling ] --> Direct Docker Engine API via /var/run/docker.sock
[ Invariant 2: Unified Runtime ] --> Single Go binary + embedded SQLite WAL + in-memory rings
[ Invariant 3: Dynamic Ingress ] --> Caddy dynamic Admin REST API (sub-15ms route mutations)
[ Invariant 4: Pure Streaming ] --> io.Pipe -> gzip -> S3 multipart (peak memory <32MB)
Letβs dive into what each of these invariants means in practice.
1. Invariant 1: Zero Shelling (API-First Engine)
In standard amateur DevOps tooling, managing containers is done by string-interpolating bash commands:
// β THE FRAGILE WAY: Vulnerable to shell injection, zombie pipes, and escaping bugs
cmd := exec.Command("sh", "-c", fmt.Sprintf("docker run -d -p %s:%s %s", hostPort, port, image))
In pikpik, shelling out to bash or sh is strictly forbidden across the entire codebase.
Every container lifecycle event, overlay network attachment, volume bind, exec PTY terminal session, and metric collection communicates directly through the typed Docker Engine SDK over /var/run/docker.sock.
- Container execution states are atomic structs.
- Streaming logs use
stdcopy.StdCopydirectly multiplexed into WebSocket frames. - Interactive terminal sessions bind directly to Docker raw TTY streams via bidirectional
io.ReadWriterproxies.
Zero shell interpolation. Zero process forks. Zero escaping bugs.
2. Invariant 2: Single Unified Runtime (SQLite WAL)
Why require an operator to manage Postgres, Redis, and three worker daemons just to deploy a Next.js app or a Postgres container?
pikpik compiles the entire control plane into a single standalone binary (cmd/pikpik, ~14MB).
- SQLite in Write-Ahead Logging (
WAL) mode: Using per-connection pragmas (PRAGMA busy_timeout=5000; PRAGMA foreign_keys=ON; PRAGMA journal_mode=WAL;), SQLite handles all project namespaces, service configs, API tokens, and backup schedules with sub-millisecond local latency. - In-Memory Ring Buffers: High-frequency container metrics (CPU %, resident RAM, network I/O) are buffered in circular in-memory structures (
RingBuffer) holding 8,640 data points per container. - Inverted Agent Tunnels: Remote worker nodes run
pikpik-agent(~6.2MB), which dials back to the primary control plane over an outbound mTLS/WSS tunnel (/agent/connect), allowing full Swarm fleet orchestration behind NATs without inbound firewall openings.
3. Invariant 3: Dynamic In-Memory API Ingress Reconciler
The Achillesβ heel of most self-hosted PaaS solutions is route mutation. When a user adds a custom domain or deploys a new service, traditional tools write a file to /etc/caddy/Caddyfile and run caddy reload. This causes file I/O latency, potential syntax parse errors, and transient TCP drop spikes.
pikpik communicates directly with Caddyβs dynamic Admin REST API (http://127.0.0.1:2019/load):
// In-memory route reconciliation in <15ms
func (m *IngressManager) Reconcile(ctx context.Context, routes []Route) error {
caddyConfig := m.buildJSONConfig(routes)
return m.caddyClient.LoadJSON(ctx, caddyConfig)
}
- Routes, SNI matchers, and reverse-proxy upstreams are converted directly into Caddyβs native in-memory JSON hierarchy.
- Mutations apply atomically in <15 milliseconds.
- ACME TLS certificate automation (Letβs Encrypt / ZeroSSL) is fully managed in-memory without touching disk configuration files.
4. Invariant 4: Pure Streaming Dataflows
When backing up a 20GB production database or streaming a massive Docker build log, naΓ―ve architectures dump the entire dump into /tmp/backup.sql.gz, exhaust server storage, and crash other running containers.
In pikpik, all heavy I/O operations are bounded streaming dataflows:
- Database backup dumps stream directly:
Docker Exec stdout$\rightarrow$io.Pipe$\rightarrow$gzip.Writer$\rightarrow$S3 Multipart Upload - Peak memory usage is strictly bounded to <32MB regardless of database size.
- Zero temporary staging files are ever written to
/tmp.
Overhauling the Control Plane UI: 5-Pillar Information Architecture
An enterprise infrastructure platform is only as good as the operatorβs cognitive bandwidth. During our latest midnight sprint, we overhauled the frontend into a structured, high-density 5-pillar domain model:
| Operational Domain | Cognitive Role | Key Capabilities |
|---|---|---|
| 1. Overview & Catalog | High-level situational awareness | Real-time cluster health, sparkline telemetry, 1-Click marketplace recipes |
| 2. Workloads & Compute | Core execution layer | Standalone applications, multi-service Compose stacks, managed databases |
| 3. Traffic & Ingress | Edge & routing layer | Caddy reverse proxy, Auto-TLS, virtual overlay mesh networks, route diagnostics |
| 4. Fleet & Storage | Physical & persistent resources | Swarm worker nodes, agent WSS tunnels, internal OCI registry, S3 snapshots |
| 5. Platform & Governance | System health & security | Host metrics, Docker disk cache pruning, RBAC permissions, AES secret vault |
To complement this hierarchy, we introduced a global Command Palette (Cmd+K) that allows platform engineers to jump directly to any container, database, or swarm node with instant fuzzy filtering and keyboard navigation.
The Developer Velocity Secret: Instant HMR Without Go Rebuilds
One of the biggest pain points in building single-binary Go applications with embedded web SPAs (//go:embed all:dist) is the feedback loop. Rebuilding Go binaries on every CSS or React tweak kills creative flow.
We solved this with a dual-mode development protocol:
- Production: Go binary embeds
web/distviaembed.FSand serves assets directly with immutable cache headers. - Development: Vite dev server runs on
:3000with React Fast Refresh / HMR and transparently reverse-proxies/api,/ws, and/healthzto Go on:8080. - Public Testing: Exposed via Cloudflare Quick Tunnels (
trycloudflare.com) withserver.allowedHosts: true, enabling live testing across remote devices in real-time.
React 19 / Tailwind (HMR :3000) <--[Reverse Proxy]--> pikpik Go Control Plane (:8080)
β β
βΌ βΌ
Cloudflare Quick Tunnel SQLite WAL + Docker Socket
Takeaways for Modern Systems Engineering
Building pikpik reinforced a timeless truth of systems design: Complexity is usually the result of unexamined defaults.
You do not need Kubernetes for a 50-node fleet. You do not need Postgres and Redis to manage container metadata. You do not need shell scripts to speak to Docker.
When you enforce strict boundary invariants, trust the standard library, and embrace single-binary architecture, you get software that boots in 12 milliseconds, runs in 18MB of RAM, and never wakes you up at 3 AM.