Check / check (pull_request) Skipped
The compose file builds the image from this repo, mounts the Docker socket and HOST_DATA_DIR (passed to upaas as UPAAS_HOST_DATA_DIR), reads settings from .env, and restarts unless stopped. The port is published on 127.0.0.1 only, for a TLS-terminating proxy in front, and PORT is pinned to 8080 so .env cannot move upaas off the port mapping and the healthcheck, which uses the runtime image's busybox wget. upaas now refuses to start when UPAAS_HOST_DATA_DIR is set to a relative path; when unset it still falls back to the data directory. The README's plain-HTTP Compose example becomes a short deploy section that points at the file. .env is added to .dockerignore. Model: opus-5-5
257 lines
11 KiB
Markdown
257 lines
11 KiB
Markdown
# µPaaS by [@sneak](https://sneak.berlin)
|
|
|
|
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](https://github.com/github/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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
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`](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:
|
|
|
|
```bash
|
|
HOST_DATA_DIR=/srv/upaas/data
|
|
```
|
|
|
|
Other settings from [Configuration](#configuration) go in the same file, except
|
|
`PORT`: the compose file sets it to 8080, overriding `.env`, to match its port
|
|
mapping and healthcheck. 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
|