SigNoz is an open source application performance monitoring (APM) platform built on OpenTelemetry. It stores traces, metrics and logs in ClickHouse and shows them in one interface, so you can go from a slow endpoint to the exact span and log line that caused it. In this tutorial you will deploy SigNoz with Docker Compose on Ubuntu 24.04, instrument a small Python web app with OpenTelemetry auto-instrumentation, explore its traces and logs, and publish the SigNoz UI over HTTPS behind Nginx.

Prerequisites

To follow this tutorial you need:

  • A server running Ubuntu 24.04 LTS with at least 4 vCPUs, 8 GB of RAM and 40 GB of free disk, for example a CubePath VPS. ClickHouse is the heaviest component and grows with the amount of telemetry you keep.
  • A non-root user with sudo privileges and UFW enabled with SSH allowed.
  • Docker Engine and the Docker Compose plugin installed from Docker's official repository, plus git.
  • A domain name with a DNS A record for signoz.your_domain pointing to your_server_ip.

Step 1 - Downloading the SigNoz deployment files

SigNoz publishes its Docker Compose deployment in the main repository. Clone it into /opt:

sudo mkdir -p /opt/signoz
sudo chown "$USER": /opt/signoz
git clone -b main https://github.com/SigNoz/signoz.git /opt/signoz
cd /opt/signoz/deploy/docker

List the services the Compose file defines:

docker compose config --services

The output includes ClickHouse, ZooKeeper, one or more schema migration jobs, signoz and otel-collector; the exact list changes between releases. The two you interact with are signoz (the query service and web UI on port 8080) and otel-collector (which receives OpenTelemetry data on port 4317 for gRPC and 4318 for HTTP).

Step 2 - Keeping the UI off the public interface

By default the Compose file publishes port 8080 on all interfaces. Docker writes its own iptables rules for published ports and bypasses UFW, so a ufw deny rule would not protect it. Instead, override the mapping so the UI only listens on localhost; Nginx will expose it with TLS in Step 6.

Create an override file next to the main Compose file. Compose loads it automatically, and keeping your change there means git pull never conflicts with it:

nano /opt/signoz/deploy/docker/docker-compose.override.yaml
services:
  signoz:
    ports: !override
      - "127.0.0.1:8080:8080"

The !override tag replaces the port list instead of merging it with the original one. If docker compose config --services showed a different name for the UI service in your release, use that name here.

Check the result:

docker compose config | grep -B2 -A1 'published: "8080"'
        host_ip: 127.0.0.1
        target: 8080
        published: "8080"
        protocol: tcp

The collector ports 4317 and 4318 stay public because your applications need to reach them. Restrict them to your application servers with the CubePath firewall in your panel, or apply the same override pattern to otel-collector if all your apps run on this server.

Step 3 - Starting SigNoz

Start the stack:

docker compose up -d --remove-orphans

The first start pulls several images and runs the schema migrations in ClickHouse, which takes a few minutes. Watch the state until signoz, clickhouse and otel-collector are running and healthy:

docker compose ps

Then check the query service health endpoint:

curl -s http://127.0.0.1:8080/api/v1/health
{"status":"ok"}

Confirm that the collector accepts OTLP over HTTP. An empty JSON payload is a valid request and returns HTTP 200:

curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:4318/v1/traces \
  -H 'Content-Type: application/json' -d '{}'
200

If a container keeps restarting, docker compose logs --tail=100 clickhouse is the first place to look; it is usually memory pressure on servers below 8 GB.

Step 4 - Instrumenting a Python application

To see real data, run a small Flask app with OpenTelemetry's zero-code instrumentation. It patches Flask, requests, database drivers and the logging module at startup, so you do not change application code.

Ubuntu 24.04 does not allow pip to install into the system Python, so create a virtual environment:

sudo apt install python3-venv
mkdir -p ~/otel-demo
cd ~/otel-demo
python3 -m venv venv
source venv/bin/activate

Install Flask, the OpenTelemetry distribution and the OTLP exporter, then let opentelemetry-bootstrap add the instrumentation packages that match the libraries it finds:

pip install flask opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install

Create the application:

nano ~/otel-demo/app.py
import logging
import random
import time

from flask import Flask

app = Flask(__name__)
log = logging.getLogger(__name__)


@app.route("/")
def index():
    delay = random.uniform(0.01, 0.3)
    time.sleep(delay)
    log.warning("served index in %.3f s", delay)
    return "ok\n"


@app.route("/error")
def error():
    raise RuntimeError("demo failure")

