µPaaS - lightweight app for auto rebuilding/restarting docker containers on repo changes via webhook
Go to file
user 478746c356 rebase: apply audit bug fixes on latest main
Rebased fix/1.0-audit-bugs onto current main (post-merge of PRs #119, #127).

Changes:
- Custom domain types: docker.ImageID, docker.ContainerID, webhook.UnparsedURL
- Type-safe function signatures throughout docker/deploy/webhook packages
- Remove docker-compose.yml, test helper files
- README and Dockerfile updates
- Prettier-formatted JS files
2026-02-23 11:56:09 -08:00
.gitea/workflows ci: add Gitea Actions workflow for make check (closes #96) 2026-02-20 02:51:10 -08:00
cmd/upaasd Initial commit with server startup infrastructure 2025-12-29 15:46:03 +07:00
internal rebase: apply audit bug fixes on latest main 2026-02-23 11:56:09 -08:00
static rebase: apply audit bug fixes on latest main 2026-02-23 11:56:09 -08:00
templates fix: sanitize container log output and fix lint issues 2026-02-20 02:54:07 -08:00
.golangci.yml Revert "Merge pull request 'feat: add Gitea Actions CI for make check (closes #96)' (#98) from feat/ci-make-check into main" 2026-02-19 20:36:22 -08:00
CONVENTIONS.md Initial commit with server startup infrastructure 2025-12-29 15:46:03 +07:00
Dockerfile rebase: apply audit bug fixes on latest main 2026-02-23 11:56:09 -08:00
go.mod Add CSRF protection to state-changing POST endpoints 2026-02-15 14:17:55 -08:00
go.sum Add CSRF protection to state-changing POST endpoints 2026-02-15 14:17:55 -08:00
LICENSE Add WTFPL license 2025-12-29 16:25:22 +07:00
Makefile Integrate Alpine.js for reactive UI 2026-01-01 05:37:46 +07:00
README.md rebase: apply audit bug fixes on latest main 2026-02-23 11:56:09 -08:00

µPaaS by @sneak

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

Features

  • Single admin user with argon2id password hashing
  • Per-app SSH keypairs for read-only deploy keys
  • Per-app UUID-based webhook URLs for Gitea integration
  • Branch filtering - only deploy on configured branch changes
  • Environment variables, labels, and volume mounts 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-Gitea webhooks

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/     # Gitea webhook processing
│   └── 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

Development

Prerequisites

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

Commands

make fmt      # Format code
make lint     # Run comprehensive linting
make test     # Run tests with race detection
make check    # Verify everything passes (lint, test, build, format)
make build    # Build binary

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
UPAAS_HOST_DATA_DIR Host path for DATA_DIR (when running in container) same as DATA_DIR
UPAAS_DOCKER_HOST Docker socket path unix:///var/run/docker.sock
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 \
  upaas

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}
      # 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: Set HOST_DATA_DIR to an absolute path on the Docker host before running docker compose up. Relative paths will not work because docker-compose may not run on the same machine as µPaaS. This value is used both for the bind mount and passed to µPaaS as UPAAS_HOST_DATA_DIR so it can create correct bind mounts during builds.

Example:

export 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