{"id":43690267,"url":"https://github.com/felipecsl/hl","last_synced_at":"2026-02-05T03:09:17.533Z","repository":{"id":319217088,"uuid":"1077768977","full_name":"felipecsl/hl","owner":"felipecsl","description":"Turn your homelab into a PaaS","archived":false,"fork":false,"pushed_at":"2026-01-15T19:27:15.000Z","size":10648,"stargazers_count":1,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-01-15T21:50:22.750Z","etag":null,"topics":["deployment","devops","homelab"],"latest_commit_sha":null,"homepage":"https://felipecsl.github.io/hl-docs/","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/felipecsl.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-10-16T17:58:01.000Z","updated_at":"2026-01-15T19:27:19.000Z","dependencies_parsed_at":"2025-10-18T13:30:23.925Z","dependency_job_id":"a9df8083-b1f3-42dd-afab-b55a7f086559","html_url":"https://github.com/felipecsl/hl","commit_stats":null,"previous_names":["felipecsl/hl"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/felipecsl/hl","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felipecsl%2Fhl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felipecsl%2Fhl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felipecsl%2Fhl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felipecsl%2Fhl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/felipecsl","download_url":"https://codeload.github.com/felipecsl/hl/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felipecsl%2Fhl/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29108474,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-05T02:48:39.389Z","status":"ssl_error","status_checked_at":"2026-02-05T02:48:27.400Z","response_time":65,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["deployment","devops","homelab"],"created_at":"2026-02-05T03:09:16.974Z","updated_at":"2026-02-05T03:09:17.527Z","avatar_url":"https://github.com/felipecsl.png","language":"Rust","readme":"# `hl` — A tiny, deterministic “git-push deploys” CLI for single-host deployments\n\n**Goal:** Keep deployments on a single host dead-simple, explicit, and reliable — without adopting a full orchestrator.\n\n## Getting started\n\n### Quick Install\n\nUse the installation script for an interactive setup:\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/felipecsl/hl/master/install.sh | bash\n```\n\nOr download and run it manually:\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/felipecsl/hl/master/install.sh -o install.sh\nchmod +x install.sh\n./install.sh\n```\n\nThe script will:\n\n- Prompt for your remote SSH username and hostname\n- Test the SSH connection (requires SSH key authentication)\n- Download the latest `hl` release binary from Github\n- `scp` it to your server\n- Create a wrapper script that invokes it via `ssh`\n- Add the wrapper to your `$PATH`\n\n**Note:** This assumes SSH public key authentication is already configured. If not, set it up first with `ssh-copy-id user@hostname`.\n\n### Manual Installation\n\nFirst, build and copy the binary to your remote server:\n\n```bash\ncargo build --release\nscp target/release/hl \u003chost\u003e:~/.local/bin\n```\n\nThen create a wrapper script for invoking `hl` on the remote host via `ssh`:\n\n```bash\ncat \u003e ~/.local/bin/hl \u003c\u003c'BASH'\n#!/usr/bin/env bash\nset -euo pipefail\nREMOTE_USER=\"${REMOTE_USER:-homelab}\"\nREMOTE_HOST=\"${REMOTE_HOST:-homelab.local}\"   # or your server fqdn\nssh \"${REMOTE_USER}@${REMOTE_HOST}\" \"~/.local/bin/hl $*\"\nBASH\nchmod +x ~/.local/bin/hl\n```\n\n---\n\n## Motivation \u0026 Goals\n\n**What this solves**\n\n- You have a single VPS/home server and multiple apps.\n- You want **Heroku-style** “`git push` → build → deploy”, but:\n  - no complex control planes,\n  - no multi-host orchestration,\n  - no hidden daemons updating containers behind your back.\n\n**Design goals**\n\n- **Deterministic:** deploy exactly the pushed commit, no working tree drift.\n- **Explicit:** no Watchtower; restarts are performed by `hl`.\n- **Boring primitives:** Git, Docker (Buildx), Traefik, Docker Compose, systemd.\n- **Ergonomics:** one per-app folder on the server (`~/hl/apps/\u003capp\u003e`), one systemd unit, minimal YAML.\n- **Small blast radius:** per-app everything (compose/env/config) — easy to reason about and recover.\n\n---\n\n## How It Works\n\n**Core flow**\n\n1. **Push:** You push to a **bare repo** on the server (e.g., `~/hl/git/\u003capp\u003e.git`).\n2. **Hook → `hl deploy`:** The repo’s `post-receive` hook invokes `hl deploy` with `--sha` and `--branch`.\n3. **Export commit:** `hl` **exports that exact commit** (via `git archive`) to an **ephemeral build context**.\n4. **Build \u0026 push image:** Docker **Buildx** builds and pushes tags:\n   - `:\u003cshortsha\u003e`, `:\u003cbranch\u003e-\u003cshortsha\u003e`, and `:latest`.\n\n5. **Migrations (optional):** `hl` runs DB migrations in a one-off container using the new image tag.\n6. **Retag and restart:** `hl` **retags `:latest`** to the new sha and **restarts** the app using **systemd** (which runs `docker compose` under the hood).\n7. **Health-gate:** `hl` waits until the app is healthy. Deploy completes only once healthy.\n\n**Runtime layout (per app)**\n\n```\n~/hl/apps/\u003capp\u003e/\n  compose.yml              # app service + Traefik labels\n  compose.\u003caccessory\u003e.yml  # e.g., compose.postgres.yml\n  .env                     # runtime secrets (0600)\n  hl.yml                   # server-owned app config\n  pgdata/ ...              # volumes (if using Postgres)\nsystemd: app-\u003capp\u003e.service # enabled at boot\n```\n\n**Networking \u0026 routing**\n\n- Traefik runs separately and exposes `web`/`websecure`.\n- Apps join a shared Docker network (e.g., `traefik_proxy`) and advertise via labels.\n- Certificates are issued by ACME (e.g., Route53 DNS challenge).\n\n---\n\n## How It’s Different From Existing Tools\n\n| Tool                 | What it is                                                 | Where `hl` differs                                                                                                               |\n| -------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |\n| **Watchtower**       | Image watcher that auto-updates containers                 | `hl` **does not auto-update**. Deploys are explicit and health-gated.                                                            |\n| **Kamal**            | SSH deploy orchestrator (blue/green, fan-out, hooks)       | `hl` intentionally avoids multi-host/fleet features and blue/green. It’s a **single-host release tool** with simpler ergonomics. |\n| **Docker Swarm/K8s** | Schedulers with service discovery and reconciliation loops | `hl` doesn’t introduce a scheduler. It leans on systemd + compose for simple, predictable runtime.                               |\n\n**Bottom line:** `hl` is a small, single-host release manager that turns a Git push into a reproducible build and a clean, health-checked restart — with Traefik for ingress. No magic daemons, no control plane.\n\n---\n\n## Pros / Cons of the Approach\n\n**Pros**\n\n- **Simplicity:** Git hooks + Docker Buildx + Compose + systemd.\n- **Deterministic builds:** every deploy uses `git archive` of the exact commit.\n- **Fast rollback:** `hl rollback \u003capp\u003e \u003csha\u003e` retags and health-checks.\n- **Clear logs:** `journalctl -u app-\u003capp\u003e.service` for runtime; deploy logs in hook/CLI output.\n- **Separation of concerns:** build (ephemeral) vs. runtime (per-app directory).\n- **Server-owned config:** domains, networks, health, secrets stay off the image.\n\n**Cons / Trade-offs**\n\n- **No blue/green:** restarts are in-place (health-gated, but not traffic-switched).\n- **Single host:** no parallel fan-out or placement strategies.\n- **Manual accessories:** DBs/Redis are compose fragments, not managed clusters.\n- **Layer caching:** ephemeral build contexts reduce cache reuse (you can configure a persistent workspace if needed).\n\n---\n\n## Configuration (`hl.yml`)\n\n\u003e **Server-owned** file at `~/hl/apps/\u003capp\u003e/hl.yml`.\n\n```yaml\napp: recipes\nimage: registry.example.com/recipes\ndomain: recipes.example.com\nservicePort: 8080\nresolver: myresolver # Traefik ACME resolver name\nnetwork: traefik_proxy # Docker network shared with Traefik\nplatforms: linux/amd64 # Buildx platforms\n\nhealth:\n  url: http://recipes:8080/healthz\n  interval: 2s\n  timeout: 45s\n\nmigrations:\n  command: [\"bin/rails\", \"db:migrate\"]\n  env:\n    RAILS_ENV: \"production\"\n\nsecrets:\n  - RAILS_MASTER_KEY\n  - SECRET_KEY_BASE\n```\n\n---\n\n## Health Checks\n\n`hl` runs a short-lived `curl` container on the **app network** to hit `http://\u003cservice\u003e:\u003cport\u003e\u003cpath\u003e`. Works even when nothing is published on host ports.\n\n**Optional container healthcheck** in `compose.yml` keeps startup ordering crisp:\n\n```yaml\nservices:\n  recipes:\n    healthcheck:\n      test:\n        [\n          \"CMD-SHELL\",\n          \"wget -qO- http://localhost:8080/healthz \u003e/dev/null 2\u003e\u00261 || exit 1\",\n        ]\n      interval: 5s\n      timeout: 3s\n      retries: 10\n```\n\n---\n\n## Accessories (Example: Postgres)\n\n`hl accessory add \u003capp\u003e postgres` will:\n\n- Write `compose.postgres.yml` with a healthy `pg` service on the same network.\n- Add `depends_on: { pg: { condition: service_healthy } }` to your app (via the fragment).\n- Generate/update `.env` with:\n  - `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`\n  - `DATABASE_URL=postgres://USER:PASSWORD@pg:5432/DB`\n\n- Patch the systemd unit to run with **both** files:\n  `docker compose -f compose.yml -f compose.postgres.yml up -d`\n- Restart the unit.\n\n\u003e Same pattern can add **Redis** (`compose.redis.yml`, `REDIS_URL=redis://redis:6379/0`) and others.\n\n---\n\n### 1) Bootstrap an app\n\n```bash\n# Create runtime home, compose, hl.yml, systemd\nhl init \\\n  --app recipes \\\n  --image registry.example.com/recipes \\\n  --domain recipes.example.com \\\n  --port 8080\n```\n\nThis creates:\n\n- `~/hl/apps/recipes/{compose.yml,.env,hl.yml}`\n- `app-recipes.service` (enabled)\n\n### 2) Environment Variables\n\nAdd optional `--build` for build-time env vars (e.g., docker build secrets).\n\n```bash\nhl env set [--build] recipes RAILS_MASTER_KEY=... SECRET_KEY_BASE=...\nhl env ls recipes  # prints keys with values redacted\n```\n\n### 3) Add Postgres (optional)\n\n```bash\nhl accessory add recipes postgres --version 16\n# Writes compose.postgres.yml, updates systemd, restarts.\n```\n\n### 4) Push to deploy\n\n```bash\ngit remote add production ssh://\u003cuser\u003e@\u003chost\u003e/home/\u003cuser\u003e/hl/git/recipes.git\ngit push production master\n```\n\nThe pipeline:\n\n- Exports the pushed commit\n- Builds \u0026 pushes image (`:\u003csha\u003e`, `:\u003cbranch\u003e-\u003csha\u003e`, `:latest`)\n- Runs migrations on `:\u003csha\u003e`\n- Retags `:latest` → `:\u003csha\u003e`\n- Restarts `app-recipes.service`\n- Waits for health\n\n### 5) Rollback\n\n```bash\nhl rollback recipes eef6fc6\n```\n\nRetags `:latest` to the specified sha, restarts, and health-checks.\n\n---\n\n## Available Commands\n\n\u003e **Command names/flags may differ in your Rust implementation, but this is the intended surface:**\n\n- `hl init --app \u003cname\u003e --image \u003cref\u003e --domain \u003chost\u003e --port \u003cnum\u003e [--network traefik_proxy] [--resolver myresolver]`\n  Create `compose.yml`, `.env`, `hl.yml`, and systemd unit.\n\n- `hl deploy --app \u003cname\u003e --sha \u003csha\u003e [--branch \u003cname\u003e]`\n  Export commit → build \u0026 push → migrate → retag → restart (systemd) → health-gate.\n\n- `hl rollback \u003capp\u003e \u003csha\u003e`\n  Retag `:latest` → `\u003csha\u003e`, restart, health-gate.\n\n- `hl secrets set \u003capp\u003e KEY=VALUE [KEY=VALUE ...]`\n  Update the app’s `.env` (0600).\n  `hl secrets ls \u003capp\u003e` to list keys redacted.\n\n- `hl accessory add \u003capp\u003e postgres [--version \u003cv\u003e] [--user \u003cu\u003e] [--db \u003cname\u003e] [--password \u003cp\u003e]`\n  Add Postgres as an accessory and wire `DATABASE_URL`.\n\n- `hl accessory add \u003capp\u003e redis [--version \u003cv\u003e]`\n  Add Redis as an accessory and wire `REDIS_URL`.\n\n---\n\n## Example `compose.web.yml` (app)\n\n```yaml\nservices:\n  recipes:\n    image: registry.example.com/recipes:latest\n    restart: unless-stopped\n    env_file: [.env]\n    networks: [traefik_proxy]\n    labels:\n      - \"traefik.enable=true\"\n      - \"traefik.http.routers.recipes.rule=Host(`recipes.example.com`)\"\n      - \"traefik.http.routers.recipes.entrypoints=websecure\"\n      - \"traefik.http.routers.recipes.tls.certresolver=myresolver\"\n      - \"traefik.http.services.recipes.loadbalancer.server.port=${SERVICE_PORT}\"\n\nnetworks:\n  traefik_proxy:\n    external: true\n    name: traefik_proxy\n```\n\n---\n\n## Security \u0026 Operational Notes\n\n- **Env vars:** keep in `.env` with mode `0600`. Do **not** bake secrets into images.\n- **Registry auth:** the server must be logged in to your registry prior to deploys.\n- **Traefik network:** ensure **one canonical network name** (e.g., `traefik_proxy`) shared by Traefik and apps.\n- **Backups:** if using Postgres accessory, back up `pgdata/` and consider nightly `pg_dump`.\n- **Layer cache:** if builds become slow, configure a persistent build workspace for better cache reuse.\n\n---\n\n## Roadmap / Next Steps\n\n- **Accessories:** Redis helper (compose fragment + `REDIS_URL`), S3-compatible storage docs.\n- **Hooks:** `preDeploy`/`postDeploy` (assets precompile, cache warmers).\n- **Diagnostics:** `hl status/logs` wrapping `systemctl`/`journalctl` and `docker compose ps/logs`.\n- **Rollback UX:** `hl releases \u003capp\u003e` to list recent SHAs/tags with timestamps.\n- **Build cache toggle:** support persistent build workspace path in `homelab.yml`.\n- **Backup tasks:** `hl pg backup/restore` helpers.\n- **CI bridge:** optional GitHub Actions job that invokes the server over SSH and runs `hl deploy`.\n\n---\n\n## Architecture (At a Glance)\n\n```\nLaptop                                     Server\n------                                     -------------------------------------\ngit push  ───────────────────────────────▶  bare repo: \u003capp\u003e.git\n                                           post-receive → hl deploy --app --sha --branch\n                                           ├─ export commit (git archive) → ephemeral dir\n                                           ├─ docker buildx build --push (:\u003csha\u003e, :\u003cbranch\u003e-\u003csha\u003e, :latest)\n                                           ├─ run migrations (docker run ... :\u003csha\u003e)\n                                           ├─ retag :latest → :\u003csha\u003e + push\n                                           ├─ systemctl restart app-\u003capp\u003e.service\n                                           └─ health-gate (docker-mode or http-mode)\n\nRuntime\n-------\nTraefik ◀─────────────── docker network (traefik_proxy) ────────────────▶ App container\n                                                      └──────(optional)▶ Postgres accessory\n```\n\n---\n\n## FAQ\n\n**Q: Where does the build context come from?**\nA: From the **bare repo** you pushed to. `hl` uses `git archive` for the exact commit — no persistent working tree.\n\n**Q: Why not Watchtower for restarts?**\nA: To keep rollouts **explicit** and **health-gated** in one place (the deploy command).\n\n**Q: Can I pin a version?**\nA: Yes. Use `docker image` tags directly or `hl rollback \u003csha\u003e` to retag `:latest` to a known-good image.\n\n**Q: Can I use a public URL for health checks?**\nA: Yes — set `health.mode: http` with a URL. Docker-mode is preferred for independence from DNS/ACME.\n\n---\n\n## License / Contributing\n\n- MIT\n- Contributions welcome — especially adapters for accessories (Redis), backup helpers, and CI bridges.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffelipecsl%2Fhl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffelipecsl%2Fhl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffelipecsl%2Fhl/lists"}