Run it through opentelemetry-instrument. The environment variables name the service and point all three signals at the SigNoz collector over gRPC:

OTEL_SERVICE_NAME=otel-demo \
OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4317 \
OTEL_EXPORTER_OTLP_PROTOCOL=grpc \
OTEL_EXPORTER_OTLP_INSECURE=true \
OTEL_LOGS_EXPORTER=otlp \
OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true \
opentelemetry-instrument flask --app app run --port 5000

On an application running on another server, replace 127.0.0.1 with your_server_ip and make sure the firewall allows that server to reach port 4317.

In a second SSH session, generate some traffic, including one failing request:

for i in $(seq 1 50); do curl -s http://127.0.0.1:5000/ > /dev/null; done
curl -s http://127.0.0.1:5000/error > /dev/null

The Python SDK batches data and exports it every few seconds, so allow about 30 seconds before checking the UI.

Step 5 - Exploring traces, metrics and logs

Until Nginx is in place, reach the UI through an SSH tunnel from your workstation:

ssh -L 8080:127.0.0.1:8080 your_user@your_server_ip

Browse to http://localhost:8080. The first visit asks you to create the admin account and organization; use a strong password, since this account controls all telemetry.

After signing in:

  • Services lists otel-demo with its request rate, error rate and P99 latency, all derived from the spans. Open it to see per-endpoint latency and the /error route with a 100% error rate.
  • Traces lets you filter spans by service, operation, status and duration. Open a trace for GET /error to see the span marked as an error with the RuntimeError exception and stack trace attached as an event.
  • Logs shows the served index in ... s lines. Each record carries the trace_id of the request that produced it, so from a log line you can jump straight to the trace, and the other way around.

To be notified when things break, go to Alerts > New Alert, pick the signal (for example, the error rate of otel-demo, or logs matching severity_text = ERROR), set a threshold and evaluation window, and attach a notification channel created under Settings > Notification Channels (Slack, PagerDuty, Opsgenie, Microsoft Teams, email or a generic webhook).

Step 6 - Serving the UI over HTTPS with Nginx

Install Nginx and Certbot:

sudo apt update
sudo apt install nginx certbot python3-certbot-nginx

Create a server block:

sudo nano /etc/nginx/sites-available/signoz
server {
    listen 80;
    listen [::]:80;
    server_name signoz.your_domain;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 300s;
    }
}

The Upgrade headers let the live log tail work through the proxy, and the longer read timeout gives heavy queries time to finish. Enable the site and reload Nginx:

sudo ln -s /etc/nginx/sites-available/signoz /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Open HTTP and HTTPS in UFW and request a certificate. Certbot adds the TLS settings and an HTTP to HTTPS redirect:

sudo ufw allow 'Nginx Full'
sudo certbot --nginx -d signoz.your_domain

Browse to https://signoz.your_domain and sign in with the account you created.

Step 7 - Controlling disk usage

Telemetry grows quickly. SigNoz sets retention per signal under Settings > General: lower the trace and log retention to what you actually need (for example, 7 days for traces and logs, 30 days for metrics) before the disk fills. Check how much space the Docker volumes use:

docker system df -v | grep -i -E 'clickhouse|signoz'

On busy applications, reduce trace volume at the source with sampling, for example by adding OTEL_TRACES_SAMPLER=parentbased_traceidratio and OTEL_TRACES_SAMPLER_ARG=0.2 to keep 20% of traces.

Troubleshooting

The service never appears in SigNoz. Run the app with OTEL_TRACES_EXPORTER=console added to confirm spans are produced at all. If they are, test the network path from the app host with nc -zv your_server_ip 4317 and read docker compose logs --tail=100 otel-collector for rejected data.

Logs are missing while traces arrive. Both OTEL_LOGS_EXPORTER=otlp and OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true must be set, and only records at or above the logger's level are exported (WARNING by default in Python).

ClickHouse restarts or queries are very slow. Check memory with docker stats --no-stream. The fix is more RAM or less data: shorter retention and trace sampling.

Conclusion

You deployed SigNoz with Docker Compose, kept its UI behind Nginx with HTTPS, and sent traces, metrics and correlated logs from a Python service using OpenTelemetry without code changes. Because the instrumentation is plain OpenTelemetry, the same environment variables work with other backends. Next, instrument your other services with the OpenTelemetry SDKs for Node.js, Go or Java, point them at the collector on port 4317, and build dashboards and alerts for your key endpoints. To upgrade later, run git pull in /opt/signoz and docker compose up -d --remove-orphans from deploy/docker.