This guide shows you how to trace requests through NGINX with OpenTelemetry and send the spans to SigNoz. You load the native NGINX OpenTelemetry module (ngx_otel_module), point it at SigNoz, and NGINX creates a server span for every request it handles.
Prerequisites
- Linux on x86-64 or ARM64
- NGINX from the nginx.org packages or the official
nginxDocker image. The module package matches these builds. Ubuntu and Debian repositories do not ship it. - An instance of SigNoz (either Cloud or Self-Hosted)
Send traces to SigNoz
Step 1. Install the OpenTelemetry module
Set up the nginx.org package repository for your distribution with the official NGINX instructions. Then install NGINX and the module:
sudo apt update
sudo apt install nginx nginx-module-otelsudo yum install nginx nginx-module-otelThe package installs ngx_otel_module.so into /etc/nginx/modules/.
Step 2. Load the module
Add this line at the top of /etc/nginx/nginx.conf, outside every block:
load_module modules/ngx_otel_module.so;Step 3. Configure the exporter
Create /etc/nginx/conf.d/otel.conf. The default nginx.conf includes conf.d/*.conf inside its http block, so these directives apply to every server:
otel_exporter {
endpoint https://ingest.<region>.signoz.cloud:443;
header signoz-ingestion-key <your-ingestion-key>;
}
otel_service_name <service-name>;
otel_resource_attr service.version <service-version>;
otel_trace on;
otel_trace_context propagate;Verify these values:
<region>: Your SigNoz Cloud region<your-ingestion-key>: Your SigNoz ingestion key<service-name>: A descriptive name for this NGINX instance (for example,edge-nginx)<service-version>(optional): Your release version or git SHA (for example,1.4.2). Delete the line if you do not need it.
Two directives control tracing:
otel_trace oncreates a span for every request.otel_trace_context propagatecontinues an incoming W3Ctraceparentheader and passes the context to upstream servers. Instrumented backends then join the same trace.
Step 4. Restart NGINX
Check the configuration, then restart NGINX:
sudo nginx -t
sudo systemctl restart nginxnginx -t should print syntax is ok and test is successful. Use restart rather than reload: a fresh nginx.org install leaves the service stopped, and reload fails on a stopped service.
The official nginx image has -otel tags (for example, nginx:1.31-otel) that include the module. The image fills in environment variables in files under /etc/nginx/templates/ at startup, so you can pass the ingestion key at runtime.
Step 1. Create the exporter template
Create otel.conf.template next to your Dockerfile:
otel_exporter {
endpoint https://ingest.<region>.signoz.cloud:443;
header signoz-ingestion-key ${SIGNOZ_INGESTION_KEY};
}
otel_service_name <service-name>;
otel_trace on;
otel_trace_context propagate;Verify these values:
<region>: Your SigNoz Cloud region<service-name>: A descriptive name for this NGINX instance (for example,edge-nginx)
Keep ${SIGNOZ_INGESTION_KEY} as written. The container start script replaces it with the environment variable.
Step 2. Create the Dockerfile
FROM nginx:1.31-otel
# load_module must be the first line of nginx.conf
RUN sed -i '1i load_module modules/ngx_otel_module.so;' /etc/nginx/nginx.conf
COPY otel.conf.template /etc/nginx/templates/otel.conf.template
COPY default.conf /etc/nginx/conf.d/default.confReplace default.conf with your own server configuration. The image writes the rendered template to /etc/nginx/conf.d/otel.conf, which the default nginx.conf includes inside its http block.
Step 3. Build and run the container
docker build -t nginx-otel .
docker run -d -p 8080:80 -e SIGNOZ_INGESTION_KEY="<your-ingestion-key>" nginx-otelVerify these values:
<your-ingestion-key>: Your SigNoz ingestion key
This setup runs the official nginx:1.31-otel image and mounts its configuration from a ConfigMap. A Secret holds the ingestion key.
Step 1. Create a Secret for the ingestion key
kubectl create secret generic signoz-ingestion \
--from-literal=ingestion-key="<your-ingestion-key>"Verify these values:
<your-ingestion-key>: Your SigNoz ingestion key
Step 2. Create the ConfigMap
The ConfigMap holds two files. nginx.conf loads the module and holds your server block. otel.conf.template holds the exporter settings, and the image fills in the ingestion key at startup:
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-otel
data:
nginx.conf: |
load_module modules/ngx_otel_module.so;
worker_processes auto;
events {}
http {
include /etc/nginx/conf.d/otel.conf;
server {
listen 80;
location / {
proxy_pass http://<upstream-service>:<port>;
}
}
}
otel.conf.template: |
otel_exporter {
endpoint https://ingest.<region>.signoz.cloud:443;
header signoz-ingestion-key ${SIGNOZ_INGESTION_KEY};
}
otel_service_name <service-name>;
otel_trace on;
otel_trace_context propagate;Verify these values:
<region>: Your SigNoz Cloud region<service-name>: A descriptive name for this NGINX instance (for example,edge-nginx)<upstream-service>:<port>: The Kubernetes Service that NGINX proxies to. Replace theserverblock with your own configuration.
Step 3. Mount the ConfigMap in your Deployment
Add the environment variable and volume mounts to the NGINX container in your Deployment:
spec:
template:
spec:
containers:
- name: nginx
image: nginx:1.31-otel
env:
- name: SIGNOZ_INGESTION_KEY
valueFrom:
secretKeyRef:
name: signoz-ingestion
key: ingestion-key
volumeMounts:
- name: nginx-otel
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
- name: nginx-otel
mountPath: /etc/nginx/templates/otel.conf.template
subPath: otel.conf.template
volumes:
- name: nginx-otel
configMap:
name: nginx-otelStep 4. Apply the changes
kubectl apply -f nginx-otel-configmap.yaml
kubectl apply -f nginx-deployment.yaml
kubectl rollout status deployment/<deployment-name>Verify these values:
<deployment-name>: The Deployment that runs NGINX
Validate
- Send a few requests through NGINX, for example
curl http://localhost/. - Open SigNoz and navigate to Traces.
- Filter by the
service.nameyou set withotel_service_name.

- Click a span to see its attributes, such as
http.method,http.target, andhttp.status_code.

NGINX exports spans every 5 seconds, so wait a few seconds before you refresh.
Each span is named after the location block that matched the request, such as / or /api/orders. To set a different name, add otel_span_name inside a location block:
location /api/orders {
otel_span_name "GET /api/orders";
proxy_pass http://orders:8080;
}NGINX marks spans for 5xx responses as errors. Spans for 4xx responses keep an unset status.
Troubleshooting
NGINX writes exporter errors to its error log. On a VM, check /var/log/nginx/error.log. In Docker and Kubernetes, run docker logs <container> or kubectl logs deployment/<deployment-name>.
Why does nginx -t report unknown directive "otel_exporter"?
NGINX has not loaded the module. Add load_module modules/ngx_otel_module.so; as the first line of nginx.conf, outside every block, then run nginx -t again. The error can name any otel_* directive.
Why does the log show OTel export failure: Invalid or missing key?
SigNoz rejected the ingestion key. Check the header signoz-ingestion-key value, and confirm that the key belongs to the region in your endpoint. In Docker and Kubernetes, confirm that the SIGNOZ_INGESTION_KEY environment variable reaches the container.
Why does the log show OTel export failure: Stream removed?
The endpoint contains a path, such as /v1/traces. The module exports over OTLP/gRPC, which takes only a host and port. Set the endpoint to https://ingest.<region>.signoz.cloud:443.
Why does the log show OTel export failure: failed to connect to all addresses?
The endpoint lacks the https:// prefix, so the module opens an unencrypted connection to a TLS port. Write the endpoint as https://ingest.<region>.signoz.cloud:443. For a Collector on the same host, check that it listens on port 4317.
Why does the service show as unknown_service:nginx?
The configuration has no otel_service_name directive. Add it to the http context, next to otel_exporter.
Why is the NGINX span in a separate trace from the backend?
By default, NGINX ignores trace headers. It starts a new trace for its own span and forwards the original traceparent header unchanged, so the backend joins the caller's trace instead. Add otel_trace_context propagate; so NGINX continues the incoming trace and passes its own span context upstream.
For more help with missing data, see Debug missing traces, logs, and metrics in SigNoz.
Setup OpenTelemetry Collector (Optional)
What is the OpenTelemetry Collector?
The Collector sits between NGINX and SigNoz. NGINX sends spans to the Collector, and the Collector forwards them to SigNoz.
Why use it?
- Cleaning up data: Filter out noisy spans, or remove sensitive attributes before they leave your servers.
- Adding context: Tag spans with host, Kubernetes, or cloud metadata.
- One agent for all signals: The same Collector can also scrape NGINX metrics and tail NGINX logs.
To send spans through a Collector, point the exporter at the Collector's OTLP/gRPC receiver and remove the header line, because the Collector holds the ingestion key:
otel_exporter {
endpoint http://localhost:4317;
}See Switch from direct export to Collector to set up the Collector, and the Collector configuration guide for its options.
Alternative: OpenTelemetry webserver module (Optional)
The OpenTelemetry project maintains a separate webserver module for NGINX. Besides the request span, it creates a span for each NGINX module that handles the request, such as ngx_http_rewrite_module. Use it only if you need those per-module spans.
This Dockerfile installs the webserver module on NGINX 1.26.0:
FROM nginx:1.26.0
RUN apt-get update && apt-get install -y curl \
&& curl -fsSL https://github.com/open-telemetry/opentelemetry-cpp-contrib/releases/download/webserver%2Fv1.1.0/opentelemetry-webserver-sdk-x64-linux.tgz \
| tar xz -C /opt \
&& cd /opt/opentelemetry-webserver-sdk && ./install.sh
ENV LD_LIBRARY_PATH=/opt/opentelemetry-webserver-sdk/sdk_lib/lib
RUN sed -i '1i load_module /opt/opentelemetry-webserver-sdk/WebServerModule/Nginx/1.26.0/ngx_http_opentelemetry_module.so;' /etc/nginx/nginx.conf
COPY opentelemetry_module.conf.template /etc/nginx/templates/opentelemetry_module.conf.templateCreate opentelemetry_module.conf.template next to the Dockerfile:
NginxModuleEnabled ON;
NginxModuleOtelSpanExporter otlp;
NginxModuleOtelExporterEndpoint https://ingest.<region>.signoz.cloud:443;
NginxModuleOtelSslEnabled ON;
NginxModuleOtelSslCertificatePath /etc/ssl/certs/ca-certificates.crt;
NginxModuleOtelExporterOtlpHeaders signoz-ingestion-key=${SIGNOZ_INGESTION_KEY};
NginxModuleServiceName <service-name>;
NginxModuleServiceNamespace <service-namespace>;
NginxModuleServiceInstanceId <service-instance-id>;
NginxModuleResolveBackends ON;
NginxModuleTraceAsError ON;Verify these values:
<region>: Your SigNoz Cloud region<service-name>: A descriptive name for this NGINX instance (for example,edge-nginx)<service-namespace>: A namespace for the service (for example,platform)<service-instance-id>: A unique ID for this instance (for example,nginx-1)
Build and run it as shown in the Docker tab, passing SIGNOZ_INGESTION_KEY with -e.
Send NGINX metrics and logs
The OpenTelemetry module sends traces only. Use the OpenTelemetry Collector for the other signals:
- Metrics: Enable the NGINX
stub_statusendpoint and scrape it with the Collector'snginxreceiver. See Monitor NGINX metrics. - Logs: Tail the NGINX access and error logs with the Collector and parse them into structured fields. See NGINX logs.
Next steps
- Create trace-based alerts on NGINX error rate or latency
- Import the NGINX dashboard after you send metrics
- Configure sampling and span attributes with the NGINX ngx_otel_module reference
Get Help
If you need help with the steps in this topic, please reach out to us on SigNoz Community Slack. If you are a SigNoz Cloud user, please use in product chat support located at the bottom right corner of your SigNoz instance or contact us at cloud-support@signoz.io.