clawbot 19619b1cd2
Check / check (pull_request) Successful in 3m24s
Wait for webhook deployments in handlers tests instead of sleeping (closes #211)
`TestHandleWebhookProcessesValidWebhook` waited for the background deployment with a 100 ms sleep, so its temp directory could be removed while the deployment was still writing its log there, and the test failed now and then. It now waits with the webhook service's `WaitForDeployments`, the fix already applied to the webhook service tests for #198. The test context keeps the webhook service it already built. No other handlers test can start a deployment. No production code change.

Model: opus-5-5
2026-09-23 03:04:31 +02: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 8080
UPAAS_DATA_DIR Data directory for SQLite and keys ./data (local dev only — use absolute path for Docker)
UPAAS_HOST_DATA_DIR Host path for DATA_DIR (when running in container) (none — must be set to an absolute path)
UPAAS_DOCKER_HOST Docker socket path 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
DEBUG Enable debug logging false
SENTRY_DSN Sentry error reporting DSN ""
METRICS_USERNAME Basic auth for /metrics ""
METRICS_PASSWORD Basic auth for /metrics ""

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.

Docker Compose

services:
    upaas:
        build: .
        restart: unless-stopped
        ports:
            - "8080:8080"
        volumes:
            - /var/run/docker.sock:/var/run/docker.sock
            - ${HOST_DATA_DIR}:/var/lib/upaas
        environment:
            - UPAAS_HOST_DATA_DIR=${HOST_DATA_DIR}
            # Set when serving plain HTTP (no TLS-terminating proxy); drop behind one
            - UPAAS_PLAINTEXT_HTTP=true
            # Optional: uncomment to enable debug logging
            # - DEBUG=true
            # Optional: Sentry error reporting
            # - SENTRY_DSN=https://...
            # Optional: Prometheus metrics auth
            # - METRICS_USERNAME=prometheus
            # - METRICS_PASSWORD=secret

Important: You must set HOST_DATA_DIR to an absolute path on the host before running docker compose up. This value is bind-mounted into the container and passed as UPAAS_HOST_DATA_DIR so that Docker bind mounts during builds resolve correctly. Relative paths (e.g. ./data) will break container builds because the Docker daemon resolves paths relative to the host, not the container.

Example: HOST_DATA_DIR=/srv/upaas/data docker compose up -d

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.2 MiB
MVP
Latest
2026-02-26 14:02:56 +01:00
Languages
Go 80.7%
HTML 11.2%
JavaScript 4.9%
Shell 1.4%
CSS 1.3%
Other 0.5%