{"id":50307254,"url":"https://github.com/hra42/deployer","last_synced_at":"2026-05-28T17:30:39.266Z","repository":{"id":356180756,"uuid":"1231005877","full_name":"hra42/deployer","owner":"hra42","description":"A lightweight Go CLI for one-command deployment of containerized apps to a single host via SSH, Docker Compose, Traefik, and Cloudflare (DNS + Zero Trust).","archived":false,"fork":false,"pushed_at":"2026-05-14T11:00:56.000Z","size":43,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-14T11:17:46.452Z","etag":null,"topics":["automation","cli","cloudflare","deployment","docker-compose","go","self-hosted","ssh","traefik","zero-trust"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"unlicense","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hra42.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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":null,"dco":null,"cla":null}},"created_at":"2026-05-06T14:31:00.000Z","updated_at":"2026-05-14T11:00:49.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/hra42/deployer","commit_stats":null,"previous_names":["hra42/deployer"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/hra42/deployer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hra42%2Fdeployer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hra42%2Fdeployer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hra42%2Fdeployer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hra42%2Fdeployer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hra42","download_url":"https://codeload.github.com/hra42/deployer/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hra42%2Fdeployer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33619965,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-28T02:00:06.440Z","response_time":99,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["automation","cli","cloudflare","deployment","docker-compose","go","self-hosted","ssh","traefik","zero-trust"],"created_at":"2026-05-28T17:30:38.754Z","updated_at":"2026-05-28T17:30:39.261Z","avatar_url":"https://github.com/hra42.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# deployer\n\nA small Go CLI that ships a containerised app to a single host with one command. Point it at a GitHub repo and a domain; it clones the repo over SSH, brings up the Compose stack behind a Traefik instance you already run, sets the DNS record on Cloudflare, and protects the app with a pre-existing Cloudflare Zero Trust policy.\n\nThe boring config (SSH host, key path, GitHub token, Cloudflare credentials) lives in `~/.deployer.yml`. You set it up once with `deployer setup` and re-use it for every deploy.\n\n## Install\n\nOne-line install (downloads the latest release binary, verifies its SHA-256, installs to `/usr/local/bin` or `$HOME/.local/bin`):\n\n```sh\ncurl -fsSL https://deployer.hra42.lol/install | sh\n```\n\nPin a specific version or override the install location:\n\n```sh\ncurl -fsSL https://deployer.hra42.lol/install | VERSION=v0.1.0 sh\ncurl -fsSL https://deployer.hra42.lol/install | INSTALL_DIR=$HOME/.local/bin sh\n```\n\nSupported targets: `darwin/arm64`, `linux/arm64`, `linux/amd64`. macOS x86_64 is not built — install from source instead.\n\nFrom source:\n\n```sh\ngo install github.com/hra42/deployer@latest\n```\n\n## First-time setup\n\n```sh\ndeployer setup\n```\n\nWalks through every config field interactively and writes `~/.deployer.yml` (overwriting any existing file). See [Config reference](#config-reference) for what each field means.\n\n## Deploy\n\n```sh\ndeployer deploy --repo owner/name --domain app.example.com\n```\n\n`--repo` accepts any of `owner/name`, `github.com/owner/name`, or `https://github.com/owner/name`.\n\nExpected output on a successful run:\n\n```\n==\u003e [1/6] Connect to host\n    ✓ connected to deploy@1.2.3.4\n\n==\u003e [2/6] Sync repo\n    git clone into /srv/apps/app-example-com\n    ✓ repo synced\n\n==\u003e [3/6] Validate compose files\n    ✓ Dockerfile and docker-compose.yml present\n\n==\u003e [4/6] Bring up containers\n    ✓ project app-example-com up\n\n==\u003e [5/6] Cloudflare DNS\n    creating CNAME app.example.com → host.example.com\n    ✓ proxied CNAME app.example.com → host.example.com\n\n==\u003e [6/6] Cloudflare Zero Trust\n    creating Access app for app.example.com\n    ✓ Access app for app.example.com protected by policy \u003cid\u003e\n\n── Summary ──\n    ✓ Connect to host        ok — deploy@1.2.3.4\n    ✓ Sync repo              ok — /srv/apps/app-example-com\n    ✓ Validate compose files ok\n    ✓ Bring up containers    ok — project app-example-com\n    ✓ Cloudflare DNS         ok — CNAME app.example.com → host.example.com\n    ✓ Cloudflare Zero Trust  ok — policy \u003cid\u003e\n    ✓ deploy complete: app.example.com\n```\n\nIf a phase fails, the summary still prints — so you can see at a glance which phases ran, which were skipped, and which failed (e.g. containers up but DNS broken).\n\n## Host requirements\n\nThe target host must already have:\n\n- **Docker** with the **Compose v2** plugin (`docker compose ...`, not `docker-compose`).\n- A **Traefik** container running on the host, configured with HTTPS entrypoints and a cert resolver.\n- An **external Docker network** that Traefik watches and that every deployed app joins.\n\ndeployer does **not** install or manage any of these. It assumes they're set up.\n\nA minimal Traefik host setup (for reference) looks like:\n\n```yaml\n# /srv/traefik/docker-compose.yml — runs on the host, started once\nservices:\n  traefik:\n    image: traefik:v3\n    restart: unless-stopped\n    command:\n      - --providers.docker=true\n      - --providers.docker.exposedbydefault=false\n      - --providers.docker.network=web\n      - --entrypoints.web.address=:80\n      - --entrypoints.websecure.address=:443\n      - --certificatesresolvers.le.acme.email=you@example.com\n      - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json\n      - --certificatesresolvers.le.acme.tlschallenge=true\n    ports: [\"80:80\", \"443:443\"]\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock:ro\n      - ./letsencrypt:/letsencrypt\n    networks: [web]\n\nnetworks:\n  web:\n    external: true\n```\n\nCreate the network once: `docker network create web`. Then put `web` (or whatever you named it) in `traefik_network` in your config.\n\n## App repo requirements\n\nEvery repo you deploy through deployer **must** include:\n\n1. A `Dockerfile` at the repo root.\n2. A `docker-compose.yml` at the repo root that joins the external Traefik network and declares Traefik labels for the routed service.\n\nMinimal copy-pasteable template:\n\n```yaml\n# docker-compose.yml in your app repo\nservices:\n  app:\n    build: .\n    restart: unless-stopped\n    networks: [web]\n    labels:\n      - traefik.enable=true\n      - traefik.docker.network=web\n      - traefik.http.routers.app.rule=Host(`app.example.com`)\n      - traefik.http.routers.app.entrypoints=websecure\n      - traefik.http.routers.app.tls.certresolver=le\n      - traefik.http.services.app.loadbalancer.server.port=8080\n\nnetworks:\n  web:\n    external: true\n```\n\nNotes:\n\n- Replace `web` with whatever you set for `traefik_network`.\n- `Host(...)` must match the `--domain` you pass to `deployer deploy`.\n- `loadbalancer.server.port` should match whatever port your container listens on internally.\n- At deploy time, `COMPOSE_PROJECT_NAME` is set to a slug of the domain (e.g. `app.example.com` → `app-example-com`), so multiple deploys on the same host don't collide. Keep service names generic (`app`, `web`, `api`) — they'll be namespaced by the project name.\n\nIf `Dockerfile` or `docker-compose.yml` is missing, deploy hard-fails with a clear message.\n\n## Config reference\n\nAll fields live in `~/.deployer.yml`. CLI flags (where they exist) override the file at runtime.\n\n| Field | Required | Description |\n| --- | --- | --- |\n| `ssh_host` | yes | Target host as `user@ip-or-hostname`. |\n| `ssh_key_path` | yes | Path to the private SSH key for that host. |\n| `github_token` | yes | GitHub PAT with `repo` scope; injected into the HTTPS clone URL. |\n| `clone_path` | yes | Base directory on the host where repos are cloned (e.g. `/srv/apps`). Each app lives at `\u003cclone_path\u003e/\u003cdomain-slug\u003e`. |\n| `traefik_network` | yes | Name of the external Docker network Traefik watches. |\n| `cloudflare_api_token` | optional | Cloudflare API token. If unset, DNS and Zero Trust phases are skipped with a warning. |\n| `cloudflare_zone_id` | optional | Zone ID for the domain's parent zone. Required for the DNS phase. |\n| `cloudflare_account_id` | optional | Cloudflare account ID. Required for the Zero Trust phase. |\n| `zero_trust_policy_id` | optional | ID of a pre-existing Access policy to attach to each deployed app. |\n| `cname_target` | optional | What every CNAME points at. Defaults to `host.example.com`. |\n\nExample:\n\n```yaml\nssh_host: deploy@1.2.3.4\nssh_key_path: /Users/me/.ssh/id_ed25519\ngithub_token: ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nclone_path: /srv/apps\ntraefik_network: web\ncloudflare_api_token: cf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\ncloudflare_zone_id: 0123456789abcdef0123456789abcdef\ncloudflare_account_id: fedcba9876543210fedcba9876543210\nzero_trust_policy_id: 11111111-2222-3333-4444-555555555555\ncname_target: host.example.com\n```\n\nIf the Cloudflare fields are blank, deployer still ships the containers — it just skips DNS and Zero Trust and tells you so in the summary.\n\n## What deployer does NOT do\n\n- Install Docker or Compose on the host.\n- Set up or run Traefik — bring your own.\n- Create or manage Cloudflare Zero Trust policies. It only **attaches** a policy you've already created, by ID.\n- Roll back on partial failure. If phase 5 fails after phase 4 succeeded, the containers stay up; the summary tells you so. Fix the underlying issue and re-run.\n- Verify SSH host keys against `known_hosts` (yet). The current build uses `InsecureIgnoreHostKey`. Don't run this against hosts you don't already trust.\n\n## License\n\n[The Unlicense](LICENSE) — public domain. Do whatever you want.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhra42%2Fdeployer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhra42%2Fdeployer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhra42%2Fdeployer/lists"}