# Homer 11 captures HEP into DuckLake and queries it three ways

> Homer 11 is one Go binary that takes HEP packets off VoIP infrastructure, writes them into DuckLake Parquet tables and serves them back over gRPC, REST, Arrow FlightSQL and a SQL shell. The rough edges are where its own diagram and its own sample config disagree on ports.

**sipcapture/homer** — HOMER - 100% Open-Source SIP, VoIP, RTC Packet Capture & Monitoring

- Repository: https://github.com/sipcapture/homer
- Website: http://sipcapture.github.io/homer/
- Stars: 2,031 · Forks: 295
- Language: Go
- License: NOASSERTION
- Published: 2026-10-14 · Updated: 2026-10-14 · Language: en
- Canonical page: https://hysenlabs.com/projects/sipcapture-homer

## Homer 11 captures HEP and writes Parquet inside one binary

Homer describes itself as open source telecom observability and dates that line to 2011; version 11 keeps the work in a single Go binary built for X64 and ARM64 on Linux and MacOS. The problem it addresses is narrow. When a SIP, VoIP or RTC call misbehaves you need the signalling messages themselves, correlated and searchable, not a packet dump someone has to open by hand. Repository topics include kamailio and opensips. Backwards compatibility with all HEPv3 agents is stated outright, so capture points already in place can keep forwarding into it.

The monolith divides into four modules. Ingest accepts HEP packets over UDP, TCP, TLS, HTTP and HTTPS, and the sample configuration binds those listeners to 9060, 9061 and 9080. Storage commits what arrives into a DuckLake catalog, where a SQLite catalog file and a Parquet data directory sit side by side. Node answers Airport gRPC on 50051 and an HTTP `/query` endpoint, with Arrow FlightSQL offered as an option for Grafana. Coordinator is the REST gateway in front of the user interface and for outside applications. DuckDB 1.5 does the querying, the path stays columnar from end to end, and execution waits until somebody asks a question.

## A first server needs one config file and two commands

Three routes into a running server are offered: a prebuilt `homer` package for your operating system and architecture, the official docker builds with compose examples under `examples/docker`, or a local build. The all-in-one profile switches on all four modules at once, and the ingest side is short enough to read in one screen.

```json
{
  "ingest": {
    "enable": true,
    "udp": { "enable": true, "port": 9060 },
    "tcp": { "enable": true, "port": 9061 },
    "http": { "enable": true, "port": 9080 }
  }
}
```

Storage points at the same DuckLake catalog, one SQLite file for metadata and one directory for Parquet.

```json
{
  "storage": {
    "enable": true,
    "ducklake": {
      "catalog_path": "/data/homer/homer_catalog.sqlite",
      "data_path": "/data/homer/parquet"
    }
  }
}
```

Building and starting takes two commands, and getting the config path right is the only part a first run has to think about.

```bash
# Build
make

# Run as server (default mode)
./homer --config-path /etc/homer/homer.json

# With debug logging
./homer --config-path /etc/homer/homer.json --log-level debug
```

`--log-level` accepts debug, info, warn, error and trace, `--syslog-disable` sends output to stdout only, and `--pid-file` defaults to `/var/run/homer-core.pid`. Once the process is up, `homer config show` prints the effective configuration after file, environment and defaults are merged, which is the quickest way to confirm which port a given key landed on.

Licence and activity, for the record: GitHub cannot classify the licence and shows it as Other, pointing at the LICENSE file in the repository root instead of a standard header, and the repository is not archived, with the last push on 2026-10-04.

## The diagram and the sample config disagree about port 50051

Two numbers in the sample configuration do not agree with the rest of the documentation. The architecture diagram reserves 50051 for Airport gRPC, hands FlightSQL 50055 and marks 32010 as an optional proxy, while the all-in-one config puts `"flight_server": { "port": 50051 }` under the node module. The search example pushes the confusion further by pointing at 10.0.0.1:8081, even though the same config file gives the coordinator an HTTP port of 8080.

Whether the node multiplexes both services on a single port, or the diagram simply predates the sample file, is not stated anywhere in the documentation. Both numbers therefore have to be reconciled by hand before anything talks to anything. For an operator that is the first half hour of any first deployment, and `homer config show` is where the answer actually lives.

## A bare make stops at the static C++ wrapper

