clawbot 8e846d07cf
Check / check (pull_request) Skipped
Show the deploy branch in the app page title (closes #240)
The app page shows the app's configured branch as a neutral label next
to the status badge, so it can be read without opening the edit page.
The line under the title now shows only the repository. A new test
renders the app page for an app on a non-main branch and checks the
branch is in the title row.

Model: opus-5-5
2026-09-29 10:02:44 +00:00
2025-12-29 16:25:22 +07:00
2026-07-07 02:15:20 +02:00

µPaaS by @sneak

A simple self-hosted PaaS that auto-deploys Docker containers from Git repositories via webhooks from Gitea, GitHub, or GitLab.

Features

  • Single admin user with argon2id password hashing
  • Per-app SSH keypairs for read-only deploy keys
  • Per-app UUID-based webhook URLs with auto-detection of Gitea, GitHub, and GitLab
  • Branch filtering - only deploy on configured branch changes
  • Environment variables, labels, and volume mounts per app
  • CPU and memory resource limits per app
  • Docker builds via socket access
  • Notifications via ntfy and Slack-compatible webhooks
  • Simple server-rendered UI with Tailwind CSS

Non-Goals

  • Multi-user support
  • Complex CI pipelines
  • Multiple container orchestration
  • SPA/API-first design
  • Support for non-push webhook events (e.g. issues, merge requests)

Architecture

Project Structure

upaas/
├── cmd/upaasd/          # Application entry point
├── internal/
│   ├── config/          # Configuration via Viper
│   ├── database/        # SQLite database with migrations
│   ├── docker/          # Docker client for builds/deploys
│   ├── globals/         # Build-time variables (version, etc.)
│   ├── handlers/        # HTTP request handlers
│   ├── healthcheck/     # Health status service
│   ├── logger/          # Structured logging (slog)
│   ├── middleware/      # HTTP middleware (auth, logging, CORS)
│   ├── models/          # Active Record style database models
│   ├── server/          # HTTP server and routes
│   ├── service/
│   │   ├── app/         # App management service
│   │   ├── auth/        # Authentication service
│   │   ├── deploy/      # Deployment orchestration
│   │   ├── notify/      # Notifications (ntfy, Slack)
│   │   └── webhook/     # Webhook processing (Gitea, GitHub, GitLab)
│   └── ssh/             # SSH key generation
├── static/              # Embedded CSS/JS assets
└── templates/           # Embedded HTML templates

Dependency Injection

Uses Uber fx for dependency injection. Components are wired in this order:

  1. globals - Build-time variables
  2. logger - Structured logging
  3. config - Configuration loading
  4. database - SQLite connection + migrations
  5. healthcheck - Health status
  6. auth - Authentication service
  7. app - App management
  8. docker - Docker client
  9. notify - Notification service
  10. deploy - Deployment service
  11. webhook - Webhook processing
  12. middleware - HTTP middleware
  13. handlers - HTTP handlers
  14. server - HTTP server

Request Flow

HTTP Request
    │
    ▼
chi Router ──► Middleware Stack ──► Handler
                    │
        (Logging, Auth, CORS, etc.)
                    │
                    ▼
             Handler Function
                    │
                    ▼
    Service Layer (app, auth, deploy, etc.)
                    │
                    ▼
         Models (Active Record)
                    │
                    ▼
               Database

Key Patterns

  • Closure-based handlers: Handlers return http.HandlerFunc allowing one-time initialization
  • Active Record models: Models encapsulate database operations (Save(), Delete(), Reload())
  • Async deployments: Webhook triggers deploy via goroutine with context.WithoutCancel()
  • Embedded assets: Templates and static files embedded via //go:embed

Entrypoints

This repository adheres to the Scripts to Rule Them All standard: normalized scripts in script/ are the entrypoints for the development workflow, and the Makefile targets are thin shims that call them. We provide:

  • script/bootstrap — install all dependencies (idempotent)
  • script/setup — make a fresh clone ready for development (bootstrap, then install-precommit)
  • script/projectname — output the project name ("upaas")
  • script/test — run the test suite
  • script/lint — run golangci-lint
  • script/fmt — format all code (writes)
  • script/fmt-check — check formatting (read-only)
  • script/check — run test, lint, and fmt-check
  • script/docker — build the Docker image tagged via script/projectname
  • script/cibuild — CI entrypoint: docker build . (the Dockerfile runs the checks, so a green build implies a green repo)
  • script/precommit — pre-commit checks (go mod tidy guard, then script/check)
  • script/install-precommit — install the git pre-commit hook that runs script/precommit

Development

Prerequisites

  • Go 1.25+
  • golangci-lint
  • Docker (for running)

Commands

make bootstrap # Install all dependencies (idempotent)
make setup     # Bootstrap + install git pre-commit hook
make fmt       # Format code
make fmt-check # Check formatting (read-only, fails if unformatted)
make lint      # Run comprehensive linting
make test      # Run tests with race detection (30s timeout)
make check     # Verify everything passes (test, lint, fmt-check)
make build     # Build binary
make docker    # Build Docker image
make hooks     # Install pre-commit hook (runs script/precommit)

Commit Requirements

All commits must pass make check before being committed.

Before every commit:

  1. Format: Run make fmt to format all code
  2. Lint: Run make lint and fix all errors/warnings
    • Do not disable linters or add nolint comments without good reason
    • Fix the code, don't hide the problem
  3. Test: Run make test and ensure all tests pass
    • Fix failing tests by fixing the code, not by modifying tests to pass
    • Add tests for new functionality
  4. Verify: Run make check to confirm everything passes
# Standard workflow before commit:
make fmt
make lint    # Fix any issues
make test    # Fix any failures
make check   # Final verification
git add .
git commit -m "Your message"

The Docker build runs make check and will fail if:

  • Code is not formatted
  • Linting errors exist
  • Tests fail
  • Code doesn't compile

This ensures the main branch always contains clean, tested, working code.

Configuration

Environment variables:

Variable Description Default
PORT HTTP listen port. UPAAS_PORT is also read and wins when both are set. 8080
UPAAS_DATA_DIR Directory for the SQLite database, session key, builds and deployment logs. Deploys need it to be an absolute path unless UPAAS_HOST_DATA_DIR is set. ./data (the Docker image sets /var/lib/upaas)
UPAAS_HOST_DATA_DIR Host path of UPAAS_DATA_DIR, needed when upaas runs in a container so the bind mounts it passes to Docker point at the right host directory. When set, it must be absolute, or upaas refuses to start. the value of UPAAS_DATA_DIR
UPAAS_DOCKER_HOST Docker daemon address unix:///var/run/docker.sock
UPAAS_PLAINTEXT_HTTP Set when µPaaS is reached over plain HTTP (no TLS-terminating proxy in front) so CSRF origin checks use http://. Leave unset behind a TLS-terminating reverse proxy. false
UPAAS_DEBUG Enable debug logging. Also sends the session cookie without the Secure flag. false
UPAAS_SENTRY_DSN Read but not used: upaas sends nothing to Sentry ""
UPAAS_METRICS_USERNAME When set, /metrics is served behind basic auth with this username. When unset, there is no /metrics. ""
UPAAS_METRICS_PASSWORD Basic auth password for /metrics ""
UPAAS_MAINTENANCE_MODE Only shown as maintenanceMode in the /health response; it blocks nothing false
UPAAS_SESSION_SECRET Key that signs the session and CSRF cookies. When unset, a random key is generated once and kept in $UPAAS_DATA_DIR/session.key. ""
UPAAS_CORS_ORIGINS Comma-separated origins allowed to make cross-origin requests with cookies. When unset, no CORS headers are sent. ""

The Docker client also reads the standard DOCKER_API_VERSION, DOCKER_CERT_PATH and DOCKER_TLS_VERIFY variables; UPAAS_DOCKER_HOST, which has a default, always overrides DOCKER_HOST.

Running with Docker

docker run -d \
  -p 8080:8080 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /path/on/host/upaas-data:/var/lib/upaas \
  -e UPAAS_HOST_DATA_DIR=/path/on/host/upaas-data \
  -e UPAAS_PLAINTEXT_HTTP=true \
  upaas

This recipe serves plain HTTP, so UPAAS_PLAINTEXT_HTTP=true is required for setup and every other form to pass the CSRF origin check. Behind a TLS-terminating reverse proxy, drop that line.

Deploying with Docker Compose

docker-compose.yml builds the image from this repo and runs it with the Docker socket and the data directory mounted. It reads its settings from a .env file next to it, which needs at least:

HOST_DATA_DIR=/srv/upaas/data

Other settings from Configuration go in the same file, except PORT, UPAAS_PORT and UPAAS_DATA_DIR: the compose file sets both port settings to 8080 and UPAAS_DATA_DIR to /var/lib/upaas, overriding .env, to match its port mapping, healthcheck and data directory mount. Then run docker compose up -d from the repo root; docker compose ps shows the container as healthy once /health answers.

Important: HOST_DATA_DIR must be an absolute path on the host. It is bind-mounted into the container and passed as UPAAS_HOST_DATA_DIR so that Docker bind mounts during builds resolve correctly, because the Docker daemon resolves paths on the host, not in the container. upaas refuses to start when UPAAS_HOST_DATA_DIR is a relative path such as ./data.

The port is published on 127.0.0.1:8080 only, for a TLS-terminating reverse proxy in front of it. Leave UPAAS_PLAINTEXT_HTTP unset behind that proxy.

Apps are built with BuildKit, so the stages of a multi-stage build are kept in Docker's build cache rather than as untagged images. Docker Engine 28.2 and later keeps that cache under a size limit by default; on older engines, set "builder": {"gc": {"enabled": true}} in the host's daemon.json.

Session secrets are automatically generated on first startup and persisted to $UPAAS_DATA_DIR/session.key.

License

WTFPL

S
Description
µPaaS - lightweight app for auto rebuilding/restarting docker containers on repo changes via webhook
Readme WTFPL
1.3 MiB
MVP
Latest
2026-02-26 14:02:56 +01:00
Languages
Go 80.8%
HTML 10.8%
JavaScript 4.7%
Shell 2%
CSS 1.3%
Other 0.4%