Set up Jaeger distributed tracing behind an NGINX reverse proxy with SSL termination and authentication. Learn to configure secure access, performance optimization, and production-ready monitoring for your microservices.
Prerequisites
- Root or sudo access
- Domain name with DNS configured
- Docker and Docker Compose
- Basic understanding of NGINX configuration
What this solves
Jaeger provides distributed tracing for microservices, but exposing it directly to the internet creates security risks. This tutorial shows you how to configure NGINX as a reverse proxy for Jaeger with SSL termination, authentication, and security headers for production use.
Step-by-step configuration
Update system packages
Start by updating your package manager to ensure you get the latest versions of all components.
sudo apt update && sudo apt upgrade -y
Install Docker and Docker Compose
Jaeger runs best in containers for easy management and updates. Install Docker and Docker Compose for container orchestration.
sudo apt install -y docker.io docker-compose-v2
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
Install NGINX
Install NGINX to act as the reverse proxy for Jaeger. NGINX will handle SSL termination and authentication.
sudo apt install -y nginx apache2-utils
Install Certbot for Let's Encrypt
Certbot automatically obtains and renews SSL certificates from Let's Encrypt for secure HTTPS connections.
sudo apt install -y certbot python3-certbot-nginx
Create Jaeger Docker Compose configuration
Set up Jaeger with all-in-one deployment including the collector, query service, and UI components.
sudo mkdir -p /opt/jaeger
cd /opt/jaeger
version: '3.8'
services:
jaeger:
image: jaegertracing/all-in-one:1.52
container_name: jaeger
restart: unless-stopped
ports:
- "127.0.0.1:14268:14268" # HTTP collector
- "127.0.0.1:16686:16686" # Web UI
- "127.0.0.1:14250:14250" # gRPC collector
- "6831:6831/udp" # UDP agent
- "6832:6832/udp" # UDP agent
environment:
- COLLECTOR_OTLP_ENABLED=true
- COLLECTOR_ZIPKIN_HOST_PORT=:9411
volumes:
- jaeger_data:/badger
command: [
"--memory.max-traces=100000",
"--query.base-path=/jaeger",
"--admin.http.host-port=:16687"
]
networks:
- jaeger-net
volumes:
jaeger_data:
networks:
jaeger-net:
driver: bridge
Start Jaeger container
Launch the Jaeger service and verify it's running correctly on the expected ports.
sudo docker compose up -d
sudo docker compose ps
Verify Jaeger is accessible locally:
curl -I http://localhost:16686/jaeger
Create basic authentication file
Set up HTTP basic authentication to secure access to Jaeger. Replace admin with your desired username.
sudo mkdir -p /etc/nginx/auth
sudo htpasswd -c /etc/nginx/auth/jaeger admin
Enter a strong password when prompted. The file will contain the hashed password for secure authentication.
Configure NGINX reverse proxy
Create an NGINX configuration that proxies requests to Jaeger with proper headers and authentication.
server {
listen 80;
server_name jaeger.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name jaeger.example.com;
# SSL configuration will be added by Certbot
# Security headers
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' ws: wss:;" always;
# Enable gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
# Authentication
auth_basic "Jaeger Access";
auth_basic_user_file /etc/nginx/auth/jaeger;
# Proxy settings
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 X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# WebSocket support for real-time updates
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
location /jaeger {
proxy_pass http://127.0.0.1:16686/jaeger;
}
location / {
return 301 /jaeger/;
}
# Health check endpoint (no auth required)
location /health {
auth_basic off;
proxy_pass http://127.0.0.1:16687/;
access_log off;
}
# Block access to sensitive paths
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}
Rate limiting
limit_req_zone $binary_remote_addr zone=jaeger_limit:10m rate=10r/m;
limit_req zone=jaeger_limit burst=5 nodelay;
Enable the NGINX site
Activate the Jaeger configuration and test the NGINX configuration for syntax errors.
sudo ln -sf /etc/nginx/sites-available/jaeger /etc/nginx/sites-enabled/
sudo nginx -t
jaeger.example.com with your actual domain name before proceeding.Obtain SSL certificate
Use Certbot to automatically obtain and configure SSL certificates from Let's Encrypt.
sudo certbot --nginx -d jaeger.example.com
Follow the prompts to enter your email and agree to the terms of service. Certbot will automatically modify your NGINX configuration to include SSL settings.
Configure automatic certificate renewal
Set up automatic renewal for Let's Encrypt certificates to prevent expiration issues.
sudo systemctl enable --now certbot.timer
sudo certbot renew --dry-run
Optimize NGINX configuration
Update the main NGINX configuration for better performance with reverse proxying.
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
# Basic Settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 100M;
# Hide nginx version
server_tokens off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging Settings
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'$request_time $upstream_response_time';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
# Gzip Settings
gzip on;
gzip_vary on;
gzip_comp_level 6;
gzip_min_length 1000;
gzip_types
application/atom+xml
application/geo+json
application/javascript
application/x-javascript
application/json
application/ld+json
application/manifest+json
application/rdf+xml
application/rss+xml
application/xhtml+xml
application/xml
font/eot
font/otf
font/ttf
image/svg+xml
text/css
text/javascript
text/plain
text/xml;
# Buffer settings
proxy_buffering on;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
# Virtual Host Configs
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Start and enable services
Start NGINX and ensure both NGINX and Docker start automatically on system boot.
sudo systemctl enable --now nginx
sudo systemctl status nginx
sudo systemctl status docker
Configure firewall rules
Open the necessary firewall ports for HTTP, HTTPS, and Jaeger's UDP ports for trace collection.
sudo ufw allow 'Nginx Full'
sudo ufw allow 6831/udp
sudo ufw allow 6832/udp
sudo ufw reload
Verify your setup
Test that Jaeger is accessible through NGINX with proper SSL and authentication:
# Test SSL certificate
curl -I https://jaeger.example.com/health
Test authentication (should return 401)
curl -I https://jaeger.example.com/jaeger
Test with authentication
curl -u admin:your_password -I https://jaeger.example.com/jaeger
Check Docker container status
sudo docker compose -f /opt/jaeger/docker-compose.yml ps
Verify NGINX configuration
sudo nginx -t
Check SSL certificate expiry
sudo certbot certificates
You should be able to access Jaeger at https://jaeger.example.com/jaeger using the username and password you created.
Configure application instrumentation
Configure trace collection endpoints
Your applications need to send traces to Jaeger. Configure them to use these endpoints:
- HTTP Collector:
http://your-server-ip:14268/api/traces - gRPC Collector:
your-server-ip:14250 - UDP Agent (Compact):
your-server-ip:6831 - UDP Agent (Binary):
your-server-ip:6832
Example application configuration
Here's how to configure a Node.js application to send traces to Jaeger:
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
const jaegerExporter = new JaegerExporter({
endpoint: 'http://your-server-ip:14268/api/traces',
});
const sdk = new NodeSDK({
traceExporter: jaegerExporter,
serviceName: 'my-application',
});
sdk.start();
Monitor and maintain your setup
Set up log monitoring
Monitor NGINX and Jaeger logs for issues and performance metrics.
# Monitor NGINX access logs
sudo tail -f /var/log/nginx/access.log
Monitor NGINX error logs
sudo tail -f /var/log/nginx/error.log
Monitor Jaeger container logs
sudo docker compose -f /opt/jaeger/docker-compose.yml logs -f
Configure log rotation
Set up log rotation to prevent disk space issues with growing log files.
/var/log/nginx/access.log /var/log/nginx/error.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
create 0644 www-data www-data
postrotate
systemctl reload nginx
endscript
}
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| 502 Bad Gateway error | Jaeger container not running | sudo docker compose -f /opt/jaeger/docker-compose.yml up -d |
| SSL certificate error | Domain DNS not pointing to server | Update DNS A record to point to server IP |
| Authentication prompt not showing | auth_basic directive missing | Check NGINX configuration for auth_basic lines |
| Traces not appearing | Application misconfigured | Verify collector endpoint URL and network connectivity |
| High memory usage | Too many traces stored | Adjust --memory.max-traces parameter in docker-compose.yml |
| Certificate renewal fails | NGINX configuration conflicts | sudo certbot renew --nginx --force-renewal |
Next steps
- Set up Jaeger multi-datacenter replication for disaster recovery and high availability
- Set up Jaeger high availability clustering with load balancing and failover
- Configure Jaeger data retention policies and automated archiving with Elasticsearch backend
- Integrate Jaeger with Kubernetes service mesh for comprehensive distributed tracing
- Set up OpenTelemetry custom instrumentation and metrics collection with Prometheus integration
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Global variables
DOMAIN_NAME=""
EMAIL=""
JAEGER_DIR="/opt/jaeger"
NGINX_CONFIG_DIR=""
PKG_MGR=""
PKG_INSTALL=""
HTPASSWD_PKG=""
# Usage function
usage() {
echo "Usage: $0 <domain_name> <email>"
echo "Example: $0 jaeger.example.com admin@example.com"
exit 1
}
# Error handling
cleanup() {
echo -e "${RED}[ERROR] Script failed. Cleaning up...${NC}"
systemctl stop jaeger 2>/dev/null || true
docker compose -f ${JAEGER_DIR}/docker-compose.yml down 2>/dev/null || true
rm -rf ${JAEGER_DIR} 2>/dev/null || true
}
trap cleanup ERR
# Check prerequisites
check_prerequisites() {
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}This script must be run as root${NC}"
exit 1
fi
if [[ $# -ne 2 ]]; then
usage
fi
DOMAIN_NAME="$1"
EMAIL="$2"
if [[ ! "$EMAIL" =~ ^[^@]+@[^@]+\.[^@]+$ ]]; then
echo -e "${RED}Invalid email format${NC}"
exit 1
fi
}
# Detect distribution
detect_distro() {
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
HTPASSWD_PKG="apache2-utils"
NGINX_CONFIG_DIR="/etc/nginx/sites-available"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
HTPASSWD_PKG="httpd-tools"
NGINX_CONFIG_DIR="/etc/nginx/conf.d"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
HTPASSWD_PKG="httpd-tools"
NGINX_CONFIG_DIR="/etc/nginx/conf.d"
;;
*)
echo -e "${RED}Unsupported distro: $ID${NC}"
exit 1
;;
esac
else
echo -e "${RED}Cannot detect OS distribution${NC}"
exit 1
fi
}
# Update system packages
update_system() {
echo -e "${GREEN}[1/8] Updating system packages...${NC}"
if [[ "$PKG_MGR" == "apt" ]]; then
apt update && apt upgrade -y
else
$PKG_INSTALL epel-release 2>/dev/null || true
$PKG_MGR update -y
fi
}
# Install Docker and Docker Compose
install_docker() {
echo -e "${GREEN}[2/8] Installing Docker and Docker Compose...${NC}"
if [[ "$PKG_MGR" == "apt" ]]; then
$PKG_INSTALL docker.io docker-compose-v2
else
$PKG_INSTALL docker docker-compose
fi
systemctl enable --now docker
usermod -aG docker $USER 2>/dev/null || true
# Verify Docker installation
docker --version
docker compose version
}
# Install NGINX
install_nginx() {
echo -e "${GREEN}[3/8] Installing NGINX...${NC}"
$PKG_INSTALL nginx $HTPASSWD_PKG
systemctl enable nginx
# Create sites-enabled directory for Debian-based systems if needed
if [[ "$PKG_MGR" == "apt" ]]; then
mkdir -p /etc/nginx/sites-enabled
fi
}
# Install Certbot
install_certbot() {
echo -e "${GREEN}[4/8] Installing Certbot...${NC}"
$PKG_INSTALL certbot python3-certbot-nginx
}
# Create Jaeger Docker Compose configuration
create_jaeger_config() {
echo -e "${GREEN}[5/8] Creating Jaeger configuration...${NC}"
mkdir -p $JAEGER_DIR
cat > ${JAEGER_DIR}/docker-compose.yml <<'EOF'
version: '3.8'
services:
jaeger:
image: jaegertracing/all-in-one:1.52
container_name: jaeger
restart: unless-stopped
ports:
- "127.0.0.1:14268:14268"
- "127.0.0.1:16686:16686"
- "127.0.0.1:14250:14250"
- "6831:6831/udp"
- "6832:6832/udp"
environment:
- COLLECTOR_OTLP_ENABLED=true
- COLLECTOR_ZIPKIN_HOST_PORT=:9411
volumes:
- jaeger_data:/badger
command: [
"--memory.max-traces=100000",
"--query.base-path=/jaeger",
"--admin.http.host-port=:16687"
]
networks:
- jaeger-net
volumes:
jaeger_data:
networks:
jaeger-net:
driver: bridge
EOF
chmod 644 ${JAEGER_DIR}/docker-compose.yml
# Start Jaeger
cd $JAEGER_DIR
docker compose up -d
sleep 10
docker compose ps
}
# Create basic authentication
create_auth() {
echo -e "${GREEN}[6/8] Creating basic authentication...${NC}"
mkdir -p /etc/nginx/auth
chmod 755 /etc/nginx/auth
echo -e "${YELLOW}Enter password for admin user:${NC}"
htpasswd -c /etc/nginx/auth/jaeger admin
chmod 644 /etc/nginx/auth/jaeger
}
# Configure NGINX reverse proxy
configure_nginx() {
echo -e "${GREEN}[7/8] Configuring NGINX reverse proxy...${NC}"
if [[ "$PKG_MGR" == "apt" ]]; then
CONFIG_FILE="${NGINX_CONFIG_DIR}/jaeger"
else
CONFIG_FILE="${NGINX_CONFIG_DIR}/jaeger.conf"
fi
cat > $CONFIG_FILE <<EOF
server {
listen 80;
server_name ${DOMAIN_NAME};
return 301 https://\$server_name\$request_uri;
}
server {
listen 443 ssl http2;
server_name ${DOMAIN_NAME};
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' ws: wss:;" always;
gzip on;
gzip_vary on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
auth_basic "Jaeger Access";
auth_basic_user_file /etc/nginx/auth/jaeger;
location /jaeger {
proxy_pass http://127.0.0.1:16686;
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 X-Forwarded-Host \$host;
proxy_set_header X-Forwarded-Port \$server_port;
proxy_buffering off;
proxy_read_timeout 86400;
}
location / {
return 301 https://\$server_name/jaeger;
}
}
EOF
chmod 644 $CONFIG_FILE
# Enable site for Debian-based systems
if [[ "$PKG_MGR" == "apt" ]]; then
ln -sf /etc/nginx/sites-available/jaeger /etc/nginx/sites-enabled/
fi
# Test NGINX configuration
nginx -t
systemctl restart nginx
}
# Configure SSL with Certbot
configure_ssl() {
echo -e "${GREEN}[8/8] Configuring SSL with Let's Encrypt...${NC}"
certbot --nginx -d $DOMAIN_NAME --email $EMAIL --agree-tos --non-interactive
}
# Configure firewall
configure_firewall() {
echo -e "${GREEN}Configuring firewall...${NC}"
if command -v ufw >/dev/null; then
ufw allow 'Nginx Full'
ufw allow 6831/udp
ufw allow 6832/udp
elif command -v firewall-cmd >/dev/null; then
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --permanent --add-port=6831/udp
firewall-cmd --permanent --add-port=6832/udp
firewall-cmd --reload
fi
}
# Verify installation
verify_installation() {
echo -e "${GREEN}Verifying installation...${NC}"
if docker compose -f ${JAEGER_DIR}/docker-compose.yml ps | grep -q "Up"; then
echo -e "${GREEN}✓ Jaeger container is running${NC}"
else
echo -e "${RED}✗ Jaeger container is not running${NC}"
exit 1
fi
if systemctl is-active --quiet nginx; then
echo -e "${GREEN}✓ NGINX is running${NC}"
else
echo -e "${RED}✗ NGINX is not running${NC}"
exit 1
fi
if curl -sI http://localhost:16686/jaeger | grep -q "200 OK"; then
echo -e "${GREEN}✓ Jaeger UI is accessible locally${NC}"
else
echo -e "${RED}✗ Jaeger UI is not accessible locally${NC}"
exit 1
fi
echo -e "${GREEN}Installation completed successfully!${NC}"
echo -e "${YELLOW}Access Jaeger at: https://${DOMAIN_NAME}/jaeger${NC}"
echo -e "${YELLOW}Username: admin${NC}"
echo -e "${YELLOW}Configure your applications to send traces to:${NC}"
echo -e " - HTTP collector: ${DOMAIN_NAME}:14268"
echo -e " - gRPC collector: ${DOMAIN_NAME}:14250"
echo -e " - UDP agent: ${DOMAIN_NAME}:6831/6832"
}
# Main execution
main() {
check_prerequisites "$@"
detect_distro
update_system
install_docker
install_nginx
install_certbot
create_jaeger_config
create_auth
configure_nginx
configure_ssl
configure_firewall
verify_installation
}
main "$@"
Review the script before running. Execute with: bash install.sh