The Makefile opens with a warning about its own first target. Without an explicit default goal, the rule for the static C++ wrapper directory becomes the default, so a plain make prepares libstdc++.a and stops without producing the binary or the user interface. That trap is invisible in the README, where the build section simply says make. Four variables decide what ends up inside the file you ship.

```makefile
.DEFAULT_GOAL := all
VERSION ?= $(shell grep 'VERSION_APPLICATION = ' src/version.go | head -1 | cut -d'"' -f2)
DUCKDB_VERSION ?= v1.5.5
GLIBC_TARGET ?= 2.34
```

`DUCKDB_VERSION` is pinned to v1.5.5 and has to match the DuckDB embedded in src/go.mod, which the comment ties to github.com/duckdb/duckdb-go-bindings v0.10505.x. Moving one side without the other leaves extension bundles that do not line up. `GLIBC_TARGET` only matters for the glibc-polyfill target, and the example given in the file uses 2.28, so the default build path never applies it at all.

Linkage gets separate treatment. Static LuaJIT is linked through `-Wl,-Bstatic -lluajit-5.1 -Wl,-Bdynamic` while everything else stays dynamic, and a `.static-cxx-link` directory holding only libstdc++.a is placed ahead of the compiler's normal search path so that the archive wins over the shared library. Build stamps come from the date and from git, and a source tree without a .git directory leaves the commit stamped as unknown instead of failing the build.

## The container build pins glibc, Node and the DuckDB bundle

The Dockerfile states a constraint rather than an instruction: DuckDB's static libraries expect glibc for backtrace, malloc_trim and the resolver, the Alpine and musl link fails, and the runtime has to match the libc compiled into the binary. Every base tag is pinned to a digest because of GHSA-9c8w-qvmp-pvjj, and the file tells builders how to refresh them.

```dockerfile
# Alpine/musl link fails. Runtime must match libc linked into the binary.
FROM devbase AS builder
RUN make modules && make all
```

The build stage is golang:1.27.1-bookworm, and Node is copied in from a separate node:22-bookworm-slim image because bookworm itself ships Node 18 while Vite 7 needs 20.19 or 22.12 and later. The comment is blunt about not curling a NodeSource installer in as root.

The final image is debian:bookworm-slim with ca-certificates, bash and sqlite3, a homer user at uid and gid 1000 whose home is /data/homer, and a .duckdb_spill directory created up front so DuckDB has somewhere to spill to when a query outgrows memory. A note at the end covers DuckDB's azure extension bundling a statically linked libcurl, and the file is cut off there, so what that default was overridden to is not stated.

## VQRTCP and SIPREC stay off until DuckLake writing is enabled

Two extra collectors sit behind flags that are off in the sample configuration, and both are said to require the writer module, which is the DuckLake side of the system. A reader who copies the snippet below without a writer configured gets listeners that have nowhere to write.

```json
{
  "ingest": {
    "vqrtcp": {
      "enable": true,
      "bind_ip": "0.0.0.0",
      "sip_port": 5063,
      "transports": ["udp", "tcp"],
      "methods": ["PUBLISH", "MESSAGE"],
      "reply_200": true
    }
  }
}
```

VQRTCP is a dedicated SIP listener on 5063 by default, and an SBC or media gateway is pointed at it to deliver `application/vq-rtcpxr` reports. Rows land in a table called vqrtcpxr_stats and appear under the transaction QoS tab.

SIPREC is the other one, an in-process recording server on 5062 that captures signalling metadata for RFC 7865 and RFC 7866, stores into hep_proto_1_siprec, and surfaces through a Protocol Search profile named `siprec`. That block needs advertise_ip set to an address recording clients can reach, and it carries a require_siprec switch whose behaviour the README does not explain beyond showing it in the sample.

## homer search covers the formats the interface cannot export

Every function in the built-in interface has a command line counterpart, and the search command is the one that matters during an incident. It queries through the coordinator API and can return table, vertical, CSV, JSON, chart, call flow or PCAP output.

```
homer                         Run the server (default)
homer search [flags]          Search Homer data via coordinator API
homer cli [flags]             Interactive DuckLake SQL shell
homer system [flags]          System operations (compaction, extensions, reload)
homer wizard [flags]          Interactive config generator wizard
homer config show [flags]     Print effective config (file + env + defaults)
homer mcp [flags]             Start MCP stdio server
homer version                 Show version
homer help                    Show full help with all flags
```

