Simple homelab infrastructure management. ~250 lines of Babashka.
- EDN is the source of truth - services and integrations in one config
- Don't reinvent Docker Compose - generate YAML, pipe to
docker compose -f - - No temp files - YAML is ephemeral, piped via stdin, never written to disk
- Integrations are the value - automations between services
# 1. Install Babashka (if needed)
# macOS: brew install borkdude/brew/babashka
# Linux: curl -sLO https://raw.githubusercontent.com/babashka/babashka/master/install && bash install
# 2. Setup secrets
cp secrets.edn.example secrets.edn
vim secrets.edn
# 3. Run
./lab plan # See generated docker-compose.yml
./lab up # Start services
./lab status # Check what's running
./lab down # Stop everythingServices:
- Mealie - Recipe management and meal planning
- Donetick - Chore/task tracking
Integrations:
meal-prep-reminder- Checks tomorrow's meals for prep notes (e.g., "prep:defrost salmon"), creates chores in Donetickmealie-weekly-list- Creates weekly shopping list from meal plan
Everything lives in config.edn:
{:secrets #include "secrets.edn"
;; Services → docker-compose.yml
:services
{:mealie {:image "ghcr.io/mealie-recipes/mealie:v3.10.2"
:ports ["9925:9000"]
:env {:BASE_URL #ref [:secrets :mealie-url]}}
:donetick {:image "donetick/donetick"
:ports ["2021:2021"]
:env {:DT_JWT_SECRET #ref [:secrets :donetick :jwt-secret]}}}
;; Integrations → automations
:integrations
{:meal-prep-reminder
{:handler "integrations/meal-prep-reminder.clj"
:description "Cross-service: Mealie -> Donetick"}}}| Command | Description |
|---|---|
./lab up |
Start services (generates YAML on the fly) |
./lab down |
Stop all services |
./lab status |
Show running services |
./lab plan |
Preview the generated YAML (not written to disk) |
./lab logs [service] |
Follow logs |
./lab run <integration> |
Run an integration |
./lab integrations |
List available integrations |
Two types of integrations:
- HTTP calls - Simple webhooks/API requests defined in EDN
- Code handlers - Complex logic in Clojure files
Run manually:
./lab run meal-prep-reminderOr schedule via cron:
crontab -e
# Add: 0 8 * * * /opt/lab/lab run meal-prep-reminderFull documentation in docs/:
- Tutorial - Set up from scratch, run your first integration
- How-to Guides:
- Run on Schedule - Cron and systemd timers
- Write an Integration - Create your own automation
- Deploy to Server - Production deployment
- Reference:
- Configuration - Full config.edn schema
- CLI Commands - All commands
- Architecture - Design decisions and tradeoffs
./test.sh unit # Fast unit tests (~1s)
./test.sh integration # Integration tests with mocks
./test.sh e2e # Real containers - Mealie + Donetick (~90s)
./test.sh all # Everything# On your VPS/homelab server:
# 1. Install Babashka
curl -sLO https://raw.githubusercontent.com/babashka/babashka/master/install
sudo bash install
# 2. Clone/rsync your config
rsync -avz --exclude secrets.edn ./ user@server:/opt/lab/
# 3. On server: setup secrets and run
cd /opt/lab
cp secrets.edn.example secrets.edn
vim secrets.edn
./lab upEdit config.edn:
:services
{:new-service
{:image "nginx:latest"
:ports ["80:80"]
:volumes ["./html:/usr/share/nginx/html:ro"]
:restart "unless-stopped"}}Then: ./lab up
Simple HTTP call:
:integrations
{:notify-on-backup
{:type :http-call
:action {:method :post
:url #ref [:secrets :webhook-url]
:body {:text "Backup completed"}}}}Code handler (for complex logic):
:integrations
{:meal-prep-reminder
{:handler "integrations/meal-prep-reminder.clj"
:description "Check meal plan, create prep chores"}}Handler files receive config and return :ok or :error:
;; integrations/my-handler.clj
(fn [{:keys [secrets]}]
;; your logic here
:ok)Run: ./lab run meal-prep-reminder
| Key | Description |
|---|---|
:image |
Docker image |
:ports |
Port mappings ["host:container"] |
:volumes |
Volume mounts |
:tmpfs |
Tmpfs mounts |
:env |
Environment variables (map) |
:restart |
Restart policy |
:labels |
Docker labels |
:depends-on |
Service dependencies |
:networks |
Networks to join |
| Key | Description |
|---|---|
:handler |
Path to Clojure handler file (for complex logic) |
:type |
:http-call for simple HTTP integrations |
:description |
Human-readable description |
:schedule |
Cron expression (display only, use crontab to schedule) |
:action |
HTTP action: {:method :url :headers :body} |
Body templates: Use :current-date in :body for "Feb 18, 2026" format.
| Tag | Description |
|---|---|
#include "file" |
Include another EDN file |
#ref [:path :to :value] |
Reference another config value |
#env KEY |
Read environment variable |
#join [parts...] |
Concatenate strings |
| Aspect | v1 | v2 |
|---|---|---|
| Lines of code | ~1,700 | ~300 (core) |
| Startup time | ~3s (JVM) | ~30ms |
| Docker API | Direct via contajners | Via docker compose |
| Structure | 6 Polylith components | 1 script + integrations |
| Build step | Required | None |
The tradeoff: v2 delegates reconciliation to Docker Compose. You lose the custom plan diff, but gain simplicity and battle-tested container management.