µPaaS - lightweight app for auto rebuilding/restarting docker containers on repo changes via webhook
- Replace UUID with ULID for app ID generation (lexicographically sortable) - Remove container_id column from apps table (migration 002) - Add upaas.id Docker label to identify containers by app ID - Implement FindContainerByAppID in Docker client to query by label - Update handlers and deploy service to use label-based container lookup - Show system-managed upaas.id label in UI with editing disabled Container association is now determined dynamically via Docker label rather than stored in the database, making the system more resilient to container recreation or external changes. |
||
|---|---|---|
| cmd/upaasd | ||
| internal | ||
| static | ||
| templates | ||
| .golangci.yml | ||
| CONVENTIONS.md | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| Makefile | ||
| README.md | ||
| TODO.md | ||
upaas
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:
globals- Build-time variableslogger- Structured loggingconfig- Configuration loadingdatabase- SQLite connection + migrationshealthcheck- Health statusauth- Authentication serviceapp- App managementdocker- Docker clientnotify- Notification servicedeploy- Deployment servicewebhook- Webhook processingmiddleware- HTTP middlewarehandlers- HTTP handlersserver- 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.HandlerFuncallowing 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.23+
- 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:
- Format: Run
make fmtto format all code - Lint: Run
make lintand fix all errors/warnings- Do not disable linters or add nolint comments without good reason
- Fix the code, don't hide the problem
- Test: Run
make testand ensure all tests pass- Fix failing tests by fixing the code, not by modifying tests to pass
- Add tests for new functionality
- Verify: Run
make checkto 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 |
|---|---|---|
UPAAS_PORT |
HTTP listen port | 8080 |
UPAAS_DATA_DIR |
Data directory for SQLite and keys | ./data |
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 upaas-data:/data \
upaas
License
MIT