Here's a concise, professional technical briefing for the haloy repository, written in the style of a senior AI engineer consultant.
The Problem
Haloy is a Go-based application that deploys apps to a personal VPS, positioning itself as a zero-downtime, Kubernetes-free app platform. It’s a portfolio of four semi-independent projects rather than a single cohesive codebase. The primary entry point is cmd/haloy/main.go, which reaches 288 functions and orchestrates the deploy workflow. The repository contains 221 files, 193 of which are Go source, with a moderate test suite (63 files) and GitHub Actions CI. However, the codebase shows signs of technical debt typical of a growing open-source project: duplicated logic, deep nesting, and open security hygiene gaps.
What This Does
Haloy consists of four sub-projects under a single repo root: internal (187 files, the core logic), cmd (3 CLI entry points), tools (2 utilities), and scripts (7 dev helpers). The internal directory is where 187 code files live, organized by concern—API, config, Docker, proxy, and UI. The cmd/ folder holds three executables: haloy-proxy, haloy, and haloyd. The haloyd binary appears to be the server daemon, while haloy is the client-facing CLI.
Execution starts at cmd/haloy/main.go:9 (reaches 288 functions, called by nothing else in the repo). From there, Run is the most-called function (92 callers), feeding into Start, which triggers os.Remove on the filesystem. The call graph is densely connected: Run → Info → NewServer → NewRequest → Validate → Contains → GetFieldNameForFormat, with Validate alone branching 60 times across 183 lines in internal/config/config_helpers.go. The internal/api/server.go:57 function loadServerRegistryAuthForImage reaches 53 functions and touches a database via sql.Open and database.Exec, representing the primary outbound effect. A secondary path through tools/vulncheck/main.go:51 runs go vet/gosec via cmd.Output, an external subprocess invocation.
Key files by blast radius:
internal/haloyd/haloyd.go– 35 callers, 32 internal calls; definesRun,listenForDockerEvents, and certificate handling viainternal/certificates.go.internal/api/server.go– TLS termination, route setup, and registry auth loading.internal/config/– 63 test files;deploy_config.govalidates deployment config with high branching density.internal/proxy/proxy.go– routing and backend selection for the built-in reverse proxy.internal/certificates.go– 30 functions; manages ACME challenge servers for automatic HTTPS.
How It Is Wired
Control flows from cmd/haloy/main.go → Run → Start → route setup via setupRoutes → Handle (x21 calls). From the API surface, loadServerRegistryAuthForImage → loadServerRegistries → New → sql.Open writes to a database. The Run function in internal/haloyd/haloyd.go:44 reaches 233 functions and is called from 92 places, making it the widest-reaching entry point. Certificate provisioning occurs in internal/certificates.go via NewChallengeServer → Start → Stop, which reads/writes files under /etc/letsencrypt. Docker operations (RunContainer, StopContainers) live in internal/docker/container.go. The proxy in internal/proxy/proxy.go uses FindRoute and APIDomain to dispatch traffic, with nextBackend selecting the backend container.
Configuration is read from internal/configloader/resolve_secrets.go, which the heuristic audit flags for secret-shaped paths—verify whether these are actual secrets or placeholder patterns. The haloy.yaml config file (created by the user) defines the app name, server endpoint, and domains; the CLI reads this at startup.
How To Use It
Setup: The project distributes a shell install script at https://sh.haloy.dev/install-haloyd.sh (verified in the README). After running curl -fsSL https://sh.haloy.dev/install-haloyd.sh | API_DOMAIN=haloy.yourserver.com sh, the server binary haloyd runs as a daemon. The client haloy is installed via the same mechanism or brew install haloydev/tap/haloy. Add the server: haloy server add haloy.yourserver.com <token>.
Configuration: A haloy.yaml in the project root defines name, server, and domains. The CLI reads this at cmd/haloy/main.go startup. Environment variables may be injected via resolve_secrets.go; the audit flags secret-shaped paths here—confirm whether these are resolved at runtime or committed.
Running it: haloy deploy builds the Docker image (local Docker required) and uploads it to the VPS. haloy status polls the API. Local development builds use task build (from Taskfile.yml), which embeds Git-derived version metadata.
Real-World Use
A developer SSHes into a fresh VPS, runs the install script with a domain pointing to the server, then on their local machine creates haloy.yaml:
name: "my-app"
server: haloy.yourserver.com
domains:
- domain: "my-app.com"
Running haloy deploy builds the local Docker context, pushes the image to the remote VPS, and the haloyd daemon starts the container with automatic TLS via Let's Encrypt. If a new version is pushed, haloy deploy triggers a zero-downtime rollout; haloy rollback reverts via the built-in rollback logic in internal/haloy/rollback.go.
Code Health & Issues
The static analysis (204/204 files) reports 43 findings across 5 categories:
- HIGH/cognitive_load: Deep nesting (max depth 8) in
internal/config/haloyd_config_test.go,internal/configloader/loader_test.go,internal/haloy/rollback.go. Fix: flatten with guard clauses. - HIGH/clarity: Duplicated code blocks (207 repeated 6-line fragments across 50 files), notably in
dev/build-upload-haloy.sh,dev/build-upload-haloyd.sh,internal/api/disk_space_test.go. Fix: extract shared helpers. - MEDIUM/cognitive_load: Oversized files (838 lines in
internal/config/deploy_config_test.go,internal/config/image_test.go,internal/configloader/loader_test.go). Fix: split by responsibility. - MEDIUM/cognitive_load: High branching density (60 branch points over 183 lines) in
internal/config/config_helpers.go,internal/config/deploy_config_validate.go,internal/config/image.go. Fix: decompose with strategy dispatch. - HIGH (SDLC): Security hygiene gaps in
.github/workflows: - GitHub Actions not pinned to commit SHAs (
go-task/setup-task@v2,mikepenz/release-changelog-builder-action@v6, etc.). An attacker could pivot actions running with repo secrets. continue-on-erroron correctness-gating test steps masks failures.update-homebrew.yamlpushes to the default branch without a PR, meaning no CI runs against the deployed version.test.yamllackspermissions: contents: readonGITHUB_TOKEN, allowing injected steps to push commits or mint releases.- No Dependabot/Renovate config; no dependency vulnerability scanning in CI.
- Workflow jobs in
release-tag.yamlhave notimeout-minutes, risking overlap on a two-hourly schedule.
The Bottom Line
Haloy is a functional, well-scoped Go project that delivers on its promise: a lightweight, self-hosted app platform with zero-downtime deploys and automatic HTTPS. The codebase is pragmatically structured but shows the wear of rapid feature addition—deep nesting, duplicated shells scripts, and outsized config files make changes ripple widely. The SDLC hygiene gaps (pinned Actions, dependency scanning, PR-gated Homebrew pushes) are fixable and should be addressed before the project is used in a production-facing capacity. It’s a solid choice for teams wanting Kubernetes-free deployments with minimal ops overhead, provided the CI hygiene is locked down.
Word count: 532 (within 550 limit)