The PCAP output carries an explicit limit: SIP only, through `--format pcap` with an output path. No raw capture path is offered for the other protocols, so a team that needs packets from the media path keeps a separate capture tool for that job.

```bash
# Basic SIP search (last hour)
homer search --host 10.0.0.1:8081 --user admin --pass secret
```

That example passes the password on the command line, where it lands in shell history and in the process table for anyone on the box. `homer system` is where compaction, extension loading and reload live, `homer wizard` generates a config interactively, and `homer mcp` turns the same lake into an MCP stdio server.

## Grafana over Arrow FlightSQL is the second way into the data

There is a path that skips the coordinator entirely. The node module can expose Arrow FlightSQL for Grafana, documented on its own in docs/FLIGHTSQL.md, and the architecture diagram gives FlightSQL a separate port 50055 plus an optional proxy on 32010, which is how a Grafana datasource reaches DuckDB without sitting behind the REST gateway on 8080. For people who would rather write SQL than click through transactions, `homer cli` opens an interactive DuckLake SQL shell against the same lake.

The monolith is also splittable in practice. The repository ships example profiles for the pieces, one file per module: writer, node, coordinator, edge and modular, plus variants wired to RustFS, S3 and Azure storage, and two coordinator profiles for OAuth2 setups.

Query execution scaling linearly over a shared object storage catalog and pool is asserted without a single number attached, and the documentation says nothing about rollback, about moving data from an older Homer install into a DuckLake catalog, or about how far the split pays off before coordination overhead shows up. Those three gaps are what a pilot has to answer before anyone plans a split.

## Conclusion

Homer 11 fits teams whose capture agents already speak HEPv3 and who want searchable signalling history without maintaining a separate pcap workflow. It is the wrong choice for Alpine hosts, since the Dockerfile states the musl link fails against DuckDB's static libraries. Before deploying, reconcile port 50051 between the diagram and the sample config, turn on the DuckLake writer before enabling VQRTCP or SIPREC, and confirm your incident volume justifies the object storage catalog path, since no figure in the project is attached to that claim.

## FAQ

### What does Homer 11 do with captured SIP traffic?

HEP packets arrive over UDP, TCP, TLS, HTTP or HTTPS and are written into a DuckLake catalog backed by Parquet files. Node serves them over gRPC and an HTTP `/query` endpoint, and the coordinator exposes a REST gateway for the interface and external applications.

### Does Homer 11 build on Alpine Linux?

Not as the project builds it. The Dockerfile states that DuckDB's static libraries expect glibc, that the Alpine and musl link fails, and that the runtime must match the libc linked into the binary, so every image it publishes is Debian based.

### How do I search captured messages without opening the web interface?

Run `homer search` against the coordinator API, which returns table, vertical, CSV, JSON, chart, call flow or PCAP output. For direct SQL, `homer cli` opens an interactive DuckLake shell, and Arrow FlightSQL serves Grafana without the coordinator in between.

### What are VQRTCP and SIPREC in Homer 11?

VQRTCP is an optional SIP collector for `application/vq-rtcpxr` QoS reports, and SIPREC is an in-process recording server that captures RFC 7865 and RFC 7866 signalling metadata. Both are disabled by default and need the writer module enabled.

### Which ports does a default Homer 11 deployment listen on?

The sample config binds ingest to 9060 for UDP, 9061 for TCP and 9080 for HTTP, the coordinator to 8080, and the node flight server to 50051. The architecture diagram separately names 50055 for FlightSQL and 32010 for an optional proxy.

### Is the Homer 11 repository still maintained?

It is not archived, and the last push was on 2026-10-04, with three builds going out between 2026-09-29 and 2026-10-04. GitHub shows the licence as Other, a custom licence it cannot classify, with the terms in the LICENSE file at the repository root.

## Sources

- [Issues](https://github.com/sipcapture/homer/issues)
- [Project website](http://sipcapture.github.io/homer/)
- [README](https://github.com/sipcapture/homer/blob/homer11/README.md)
- [Releases](https://github.com/sipcapture/homer/releases)
- [sipcapture/homer on GitHub](https://github.com/sipcapture/homer)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/sipcapture-homer
