forked from sneak/upaas
## Summary Adds configurable Docker CPU and memory resource constraints per app, closes sneak/upaas#72. ## Changes ### Database - Migration `007_add_resource_limits.sql`: adds `cpu_limit` (REAL, nullable) and `memory_limit` (INTEGER in bytes, nullable) columns to the `apps` table ### Model (`internal/models/app.go`) - Added `CPULimit` (`sql.NullFloat64`) and `MemoryLimit` (`sql.NullInt64`) fields to `App` struct - Updated insert, update, scan, and column list to include the new fields ### Docker Client (`internal/docker/client.go`) - Added `CPULimit` (float64, CPU cores) and `MemoryLimit` (int64, bytes) to `CreateContainerOptions` - Added `cpuLimitToNanoCPUs()` conversion helper and `buildResources()` to construct `container.Resources` - Extracted `buildEnvSlice()` and `buildMounts()` helpers from `CreateContainer` for cleaner code - Resource limits are passed to Docker's `HostConfig.Resources` (NanoCPUs / Memory) ### Deploy Service (`internal/service/deploy/deploy.go`) - `buildContainerOptions` reads `CPULimit` and `MemoryLimit` from the app and passes them to `CreateContainerOptions` ### Handlers (`internal/handlers/app.go`) - `HandleAppUpdate` reads and validates `cpu_limit` and `memory_limit` form fields - Added `parseOptionalFloat64()` for CPU limit parsing (positive float or empty) - Added `parseOptionalMemoryBytes()` for memory parsing with unit suffixes (k/m/g) or plain bytes - Added `optionalNullString()` and `applyResourceLimits()` helpers to keep cyclomatic complexity in check ### Templates - `app_edit.html`: Added "Resource Limits" section with CPU limit (cores) and memory limit (with unit suffix) fields - `templates.go`: Added `formatMemoryBytes` template function for display (converts bytes → human-readable like `256m`, `1g`) ### Tests - `internal/docker/resource_limits_test.go`: Tests for `cpuLimitToNanoCPUs` conversion - `internal/handlers/resource_limits_test.go`: Tests for `parseOptionalFloat64` and `parseOptionalMemoryBytes` (happy paths, edge cases, validation) - `internal/models/models_test.go`: Tests for App model resource limit persistence (save/load, null defaults, clearing) - `internal/service/deploy/deploy_container_test.go`: Tests for container options with/without resource limits - `templates/templates_test.go`: Tests for `formatMemoryBytes` formatting ### README - Added "CPU and memory resource limits per app" to Features list ## Behavior - **CPU limit**: Specified in cores (e.g. `0.5` = half a core, `2` = two cores). Converted to Docker NanoCPUs internally. - **Memory limit**: Accepts plain bytes or suffixed values (`256m`, `1g`, `512k`). Stored as bytes in the database. - Both fields are **optional** — empty/unset means unlimited (no Docker constraint applied). - Limits are applied on every container creation: new deploys, rollbacks, and restarts that recreate the container. closes sneak/upaas#72 Co-authored-by: user <user@Mac.lan guest wan> Reviewed-on: sneak/upaas#165 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org>
218 lines
6.8 KiB
Markdown
218 lines
6.8 KiB
Markdown
# µPaaS by [@sneak](https://sneak.berlin)
|
|
|
|
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
|
|
- 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-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
|
|
|
|
```bash
|
|
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 (fmt-check, lint, test)
|
|
make build # Build binary
|
|
make docker # Build Docker image
|
|
make hooks # Install pre-commit hook (runs make check)
|
|
```
|
|
|
|
### 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 |
|
|
| `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 \
|
|
upaas
|
|
```
|
|
|
|
### Docker Compose
|
|
|
|
```yaml
|
|
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**: 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
|