## Summary
Adds configurable Docker CPU and memory resource constraints per app, closes https://git.eeqj.de/sneak/upaas/issues/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 https://git.eeqj.de/sneak/upaas/issues/72
- Add cpu_limit (REAL) and memory_limit (INTEGER) columns to apps table
via migration 007
- Add CPULimit and MemoryLimit fields to App model with full CRUD support
- Add resource limits fields to app edit form with human-friendly
memory input (e.g. 256m, 1g, 512k)
- Pass CPU and memory limits to Docker container creation via
NanoCPUs and Memory host config fields
- Extract Docker container creation helpers (buildEnvSlice, buildMounts,
buildResources) for cleaner code
- Add formatMemoryBytes template function for display
- Add comprehensive tests for parsing, formatting, model persistence,
and container options
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Adds configurable Docker CPU and memory resource constraints per app, closes #72.
Changes
Database
007_add_resource_limits.sql: addscpu_limit(REAL, nullable) andmemory_limit(INTEGER in bytes, nullable) columns to theappstableModel (
internal/models/app.go)CPULimit(sql.NullFloat64) andMemoryLimit(sql.NullInt64) fields toAppstructDocker Client (
internal/docker/client.go)CPULimit(float64, CPU cores) andMemoryLimit(int64, bytes) toCreateContainerOptionscpuLimitToNanoCPUs()conversion helper andbuildResources()to constructcontainer.ResourcesbuildEnvSlice()andbuildMounts()helpers fromCreateContainerfor cleaner codeHostConfig.Resources(NanoCPUs / Memory)Deploy Service (
internal/service/deploy/deploy.go)buildContainerOptionsreadsCPULimitandMemoryLimitfrom the app and passes them toCreateContainerOptionsHandlers (
internal/handlers/app.go)HandleAppUpdatereads and validatescpu_limitandmemory_limitform fieldsparseOptionalFloat64()for CPU limit parsing (positive float or empty)parseOptionalMemoryBytes()for memory parsing with unit suffixes (k/m/g) or plain bytesoptionalNullString()andapplyResourceLimits()helpers to keep cyclomatic complexity in checkTemplates
app_edit.html: Added "Resource Limits" section with CPU limit (cores) and memory limit (with unit suffix) fieldstemplates.go: AddedformatMemoryBytestemplate function for display (converts bytes → human-readable like256m,1g)Tests
internal/docker/resource_limits_test.go: Tests forcpuLimitToNanoCPUsconversioninternal/handlers/resource_limits_test.go: Tests forparseOptionalFloat64andparseOptionalMemoryBytes(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 limitstemplates/templates_test.go: Tests forformatMemoryBytesformattingREADME
Behavior
0.5= half a core,2= two cores). Converted to Docker NanoCPUs internally.256m,1g,512k). Stored as bytes in the database.closes #72
Review: PR #165 — feat: CPU/memory resource limits per app
Policy Compliance
No policy violations found.
@sha256:hash ✅.golangci.ymlnot modified ✅007_add_resource_limits.sql) follows the established numbered migration pattern already present onmain(002–006) ✅Requirements Checklist (issue #72)
cpu_limitandmemory_limitcolumns to apps tableapp_edit.htmlhas Resource Limits section with CPU and memory inputsCreateContainerOptionscarries limits →buildResources()→HostConfig.ResourcesTest Coverage
cpuLimitToNanoCPUs()internal/docker/resource_limits_test.go✅parseOptionalFloat64()internal/handlers/resource_limits_test.go✅parseOptionalMemoryBytes()internal/handlers/resource_limits_test.go✅formatMemoryBytes()templates/templates_test.go✅internal/models/models_test.go✅internal/service/deploy/deploy_container_test.go✅Build Result
docker build .— PASS (fmt-check, lint, test, compile all green)Rebase onto
main— clean, no conflicts. Build still passes after rebase.Code Quality Notes
buildEnvSlice(),buildMounts(),buildResources()helpers improves readability ofCreateContaineroptionalNullString()DRYs up repeated null-string handling inHandleAppUpdate1.5g) and case-insensitive suffixesErrInvalidMemoryFormatandErrNegativeValueare properly defined as package-level error variablesVerdict: PASS ✅
All issue requirements fully implemented. Tests cover every new code path. No policy violations. Build green.