Single Go binary vs Docker Compose: ops cost compared.
Two deployment shapes dominate self-hosted services in 2026. A single statically linked binary: one process, one file to back up, one thing to upgrade. A Docker Compose stack: declarative multi-service definitions, container isolation, shared lifecycle management. Both are real engineering choices with real tradeoffs. This guide is the honest map of what each shape costs to operate, when each one wins, and why urgentry chose the binary.
20 seconds. A single binary has a smaller failure surface, a lower memory floor, and a shorter path from “download” to “running.” Docker Compose has better multi-service coordination, more consistent environments across teams, and a lower adoption cost if you already run it. The choice depends on your existing infrastructure, not on which shape is abstractly better.
60 seconds. “Single binary” means static linking: all dependencies compiled in, no runtime to install, no library version conflicts. You copy the file, you run the file. On a 1 GB VPS with urgentry, that means ~52 MB resident memory at 400 events per second and a systemd unit as the only moving operational part.
“Docker Compose” means declarative multi-service orchestration: each service in its own container, networking and volume mounts defined in a YAML file, a daemon managing container lifecycle. The power is coordination. The cost is surface area: the daemon, the network namespace, the volume subsystem, the image registry, and each container are all things that can fail independently.
urgentry chose single binary because the service is one thing. When the service is one thing, modeling it as twenty containers is a cost without a benefit.
The two dominant shapes in 2026
Self-hosted software has converged on two deployment patterns. They are not competitors in any strict sense, and most infrastructure shops use both. But for a given service on a given host, you make a choice, and that choice has downstream consequences for every day you operate the service.
The single binary pattern traces back to the original Unix utility model and got a new generation of practitioners through Go, Rust, and the static-linking defaults in both. A Go binary compiled with CGO_DISABLED=1 and GOOS=linux is a self-contained executable. It carries its dependencies inside itself. Copy it to any Linux host with a matching architecture and it runs.
The Docker Compose pattern traces back to the proliferation of microservices and the need to wire them together for local development and small-scale production. Compose lets you express a multi-service dependency graph in YAML, bring it up with one command, and reproduce the same topology anywhere Docker runs. It standardized a lot of “works on my machine” problems.
Both patterns produce running software. The difference is in what the operator carries.
What “single binary” means in practice
Static linking is the mechanism. When you compile a Go program without cgo, the linker pulls all library code directly into the binary. The resulting executable has no runtime dependencies. There is no glibc version to match, no Python interpreter to install, no Node modules to unpack. The binary is the program.
In operational terms:
- Install is a file copy. Upgrade is a file replacement. Rollback is replacing the file again.
- The process supervisor is whatever your OS ships: systemd on modern Linux, launchd on macOS. No container daemon to manage.
- The only things to back up are the binary and the data it writes. For a tracker with SQLite, that is one database file.
- Process isolation is the process boundary, not a container boundary. The binary runs as whatever user the supervisor launches it as.
The tradeoff: the binary bundles its dependencies, which means the author of the binary controls the dependency versions you run. You cannot upgrade a shared library without recompiling or waiting for a new binary release. For most use cases this is fine. For security-critical shared libraries in high-churn CVE environments, it is worth knowing.
A well-built single binary also tends to start fast and run lean. urgentry at idle takes well under 30 MB resident memory. Under steady ingest at 400 events per second, it settles around 52 MB. Those numbers are not the result of heroic optimization; they are what happens when a service written in Go does not carry a Kafka broker or a ClickHouse server inside itself.
What “Docker Compose” means in practice
Docker Compose is a tool for defining and running multi-container applications. The Compose file describes services, networks, and volumes. docker compose up -d starts everything. docker compose down stops it. Each service runs in its own container with its own filesystem, network interface, and process namespace.
The value Compose provides is coordination. When you have a web service that needs a database, a cache, a background worker, and a message broker, Compose lets you describe those dependencies in one file and bring them up together. depends_on gives you ordering. Health checks give you readiness gating. Named volumes give you persistent state that survives container replacement.
The operational reality:
- The Docker daemon is a dependency. If it fails or gets into a bad state, nothing runs. This is rare but non-zero.
- Container images live in a registry. Pulls can fail. Layer caches can grow. Image GC is a periodic maintenance task.
- Volume mounts have permission models that differ from bare-host filesystems. The “permission denied” class of bugs is endemic to new Compose setups.
- Network namespaces between containers add a routing hop. For most use cases this is invisible. Under high ingest it can become a latency floor.
- Each service container is a unit of failure. A Compose stack with eight services has eight things that can OOM independently.
None of this is a reason to avoid Compose. It is the cost of the coordination benefit. For workloads with genuine multi-service complexity, that cost is worthwhile.
Failure modes compared
The failure mode taxonomy is where the choice becomes concrete.
| Failure scenario | Single binary | Docker Compose |
|---|---|---|
| Process crash | Supervisor (systemd) restarts it. One moving part. | Compose restart: unless-stopped restarts it. One moving part per container. |
| Dependency version mismatch | Not possible. All deps are compiled in. | Possible at image build time if base images diverge. Managed by pinning. |
| Docker daemon crash | Not applicable. | All containers stop. Daemon restart required. Service is down until it recovers. |
| Volume permission error | Not applicable. Files on the host are owned normally. | Container UID/GID mismatch with host volume. Common on first setup; fixed by explicit user mapping. |
| OOM on one service | The whole binary OOMs. Supervisor restarts it. | The affected container is killed and restarted. Other containers continue running. |
| Registry pull failure | Not applicable after initial install. | Upgrade fails if the image pull fails. Blocked by registry outage or network issue. |
| Upgrade rollback | Replace the binary. One command. | Pin the previous image tag. Two commands and a Compose pull. |
| Data backup | Back up the data file. One path to know. | Back up named volumes. Volume paths vary by service; documentation is necessary. |
The table shows a pattern: single binary failures are fewer and simpler. Compose failures are more numerous but more contained. For a single-service deployment, the containment benefit of Compose does not apply to the service itself. The service is one thing; splitting it into containers does not make it more fault-tolerant, it just adds more places for it to fail.
For a genuine multi-service stack (web + database + cache + worker), the Compose containment benefit applies. The database can OOM without taking down the web process. The worker can restart without a full stack bounce. That is real value.
The hidden ops costs of each
The announced costs of each shape are documented. The hidden costs are less visible until you have run each for six months.
For single binary, the hidden costs tend to cluster around:
- Backup discipline. On a Compose stack, the conventional wisdom around volume backups forces documentation. With a single binary and SQLite, the backup path is simpler and therefore easier to skip. Teams that skip Litestream or equivalent replication for SQLite discover the gap during a host failure, not before.
- Observability tooling. Compose stacks default to per-container log streams that Docker knows how to aggregate. A single binary writes to stdout and a log file. Shipping those logs to a central system is the operator’s job, not the daemon’s.
- Updates require replacing the binary. There is no image pull workflow. Automation around binary updates is slightly more bespoke than automation around image tags.
For Docker Compose, the hidden costs tend to cluster around:
- Daemon management. The Docker daemon needs occasional GC, log rotation for container stdout, and security patching independent of your application. Small burden, but persistent.
- Image sprawl. Pulled images accumulate on the host.
docker image pruneis a manual step or a cron job that most setups skip until disk is full. - Network namespace surprises. Container-to-container network calls go through virtual ethernet pairs and bridge interfaces. Under sustained high-throughput ingest, the overhead is measurable. Under normal event volumes, it is invisible.
- Compose file drift. The Compose file is the source of truth for the stack, but it drifts from reality when operators make manual container adjustments. Keeping the file and the running stack synchronized is a discipline problem, not a tooling problem.
When each shape wins
Single binary wins
The single binary shape is better when the service is genuinely one thing. One process, one database, one set of configuration values. That describes the large majority of self-hosted tools: error trackers, status pages, analytics collectors, log aggregators, deployment dashboards. These services do not have inherent multi-service complexity. Modeling them as multi-container stacks is an overhead import.
The shape is also better for:
- Small host, tight memory budget. A single Go binary uses what it uses. There is no Docker daemon tax on top of the service’s own footprint.
- Teams with no existing container infrastructure. Setting up Docker for one service means learning Docker, managing the daemon, and understanding the networking model. For a team that does not already run containers, that is a meaningful upfront cost.
- Infrequent deployments. A service that updates monthly benefits less from image-based deployment automation. The binary replacement workflow is simple enough that no tooling is required.
- Minimum-dependency installs. Air-gapped environments, bastion hosts, and restricted corporate machines are all easier targets for a single binary than for a full container runtime.
Docker Compose wins
Compose is better when you already run it for other services on the same host. The per-service overhead of Compose on a host that already has the daemon, the network config, and the log shipping set up is close to zero. Adding one more service to an existing Compose setup is significantly cheaper than learning a new operational model.
Compose is also better for:
- Services with genuine stateful dependencies. If the service genuinely requires Postgres and Redis alongside the application, Compose is the right tool for wiring them together. The dependency graph management is what Compose is for.
- Heterogeneous technology stacks. A service that ships as a Python web app, a Go worker, and a Node-based job processor needs a way to run each with its own runtime. Compose handles that without requiring the host to carry all three runtimes.
- Team standardization on containers. Organizations that mandate container-based deployments for auditability, security scanning, or compliance have a real reason to run everything through the container model, even simple services.
- Reproducible local development environments. Compose was designed for this case. If you want developers and production to run the same topology, Compose is hard to beat.
Why urgentry chose single binary
urgentry is one thing: a Go server that ingests Sentry SDK events, stores them in SQLite or Postgres, and serves a web UI and API. There are no auxiliary services with independent scaling requirements. There is no Kafka broker for event queuing. There is no ClickHouse for analytics. There is no Symbolicator as a separate process. These design choices make urgentry fit on a 1 GB host; they also make the deployment shape obvious.
The specific Go compiler default, CGO_DISABLED=1, means the binary compiles cleanly for Linux on any architecture the Go toolchain targets. The release pipeline produces a single archive per platform. The install script downloads the archive, verifies the signature, and copies the binary. That is the entire install surface.
The operational footprint follows:
- One systemd unit file.
- One configuration file (or environment variables).
- One SQLite file to back up (or a Postgres connection string for larger deployments).
- One binary to upgrade.
The alternative, modeling urgentry as a Compose stack, would have meant one container for the web process. That container would have a daemon dependency, a network namespace, and a volume mount for the database. The container would have zero coordination benefits because there is nothing else to coordinate with. The only output of the Compose model, in this case, is overhead.
Memory is the most visible consequence. urgentry at steady ingest settles around 52 MB resident. Sentry self-host, which models its genuinely multi-service architecture correctly as a multi-container Compose stack, idles at 6 to 8 GB on a 16 GB host. That gap is not purely a deployment shape difference; it reflects genuinely different architectures. But the deployment shape is part of it. A Kafka broker does not have a binary equivalent. A Go HTTP server does.
There is one more reason urgentry chose the binary: the target audience. The teams most likely to self-host an error tracker are the teams with the least appetite for container infrastructure management. A $5 VPS, a systemd unit, and a Litestream backup job is a setup that a solo engineer can maintain indefinitely without Docker expertise. Adding Docker to that stack would filter for a different and smaller audience without making the product better for the majority.
You can also Dockerize a single binary (and probably should for production)
Single binary and container image are orthogonal concepts. A statically linked Go binary compiles into a minimal Docker image trivially. The base image can be scratch or a minimal distroless image. The resulting image is small, contains no shell, and has no OS-level attack surface beyond the binary itself.
FROM scratch
COPY urgentry /urgentry
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/urgentry", "serve"]
urgentry ships exactly this as an official Docker image. If you already run Docker and want to add urgentry to a Compose file alongside other services, the image is there. You get the operational simplicity of the single binary inside the container model your infrastructure expects.
The combination also works well in environments where your deploy tooling is already container-native. Kubernetes, Nomad, and ECS all expect container images. A minimal image built from a static binary fits cleanly into any of those without modification.
The decision tree for production deployments is practical:
- Running Docker already and have other services in Compose? Use the urgentry Docker image in your existing Compose file.
- Fresh VPS with no container infrastructure? Install the binary, write a systemd unit, configure Litestream. Done.
- Kubernetes or another orchestrator? Use the container image. Mount SQLite storage on a PVC or point at an external Postgres.
The shape you pick should match the infrastructure you already have, not the one you imagine building. Neither model is wrong. The single binary is the default because it fits the most common starting point. The container image exists because the starting point is not always a bare VPS.
Frequently asked questions
Is a single binary actually easier to operate than Docker Compose?
For a single-service deployment on one host, yes. One process, one file to back up, one thing to upgrade, one thing that can crash. The failure surface is smaller by construction. Compose adds value when you have multiple services that genuinely need coordination.
Does a single binary mean no container at all?
No. A single binary and a container image are orthogonal choices. You can package a single Go binary into a minimal Docker image and run it under Compose. urgentry ships a Docker image for exactly this. You get the operational simplicity of the binary and the infrastructure consistency of containers.
When does Docker Compose win over a bare binary?
When you already run Compose for other services on the same host, when you have stateful dependencies that genuinely need separate lifecycle management, when your deploy tooling assumes container images, or when a team standard mandates containers for auditability.
What breaks in a single binary that does not break in Compose?
The process itself. A single binary is a single point of failure at the process level. If it panics and the supervisor does not restart it, the whole service is down. Compose with health checks can restart individual containers, but a crashed process is a crashed process under either model unless you have an external supervisor or orchestrator.
What is the memory footprint difference in practice?
urgentry at 400 events per second settles around 52 MB resident memory. A comparable Sentry self-host Compose stack idles at 6 to 8 GB on its documented 16 GB minimum host. The gap reflects genuinely different architectures: one process vs twenty containers with their own runtimes and in-process databases.
Sources and further reading
- getsentry/self-hosted — current minimum hardware spec and the full Compose stack composition.
- getsentry/self-hosted#3566, #3467, #1719 — operator-reported memory exhaustion and OOM threads on the documented minimum hardware.
- Go cgo documentation — CGO_DISABLED and static linking behavior in the Go toolchain.
- Docker Compose documentation — official reference for Compose file format, health checks, and volume semantics.
- Litestream — SQLite WAL streaming replication to S3-compatible storage.
- distroless container images — minimal base images for packaging static binaries.
Try the single-binary path.
urgentry installs as one binary on any Linux host. SQLite default, Postgres optional. No Docker required, Docker image available if you already run containers.