Why Self-Host SigNoz Instead of Just Paying Datadog
Datadog is great. It’s also priced like you’re running a FAANG data center. If you’ve ever gotten a bill mid-month and done a double-take, you already know why we’re here.
SigNoz is a full-stack observability platform (traces, metrics, logs, dashboards, alerts) built on OpenTelemetry and ClickHouse. It’s open source, it runs on your hardware, and it doesn’t charge you per seat or per GB of logs like it’s printing money.
The install story changed recently, and if you’re following an older guide (including the first version of this one), you’re going to have a bad time. SigNoz now installs through Foundry, a CLI that generates your deployment from a single YAML file. The guide below is short: install foundryctl, write about seven lines of YAML, run one command, get telemetry.
If you’re still deciding whether SigNoz is the right tool, check out SigNoz vs Uptrace or SigNoz vs Grafana LGTM first. If you know you want SigNoz and you just want the thing running, keep reading.
Full example: Clone the working files at github.com/KingPin/sumguy-examples/observability/self-host-signoz
Also worth reading alongside this guide: OpenTelemetry for Self-Hosters and ClickHouse for Self-Hosted Observability for deeper context on the underlying pieces.
What Changed: install.sh and the Bundled Compose Files Are Dead
As of SigNoz v0.130.0, the install.sh script and the Docker Compose manifests that used to live in deploy/ are deprecated and no longer distributed. If you go looking for signoz/deploy/docker/docker-compose.yaml in the repo today, you’ll find a README pointing you at Foundry and a migration guide. That’s it.
SigNoz’s reasoning is the boring, correct kind: they were maintaining a separate install surface for every platform they supported (Compose, Swarm, Kubernetes manifests, Helm, ECS, and a pile of one-click PaaS targets), and every application release meant updating all of them without getting any wrong. Foundry collapses that into one code path. You declare a target, it generates the manifests.
The vocabulary is aggressively themed, so let’s get it out of the way:
| Foundry term | What it actually is |
|---|---|
| Casting | Your casting.yaml, the one config file that describes the deployment |
| Moldings | The components (ClickHouse, Postgres, OTel Collector, SigNoz itself) |
| Pours | The generated output files, written to pours/ |
| gauge | Preflight check that the tools you need are present |
| forge | Generate the manifests, touch nothing running |
| cast | gauge, then forge, then actually deploy |
If you’ve used Terraform, this will feel familiar. forge is your plan, cast is your apply, and the generated files in pours/ are disposable artifacts you regenerate rather than hand-edit.
The architecture also shifted underneath the install change. The old four-container setup (ClickHouse, collector, SigNoz, and a standalone Alertmanager) is now five moving parts, two of which are new and one of which is gone:
- telemetrystore: ClickHouse, still where traces, metrics, and logs live
- telemetrykeeper: ClickHouse Keeper (or ZooKeeper) for cluster coordination
- metastore: PostgreSQL, storing dashboards, alerts, and users
- ingester: the SigNoz OTel Collector distribution, listening on 4317/4318
- signoz: the UI and API server
The metastore is the one that trips people up on upgrade. Older SigNoz kept metadata in SQLite inside the query service; Postgres is now the default. There’s also a one-shot telemetrystore-migrator container that runs ClickHouse schema migrations and exits, so seeing it in Exited (0) state is correct behaviour, not a crash.
Oh, and the UI moved. It’s on port 8080 now, not 3301.
Prerequisites
Before you spin anything up, make sure your host can handle it. SigNoz is not a lightweight install; ClickHouse is doing real work behind the scenes.
Minimum sizing:
- 4 CPU cores (ClickHouse is happier with 6+)
- 4 GB RAM is SigNoz’s stated floor. 8 GB is the honest homelab number, 16 GB if you’re ingesting more than a few services
- 50 to 100 GB of disk for a few weeks of retention at moderate volume, SSDs preferred
- Linux or macOS. Docker Engine 20.10+ with the Compose v2 plugin
- Ports free:
8080(UI),4317and4318(OTLP), plus8000if you enable the MCP server
Check your versions:
docker --versiondocker compose versionIf docker compose version fails, you’re on the old standalone binary. On Ubuntu:
sudo apt install docker-compose-pluginOn Windows, run this inside WSL 2 with Docker Engine installed natively in the distro, not Docker Desktop. ClickHouse Keeper segfaults (exit code 139, restart loop) under Docker Desktop’s virtualization layer on Windows. SigNoz documents this one themselves.
Step 1: Install foundryctl
One line, and it does the sensible things: detects your OS and architecture, verifies the download checksum, and drops the binary in $XDG_BIN_HOME or ~/.local/bin.
curl -fsSL https://signoz.io/foundry.sh | bashIf piping a remote script into bash makes you twitch (fair), the releases page has plain tarballs. Unpack, move foundry_*/bin/foundryctl onto your PATH, done.
Pin a version if you want reproducible installs across machines: curl -fsSL https://signoz.io/foundry.sh | FOUNDRY_VERSION=v0.2.17 bash.
Confirm it works with foundryctl --help. If that comes back “command not found”, ~/.local/bin isn’t on your PATH. Add export PATH="$HOME/.local/bin:$PATH" to your shell config and reload.
Step 2: Write Your casting.yaml
This is what replaces the 200-line Compose file the old version of this guide handed you:
mkdir -p ~/signoz && cd ~/signozapiVersion: v1alpha1kind: Installationmetadata: name: signozspec: deployment: flavor: compose mode: dockerSeven lines. That’s a complete, working, single-node SigNoz deployment with defaults for every component.
mode and flavor are the pair that picks your target, and they only work in specific combinations:
| Target | mode | flavor | platform |
|---|---|---|---|
| Docker Compose | docker | compose | - |
| Docker Swarm | docker | swarm | - |
| Systemd (binary) | systemd | binary | - |
| Kubernetes | kubernetes | kustomize or helm | - |
| AWS ECS on EC2 | ec2 | terraform | ecs |
Render, Railway, and Coolify are also supported through platform with their own flavors. You can’t mix values across rows: mode: docker with flavor: helm isn’t a thing, and Foundry says so rather than generating nonsense.
Skip the memorisation. foundryctl gen examples writes working example castings for every supported mode, so copy the one you want.
Step 3: Cast It
foundryctl cast -f casting.yamlThat runs all three stages: gauge checks your Docker install, forge renders the Compose files into pours/deployment/ and writes a casting.yaml.lock, then cast brings the stack up.
If you’re the sort who reads the diff before applying it (you should be), split it:
foundryctl gauge -f casting.yaml # validate prerequisitesfoundryctl forge -f casting.yaml # generate files, change nothing runningcd pours/deployment && docker compose up -dThe generated tree looks like this:
pours/deployment/ compose.yaml ingester/ ingester.yaml opamp.yaml telemetrykeeper/ clickhousekeeper/ keeper-0.yaml telemetrystore/ clickhouse/ config-0-0.yaml functions.yamlDo not hand-edit anything in pours/. The next forge overwrites it. Everything you want to change belongs in casting.yaml, which is the whole point of the exercise and also the thing everyone gets wrong the first time.
Check what came up:
docker psYou should see signoz-signoz-0 on 0.0.0.0:8080, signoz-ingester-1 on 4317/4318, signoz-telemetrystore-clickhouse-0-0, signoz-telemetrykeeper-clickhousekeeper-0, and signoz-metastore-postgres-0. The signoz-telemetrystore-migrator and signoz-telemetrystore-clickhouse-user-scripts containers run once and exit. That’s correct.
Give it 60 to 90 seconds on first boot. ClickHouse builds its schema, the migrator runs, and the other services will restart-loop until dependencies report healthy. Normal, not broken.
Step 4: First Login
Open http://your-host-ip:8080.
You’ll land on the setup wizard asking you to create an admin account. Fill in a name, email, and password. This is local auth stored in the Postgres metastore; nothing phones home.
After login you’ll see the main dashboard. It’s empty. That’s expected, you haven’t sent it any data yet.
Confirm the ingester is actually listening before you go instrumenting things:
curl -v http://localhost:4318/v1/traces# Expect 400 or 405. The endpoint exists and rejected your empty GET. Good.Step 5: Wire Up a Real App via OTLP
This part didn’t change, which is the nice thing about OpenTelemetry being a standard rather than a vendor feature. Your apps point at localhost:4317 (gRPC) or localhost:4318 (HTTP) and don’t care what’s behind them.
Python / Flask
OpenTelemetry has an auto-instrumentation path for Python that covers Flask, SQLAlchemy, requests, and a pile of other libraries without you touching application code.
pip install flask \ opentelemetry-distro \ opentelemetry-exporter-otlp-proto-grpc
opentelemetry-bootstrap -a installfrom flask import Flask, jsonifyimport time, random
app = Flask(__name__)
@app.route("/")def index(): # Simulate variable latency so you have something interesting to alert on time.sleep(random.uniform(0.01, 0.5)) return jsonify({"status": "ok"})
@app.route("/slow")def slow(): time.sleep(random.uniform(0.8, 2.5)) return jsonify({"status": "done, eventually"})
@app.route("/error")def error(): raise ValueError("This error is intentional. Probably.")
if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=False)Run it with auto-instrumentation pointing at SigNoz:
export OTEL_RESOURCE_ATTRIBUTES="service.name=flask-demo"export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"export OTEL_EXPORTER_OTLP_PROTOCOL="grpc"
opentelemetry-instrument python app.pyHit the endpoints a few times to generate traces:
curl http://localhost:5000/curl http://localhost:5000/slowcurl http://localhost:5000/error || trueIn 15 to 30 seconds flask-demo should appear in the SigNoz Services list.
Node.js
Install deps in your project:
npm install @opentelemetry/sdk-node \ @opentelemetry/auto-instrumentations-node \ @opentelemetry/exporter-trace-otlp-grpc \ @opentelemetry/exporter-metrics-otlp-grpcCreate a tracing bootstrap file. This must be required before anything else:
const { NodeSDK } = require("@opentelemetry/sdk-node");const { getNodeAutoInstrumentations } = require("@opentelemetry/auto-instrumentations-node");const { OTLPTraceExporter } = require("@opentelemetry/exporter-trace-otlp-grpc");const { OTLPMetricExporter } = require("@opentelemetry/exporter-metrics-otlp-grpc");const { PeriodicExportingMetricReader } = require("@opentelemetry/sdk-metrics");
const sdk = new NodeSDK({ serviceName: "node-demo", traceExporter: new OTLPTraceExporter({ url: "grpc://localhost:4317", }), metricReader: new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter({ url: "grpc://localhost:4317", }), exportIntervalMillis: 15000, }), instrumentations: [getNodeAutoInstrumentations()],});
sdk.start();
process.on("SIGTERM", () => { sdk.shutdown().then(() => process.exit(0));});Start your app with the tracing file loaded first:
node -r ./tracing.js server.jsBoth services will show up in SigNoz within a minute of generating traffic. Traces, spans, service maps, latency distributions, the whole deal.
Step 6: Retention and TTL
Foundry ships sane defaults now, which is a change worth knowing about: 7 days for logs and traces, 30 days for metrics. The old “keep everything until the disk fills” behaviour is gone, so if you were expecting three weeks of trace history, adjust before you need it rather than after.
In the SigNoz UI:
- Go to Settings → General
- Set your TTL for Traces, Metrics, and Logs independently
- Save
SigNoz translates that into ALTER TABLE ... MODIFY TTL statements against ClickHouse.
The caveat about ClickHouse TTL: data doesn’t vanish the instant it expires. ClickHouse deletes expired rows during background merges, which run on their own schedule. In practice data can stick around 1 to 3 days past your TTL. It’s a storage management tool, not a compliance control. Don’t use it as one.
To see what’s actually eating disk:
docker exec -it signoz-telemetrystore-clickhouse-0-0 \ clickhouse-client --query " SELECT database, table, formatReadableSize(sum(bytes_on_disk)) AS size FROM system.parts WHERE active = 1 GROUP BY database, table ORDER BY sum(bytes_on_disk) DESC LIMIT 20 "Note there’s no --user or --password here. Foundry’s generated ClickHouse runs with CLICKHOUSE_SKIP_USER_SETUP=1 and is reachable only on the internal signoz-network, so the default user has no password. That’s fine as long as you don’t publish port 9000 to the host, and Foundry doesn’t.
If a table from that query is the obvious culprit and you want the space back now, force the merge:
docker exec -it signoz-telemetrystore-clickhouse-0-0 \ clickhouse-client --query "OPTIMIZE TABLE <database>.<table> FINAL"For a homelab, 14 days of traces and 30 days of metrics is a reasonable starting point. Scale up once you know your real volume.
Step 7: Set Up an Alert
An observability platform without alerts is a dashboard you never look at.
Create a metrics-based alert:
- Go to Alerts → New Alert Rule
- Choose Metrics Based Alert
- Pick a metric.
signoz_calls_totalfiltered on 5xx status codes is an easy first one - Set the condition: sum > 5 over 5 minutes
- Name it something your 2 AM self will understand, like “High error rate: flask-demo”
- Severity Warning
Add a notification channel:
- Settings → Alert Channels → New Channel
- Pick Slack, webhook, PagerDuty, or email
- For Slack, paste the incoming webhook URL
- Send the test notification. If it doesn’t arrive, fix that before you trust the rule
- Assign the channel to your alert rule
One thing that changed with Foundry: there is no separate Alertmanager container any more. Alert evaluation and routing live inside the signoz service itself. If you’re following an old guide that tells you to edit alertmanager.yml and docker compose restart alertmanager, that container doesn’t exist in your stack. Configure it in the UI instead.
Step 8: Customising Without Fighting Foundry
Everything below follows the same loop: edit casting.yaml, re-run foundryctl cast.
Pinning versions. ClickHouse and the Keeper come out of Foundry already pinned, but signoz and ingester default to latest, which is a fine way to get surprised on a Tuesday. Pin them:
spec: deployment: flavor: compose mode: docker signoz: spec: image: signoz/signoz:v0.137.1 ingester: spec: image: signoz/signoz-otel-collector:v0.137.1 telemetrystore: spec: image: clickhouse/clickhouse-server:25.12.5Swapping the metastore. Postgres is the default and the right call for anything you care about. Set metastore.kind: sqlite if you want the old single-node behaviour with one less container.
Resource limits and anything else Foundry doesn’t model. Foundry splits config into two layers: spec.<component> is the application domain (images, replicas, env, config files), and spec.patches is the platform domain, applied as raw JSON Patch (RFC 6902) against a generated file. Capping ClickHouse memory looks like this:
spec: patches: - target: "deployment/compose.yaml" operations: - op: add path: /services/signoz-telemetrystore-clickhouse-0-0/mem_limit value: "6g"Run foundryctl forge once with no patches first so you can read the generated compose.yaml and get the JSON pointer paths right. Guessing them is how you spend an evening debugging a patch that silently targeted nothing.
Upgrading. Bump the image tags in casting.yaml and cast again. One gotcha worth burning into memory: Compose does not restart a container when only the contents of a mounted config file change, so a plain cast can leave you running new configs that nothing has read. Force it:
docker compose -f pours/deployment/compose.yaml up -d --force-recreateRecreate everything in one command rather than restarting services individually. Restarting only the keeper leaves ClickHouse unable to reconnect until it’s restarted too. Your data is in volumes and isn’t affected.
Migrating an Existing Compose Install
If you already have SigNoz running from install.sh or the old deploy/ Compose files, you can move to Foundry without losing data. The short version:
- Keep a copy of your existing
docker-compose.yamland any config it references. SigNoz no longer distributes these, so that copy is your only rollback path. - Install
foundryctl. - Write a casting that reproduces the legacy layout (
metastore: sqlite,telemetrykeeper: zookeeper) and usespec.patchesto rename the generated volumes onto your existing ones (signoz-clickhouse,signoz-zookeeper-1,signoz-sqlite). foundryctl forgeand read the output before you touch anything running.docker compose downon the old stack. Never pass-vor--volumes, that deletes the data you’re trying to keep.foundryctl cast.
You’ll also need to carry over your ClickHouse replica and shard macros from the old config.xml into the new casting, and the ClickHouse config moved from XML to YAML in the process. SigNoz’s migration guide has the full castings for both Compose and Swarm, including the user: root patch ZooKeeper needs to read files the legacy stack created. Follow that one rather than improvising; this is the one part of the process where getting creative costs you your trace history.
Reverse Proxy and Backups
Reverse Proxy
Port 8080 on the LAN is fine for internal use. For HTTPS and a real hostname, put Caddy in front:
signoz.your-domain.com { reverse_proxy localhost:8080}Caddy handles the cert. For Traefik, add labels via a patch on the signoz-signoz-0 service rather than editing the generated Compose file. SigNoz itself needs no special configuration either way.
Backups
Your data lives in Docker volumes, and Foundry names them predictably:
signoz-telemetrystore-0-0-data: ClickHouse, all your telemetrysignoz-metastore-postgres-0-data: dashboards, alert rules, userssignoz-telemetrykeeper-0-data: keeper state
The metastore is small and matters more than people expect. Losing it means rebuilding every dashboard and alert you’ve ever made:
docker exec signoz-metastore-postgres-0 \ pg_dump -U signoz signoz | gzip > signoz-metastore-$(date +%F).sql.gzFor ClickHouse, a cold volume copy with the stack stopped is the simplest thing that actually works:
docker compose -f pours/deployment/compose.yaml downdocker run --rm \ -v signoz-telemetrystore-0-0-data:/data:ro \ -v "$PWD/backups:/backup" \ alpine tar czf /backup/clickhouse-$(date +%F).tar.gz -C /data .docker compose -f pours/deployment/compose.yaml up -dWhere downtime isn’t acceptable, use clickhouse-backup (the third-party tool) for incremental backups and S3 upload. For a homelab, a weekly snapshot to the NAS plus the Postgres dump covers you.
Back up casting.yaml too. It’s seven lines and it rebuilds your entire deployment, which is the actual selling point here. Put it in git.
Troubleshooting
No data showing up
Check the ingester is reachable from your app host:
curl -v http://signoz-host:4318/v1/tracesnc -zv signoz-host 4317If the connection is refused, check the ports are bound:
docker compose -f pours/deployment/compose.yaml ps# Look for 0.0.0.0:4317-4318->4317-4318/tcp on the ingesterCheck ingester logs for export errors:
docker compose -f pours/deployment/compose.yaml logs ingester --tail=100 2>&1 | grep -i errorThe usual culprit is connection refused to ClickHouse while it’s still coming up.
Ports vanish when you scale the ingester
If you set ingester.spec.cluster.replicas above 1 under Compose, the OTLP ports stop being published to the host entirely. Compose can’t publish a fixed host port for a scaled service, so 4317 and 4318 become reachable only on the internal network. Same applies to the MCP server on 8000. Either run one ingester replica, put your own proxy in front, or move to the swarm or kubernetes casting where replicas get load-balanced properly.
A container is unhealthy or the UI won’t load
docker compose -f pours/deployment/compose.yaml logs -f signoz-signoz-0If signoz-signoz-0 is healthy but the page won’t load, confirm you’re on 8080 and not 3301 out of muscle memory. Everything published before mid-2026 says 3301.
Containers keep restarting
Almost always memory. SigNoz wants 4 GB allocated to Docker as a floor and is happier with 8. On Docker Desktop, raise the VM memory allocation. On Linux, check dmesg | grep -i oom for the OOM killer’s handiwork.
If ClickHouse specifically is the one dying, cap it explicitly with a patch (see the customisation section above) so it gets rejected queries rather than a hard kill.
The migrator container is “Exited”
That’s the design. signoz-telemetrystore-migrator runs the ClickHouse schema migrations once and stops. Exit code 0 means it worked. Exit code anything-else means read its logs before debugging anything downstream, because nothing above it will work correctly.
The SumGuy Take
SigNoz is still good software: OpenTelemetry-native so your instrumentation isn’t vendor-locked, a UI that’s usable without a training course, and ClickHouse underneath meaning real compression and fast queries on mid-range hardware.
Foundry is a clear improvement over what it replaced, and I say that as someone whose previous advice here was “skip their deploy script and hand-roll your own Compose file.” That made sense when the alternative was cloning a mono-repo to get one YAML file. It doesn’t now. A seven-line declarative config that generates a correct five-component stack, pins its own ClickHouse version, and works the same across Compose, systemd, and Kubernetes beats a Compose file I maintain by hand and get wrong the moment SigNoz adds a Postgres metastore I didn’t know about.
The cost is a layer of indirection and a themed vocabulary. Casting, molding, forge, pour. It’s a lot of metalworking for docker compose up. But the generated files are plain Compose YAML sitting in pours/ where you can read them, and you can stop at forge and run Compose yourself. That’s the right escape hatch to have.
The hardware reality hasn’t changed. ClickHouse is not small, 8 GB is the honest minimum, and running this alongside your app stack will hurt. On a dedicated VM it’s fine.
If you want to understand what your services are actually doing without signing up for a Datadog trial, this is a good afternoon’s work. Just put casting.yaml in git, because it’s now the only file that matters.