Apps dock here.
Pier is a lightweight control plane for k3s: a self-hosted dashboard for deploying, monitoring, and managing containerised apps on your own Kubernetes cluster.
I wanted to actually learn Kubernetes, clusters, and how cloud platforms work under the hood, not just read about them. My home lab runs on Proxmox, and every time I wanted to test or deploy something I'd spin up yet another LXC container by hand. A cluster gives you all of that in one place (scheduling, restarts, networking, routing, scaling), so Pier is my way of building the "push an app, get a URL" experience on hardware I own.
- Deploy apps: Push a Docker image, get a URL instantly via ingress (optional HTTPS through cert-manager)
- Private registries: Save Docker Hub, GHCR, or another registry credential and attach it to private image pulls without exposing the password
- Image inspection: Check an image before deployment to prefill its exposed port and warn when it declares persistent storage
- Persistent volumes: Attach retained local-path storage to apps; volume apps use one replica and Recreate updates.
- Logs & monitoring: Stream live logs, check pod status, see resource usage
- Scaling: Scale replicas up/down, pause with zero replicas
- App settings: Change image, port, env and CPU/RAM on a running app (with a quota pre-check)
- Restart: Trigger pod rollouts for live reload or recovery
- Pod actions: Restart one pod, inspect previous container logs, and read recent app events
- Networking: Apps reach public internet; isolated from each other and from the host LAN by default (configurable)
- Cluster health: Check API server status and node readiness
- Deploy steps and history: Every deploy, settings change and restart shows live, Vercel-style steps (validate, apply, schedule, pull, start, health, ready) with a plain-language reason when one fails, plus a per-app Deployments history (
GET /api/apps/:name/deployments,/:idand an SSE/:id/stream) - Activity log: Every deploy, change, scale, restart, delete and sign-in is recorded (without secrets) in a small SQLite database and shown on the Activity page
graph TB
Browser["🌐 Browser"]
Traefik["Traefik Ingress<br/>pier.example.internal"]
PierBackend["pier-backend pod<br/>default namespace"]
K8sAPI["Kubernetes API"]
App["App deployment<br/>pier-apps namespace"]
Browser -->|HTTP| Traefik
Traefik -->|Route to pier.* | PierBackend
Traefik -->|Route to app.*| App
PierBackend -->|kubectl API| K8sAPI
K8sAPI -->|Create/Delete/Scale| App
PierBackend -->|"Dashboard SPA"| Browser
App -->|"HTTP port"| Traefik
Backend: Single Fastify pod in default namespace with ServiceAccount granting cluster-wide health read and namespace-scoped app management in pier-apps.
Apps: Run in isolated pier-apps namespace with:
- Pod Security: enforced
baseline, warnedrestricted - NetworkPolicies: default-deny ingress/egress, allows Traefik ingress and DNS; optional rules for private networks
- ResourceQuota: 2 CPU / 4Gi memory request/limit per namespace
- Service type policy: ClusterIP only (no NodePort/LoadBalancer)
Before you start, ensure your k3s node has:
- k3s ≥ v1.36 with Traefik ingress controller (default in k3s)
- kubectl pointed at the cluster (via
KUBECONFIG) - Docker on your local machine (to build and push images to the cluster)
- SSH key access to the k3s node with passwordless
sudo k3s ctr(for image import) - Node.js ≥ 22.13 (the backend uses the built-in
node:sqlite), pnpm ≥ 9 (for local development) - Wildcard DNS (e.g.
*.192.0.2.10.nip.ioor your own resolver; apps use<name>.<domain>)
-
Copy the kubeconfig from the node to your local machine:
scp your-ssh-user@192.0.2.10:/etc/rancher/k3s/k3s.yaml ~/.kube/config kubectl config set clusters.k3s.server https://192.0.2.10:6443 # Edit ~/.kube/config and replace 127.0.0.1 with the node IP kubectl get nodes # verify connection
-
Clone pier and set env vars:
git clone <pier-repo> cd pier/backend cp .env.example .env
Edit
backend/.env:PIER_API_TOKEN=$(openssl rand -hex 32) K3S_HOST=192.0.2.10 K3S_SSH_USER=your-ssh-user PIER_INGRESS_DOMAIN=192.0.2.10.nip.io PIER_DASHBOARD_HOST=pier.192.0.2.10.nip.io # optional; derived if omitted
-
Add SSH key to agent and deploy:
ssh-add ~/.ssh/id_ed25519 # or your key ./scripts/deploy-local.sh
Wait for the rollout (up to 2 min).
-
Open the dashboard:
http://pier.192.0.2.10.nip.ioLog in with the token from
backend/.env. -
Deploy your first app:
- Create a new app: name=
myapp, image=nginx:alpine, port=80 - Set host:
myapp.192.0.2.10.nip.io - Open
http://myapp.192.0.2.10.nip.io— Nginx is running
For private images, open Registries, add the registry host and credentials, then select it in the deploy form. Use Check image to inspect a public image before deploying; declared volume paths link to the existing volume section.
- Create a new app: name=
-
Optional: turn on HTTPS. Run
./scripts/install-cert-manager.sh, addPIER_TLS_ISSUER=pier-catobackend/.env, redeploy, and trust Pier's root CA on your machines. New apps then gethttps://URLs and HTTP redirects to HTTPS. Full steps are in backend/k8s/README.md#tls.
git pull origin main
./backend/scripts/deploy-local.shThe script rebuilds the image, imports it, and rolls out the updated pod.
- Dashboard shows login screen (401): bearer token is missing or wrong, or session expired. Log in again with the token from
PIER_API_TOKEN. - Dashboard returns 404 via Traefik: check that
PIER_INGRESS_DOMAINis set and the Ingress was created:kubectl get ingress -n default kubectl describe ingress pier-backend -n default
- App stuck in "building" / ImagePullBackOff: Pod wasn't created due to Pod Security or quota rejection. Debug with:
kubectl get events -n pier-apps --sort-by=.lastTimestamp kubectl describe pod -n pier-apps
- App can't reach your LAN (times out): NetworkPolicy blocks private ranges. See backend/k8s/README.md for custom rules.
For more details, see:
- backend/README.md — Deploy script, token rotation, debugging
- backend/k8s/README.md — Manifests, RBAC, Pod Security, quotas, network policies
This repo uses pnpm (not npm). Backend and frontend are separate pnpm packages:
pnpm --dir backend install # installs backend dependencies
pnpm --dir frontend install # installs frontend dependencies
pnpm --dir backend dev # http://127.0.0.1:3000
pnpm --dir frontend dev # http://127.0.0.1:5173Before pushing, run the linter, type-checker, and tests:
pnpm --dir backend lint # ESLint
pnpm --dir backend typecheck # tsc --noEmit
pnpm --dir backend test # node:test
pnpm --dir backend build # tsup
pnpm --dir frontend lint # ESLint
pnpm --dir frontend typecheck # tsc -b
pnpm --dir frontend test # Vitest
pnpm --dir frontend build # Vite
docker build -f backend/Dockerfile -t pier-backend:ci . # the image deploy-local.sh shipsSee backend/README.md for API/kubeconfig setup and frontend/README.md for the dashboard stack, tokens, and assets.
Design context: PRODUCT.md and DESIGN.md at the repo root are the durable product and design references for frontend work (including impeccable and issues #43–#47). Read them before changing UI.
See CONTRIBUTING.md for:
- Commit conventions (conventional commits)
- Feature branch naming and workflow
- Code style and local checks
- Branch protection rules
