Upstream docs: none — Pagecase has no separate upstream project. The Go controller (
cmd/pagecase,internal/) and the StartOS package (startos/) are both maintained in this repository. Sections below describe the package as shipped.
Pagecase hosts static websites built from Git repositories. Push a commit to your repo and Pagecase clones it, runs the build inside a bubblewrap sandbox, and serves the output through a built-in HTTP router. One instance hosts many sites with independent custom domains.
- Image and Container Runtime
- Volume and Data Layout
- Installation and First-Run Flow
- Configuration Management
- Network Access and Interfaces
- Actions (StartOS UI)
- Backups and Restore
- Health Checks
- Limitations and Differences
- What Is Unchanged from Upstream
- Contributing
- Quick Reference for AI Consumers
| Field | Value |
|---|---|
| Image source | Custom Dockerfile (multi-stage: Go build + Alpine 3.22 runtime) |
| Base image | alpine:3.22 (Node.js 22.x, bubblewrap, git, git-lfs, tini) |
| Architectures | x86_64, aarch64 |
| Entrypoint | /sbin/tini -- /usr/local/bin/pagecase serve --data /data --addr :8080 --addr-webhook :8081 |
| Runtime user | root inside the StartOS-isolated subcontainer (per-build isolation is enforced by bubblewrap, not by the container's USER) |
| Field | Value |
|---|---|
| Volume name | main |
| Mount point | /data |
| StartOS-managed | /data/store.json — site list and global SSH keypair |
| Controller state | /data/state.db — SQLite (WAL) build history |
| Per-site layout | /data/sites/<site-id>/{repo,builds/<sha>,current,logs} |
current is an atomic symlink that points at builds/<sha>; deployments swap it via rename(2). Rollback re-aims the symlink at an earlier <sha> directory.
On install (kind == 'install'), init/seedStore.ts generates an Ed25519 SSH keypair and writes store.json with:
{
"globalSshPublicKey": "ssh-ed25519 AAAA... pagecase",
"globalSshPrivateKey": "<PEM>",
"sites": []
}No build wizard. No initial site. The operator's next step is to attach public domains to the StartOS interfaces and run Add Site for each project they want to host. The full step-by-step deploy guide lives in instructions.md (shown in the StartOS UI under the "Instructions" tab).
| StartOS-Managed (via Actions / env vars) | Operator-Managed (outside StartOS) |
|---|---|
| Site list, per-site repo URL, branch, build command, output directory, custom domains, timeout | Git repository contents and package.json / package-lock.json |
| Global SSH keypair (auto-generated on install) | Adding the public key as a deploy key on each private Git repo |
| Webhook secrets (auto-generated per site) | Pasting the secret + URL into each Git host's webhook configuration |
| StartOS-attached public domains (StartTunnel, Tor, custom domains via Gateways) | DNS records for any custom clearnet domain |
Environment variables consumed by the controller:
| Variable | Default | Purpose |
|---|---|---|
PAGECASE_LOG_LEVEL |
info |
Reserved for future use; currently unread by controller |
PAGECASE_DISABLE_SANDBOX |
unset | If 1, skips bubblewrap (development only) |
Two independent StartOS hosts so each can carry its own public domain.
| Interface | Host ID | Container port | Type | Path | Notes |
|---|---|---|---|---|---|
sites |
sites |
8080 | API | / |
Serves built sites; routes by Host header. Declared as api (not ui) so no "Open UI" button appears — there is no single launchable UI; each site is reached via its own custom domain. |
webhooks |
webhooks |
8081 | API | /_hooks/ |
Git provider webhook receiver; masked by default |
Host resolution checks X-Forwarded-Host, then X-Original-Host, then Host (port stripped, case-insensitive). A /_/<site-id>/... path-prefix fallback is available when no Host matches, but absolute asset URLs in typical Vite/React output won't resolve under that prefix — use a real custom domain for production traffic.
Access methods supported by StartOS (all work without further package config): LAN IP, mDNS .local, Tor .onion, StartTunnel, custom clearnet domains via Gateways. Operators typically attach a site's public hostname (e.g. www.example.com) to the sites interface and a separate hostname (e.g. pc-wh.example.com) to the webhooks interface, then list the site's hostname under the site's Custom Domains field.
| ID | Name | Visibility | Availability | Inputs | Outputs |
|---|---|---|---|---|---|
add-site |
Add Site | enabled | any | site ID, repo URL, branch, build command, output dir, domains, timeout | confirmation |
edit-site |
Edit Site | enabled | any | site select + all editable fields (pre-filled from store) | confirmation |
remove-site |
Remove Site | enabled | any | site select (warning shown) | confirmation |
trigger-build |
Trigger Build | enabled | only-running | site select, optional Git ref | build ID |
rollback |
Rollback | enabled | only-running | site select, commit select (deduped, newest first, with build timestamp) | commit SHA confirmation |
show-webhook-url |
Show Webhook URL | enabled | any | site select | primary URL, signing secret (masked), at most one alternate URL |
show-ssh-key |
Show SSH Public Key | enabled | any | none | the global Ed25519 public key |
list-builds |
List Builds | enabled | only-running | site select | last 20 builds with status, trigger, timestamps |
test-webhook |
Test Webhook Delivery | enabled | only-running | site select | controller HTTP status + response body (also queues a real build) |
| Field | Value |
|---|---|
| Backed up | The entire main volume (/data) via sdk.setupBackups(['main']) |
| Includes | store.json (site list + SSH keypair), state.db, all sites/ |
| Excluded | None |
| Restore behaviour | Volume contents restored byte-for-byte; controller boots, re-applies SQLite migrations idempotently, and resumes serving the previously deployed current symlinks |
| Field | Value |
|---|---|
| Daemon | controller |
| Check | sdk.healthCheck.checkPortListening(effects, 8080, …) |
| Success msg | "Controller is listening" |
| Failure msg | "Controller is not responding" |
The webhook port (8081) is not separately health-checked; the same process serves both, so the port 8080 listener implicitly covers webhook availability.
- Node-based renderers only. Hugo, Zola, Jekyll, mdBook, and other non-Node static-site generators are not supported in v1.
- Single build worker. The FIFO queue runs one build at a time. Concurrent pushes to multiple sites queue serially.
- One global SSH key. All sites share
globalSshPrivateKey. Per-site deploy keys are not yet supported. - No build cancellation. A queued or running build runs to completion or the per-site timeout (default 600 s).
- No live log streaming. Build logs are written to
/data/sites/<id>/logs/<build-id>.logand can be inspected viastart-cli package attach pagecase cat …. There is no HTTP endpoint for log tailing. - SSH host-key checking is disabled for Git cloning.
gitssh.NewPublicKeysusesInsecureIgnoreHostKey()because per-deploy-key authorization on the Git host is the trust boundary. - No automatic cleanup of old build artifacts.
/data/sites/<id>/builds/<sha>/directories accumulate indefinitely until the site is removed or the operator deletes them manually. - Webhook handler ignores non-matching branches with a 200. A push to a non-configured branch returns "ignored: wrong branch" — the Git host considers the delivery successful, but no build is queued.
Not applicable — there is no separate upstream project.
See CONTRIBUTING.md for build instructions, the project layout, the type-check command, and the commit-message convention.
package_id: pagecase
upstream_version: none
image: pagecase (built from ./Dockerfile, multi-stage Go + alpine:3.22)
architectures: [x86_64, aarch64]
volumes:
main: /data
ports:
sites: 8080
webhooks: 8081
dependencies: none
startos_managed_env_vars:
- PAGECASE_LOG_LEVEL
- PAGECASE_DISABLE_SANDBOX
actions:
- add-site
- edit-site
- remove-site
- trigger-build
- rollback
- show-webhook-url
- show-ssh-key
- list-builds
- test-webhook