Deploy Thanos Sidecar, Query, and Compactor to unify multiple Prometheus instances into a single global query view with long-term S3-backed storage. Covers deduplication, TLS, and production troubleshooting.
Prerequisites
- Two or more existing Prometheus instances with local TSDB storage
- Root or sudo access on all Thanos and Prometheus hosts
- An S3-compatible object storage backend such as MinIO
- Basic familiarity with systemd service management
- Internal CA or Vault PKI for issuing TLS certificates
What this solves
A single Prometheus instance cannot scale beyond one cluster or retain years of metrics cheaply. Thanos adds a global query layer across multiple Prometheus servers and offloads long-term storage to S3-compatible object storage with automated downsampling and compaction.
This tutorial deploys Thanos Sidecar, Query, Store Gateway, and Compactor across two Prometheus instances, backed by MinIO object storage, with TLS between components and Prometheus metrics for the Thanos processes themselves.
Understanding Thanos architecture
Thanos Sidecar runs next to each Prometheus instance and exposes its local TSDB blocks over gRPC, while also uploading completed blocks to object storage. Thanos Query (Querier) talks to all Sidecars and Store Gateways via the Store API, merges results, and deduplicates data from replicated Prometheus pairs. Store Gateway serves historical blocks directly from object storage without touching local Prometheus disks. Compactor runs as a singleton process that merges, downsamples, and applies retention rules to blocks in the bucket.
Step-by-step installation
Provision the object storage backend
Thanos needs an S3-compatible bucket for blocks. This example uses MinIO on a dedicated host at 203.0.113.20. If you already run MinIO, you can reuse it, see Install and configure MinIO object storage with SSL and clustering for a full setup.
mc alias set myminio https://203.0.113.20:9000 thanosadmin 'Str0ngM1nioSecretKey!'
mc mb myminio/thanos-metrics
mc admin user add myminio thanos-svc 'An0therStr0ngSecretKey!'
mc admin policy attach myminio readwrite --user thanos-svcCreate the object storage config file
Every Thanos component that talks to object storage needs this file. Keep it out of version control since it contains credentials.
type: S3
config:
bucket: "thanos-metrics"
endpoint: "203.0.113.20:9000"
access_key: "thanos-svc"
secret_key: "An0therStr0ngSecretKey!"
insecure: false
signature_version2: false
http_config:
tls_config:
ca_file: /etc/thanos/certs/minio-ca.pemsudo mkdir -p /etc/thanos/certs
sudo chown -R thanos:thanos /etc/thanos
sudo chmod 640 /etc/thanos/bucket.ymlCreate the thanos system user and download binaries
Run Thanos as a dedicated unprivileged user, never as root.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin thanos
sudo apt update && sudo apt install -y curl tar
curl -L -o /tmp/thanos.tar.gz https://github.com/thanos-io/thanos/releases/download/v0.36.1/thanos-0.36.1.linux-amd64.tar.gz
sudo tar -xzf /tmp/thanos.tar.gz -C /opt
sudo ln -s /opt/thanos-0.36.1.linux-amd64/thanos /usr/local/bin/thanossudo useradd --system --no-create-home --shell /sbin/nologin thanos
sudo dnf install -y curl tar
curl -L -o /tmp/thanos.tar.gz https://github.com/thanos-io/thanos/releases/download/v0.36.1/thanos-0.36.1.linux-amd64.tar.gz
sudo tar -xzf /tmp/thanos.tar.gz -C /opt
sudo ln -s /opt/thanos-0.36.1.linux-amd64/thanos /usr/local/bin/thanosDeploy Thanos Sidecar alongside each Prometheus instance
The sidecar needs read access to Prometheus's data directory and its config reload endpoint. Run this on each Prometheus host (203.0.113.11 and 203.0.113.12 in this example).
[Unit]
Description=Thanos Sidecar
After=network.target prometheus.service
[Service]
User=thanos
Group=thanos
ExecStart=/usr/local/bin/thanos sidecar \
--tsdb.path=/var/lib/prometheus \
--prometheus.url=http://127.0.0.1:9090 \
--grpc-address=0.0.0.0:10901 \
--http-address=127.0.0.1:10902 \
--objstore.config-file=/etc/thanos/bucket.yml \
--grpc-server-tls-cert=/etc/thanos/certs/sidecar.pem \
--grpc-server-tls-key=/etc/thanos/certs/sidecar-key.pem \
--grpc-server-tls-client-ca=/etc/thanos/certs/ca.pem
Restart=on-failure
LimitNOFILE=65536
[Install]
WantedBy=multi-user.targetAdd the sidecar's user to the prometheus group so it can read the TSDB directory without loosening permissions on other files.
sudo usermod -aG prometheus thanos
sudo chmod 750 /var/lib/prometheus
sudo systemctl daemon-reload
sudo systemctl enable --now thanos-sidecarEnable Prometheus remote-write flags for the sidecar
Prometheus must run with a minimum retention that overlaps with the compactor's upload window, and expose the admin API for block cleanup.
[Service]
ExecStart=
ExecStart=/usr/local/bin/prometheus \
--config.file=/etc/prometheus/prometheus.yml \
--storage.tsdb.path=/var/lib/prometheus \
--storage.tsdb.min-block-duration=2h \
--storage.tsdb.max-block-duration=2h \
--web.enable-lifecyclesudo systemctl daemon-reload
sudo systemctl restart prometheusDeploy Thanos Query for the global view
Query connects to both sidecars directly by gRPC address and, optionally, a Store Gateway for historical data. Run this on a dedicated query node at 203.0.113.30.
[Unit]
Description=Thanos Query
After=network.target
[Service]
User=thanos
Group=thanos
ExecStart=/usr/local/bin/thanos query \
--http-address=0.0.0.0:10904 \
--grpc-address=0.0.0.0:10903 \
--query.replica-label=replica \
--store=203.0.113.11:10901 \
--store=203.0.113.12:10901 \
--store=203.0.113.20:10905 \
--grpc-client-tls-cert=/etc/thanos/certs/query-client.pem \
--grpc-client-tls-key=/etc/thanos/certs/query-client-key.pem \
--grpc-client-tls-ca=/etc/thanos/certs/ca.pem
Restart=on-failure
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now thanos-queryConfigure deduplication and service discovery
If you run Prometheus in HA pairs, label each replica with a distinct replica label at scrape time so Query can deduplicate identical series. For dynamic environments, use file-based service discovery instead of hardcoded --store flags.
global:
external_labels:
replica: A
cluster: prod-eu-west[
{
"targets": ["203.0.113.11:10901", "203.0.113.12:10901"]
}
]Replace the static --store flags with --store.sd-files=/etc/thanos/stores.json and --store.sd-interval=30s so Query picks up new sidecars without a restart. This pairs well with existing service discovery patterns described in Configure HAProxy with Consul for dynamic service discovery if you already run Consul.
Deploy Thanos Store Gateway
Store Gateway serves compacted historical blocks from object storage so Query does not need to hit every sidecar for old data.
[Unit]
Description=Thanos Store Gateway
After=network.target
[Service]
User=thanos
Group=thanos
ExecStart=/usr/local/bin/thanos store \
--data-dir=/var/lib/thanos-store \
--objstore.config-file=/etc/thanos/bucket.yml \
--grpc-address=0.0.0.0:10905 \
--http-address=127.0.0.1:10906 \
--grpc-server-tls-cert=/etc/thanos/certs/store.pem \
--grpc-server-tls-key=/etc/thanos/certs/store-key.pem \
--grpc-server-tls-client-ca=/etc/thanos/certs/ca.pem
Restart=on-failure
[Install]
WantedBy=multi-user.targetsudo mkdir -p /var/lib/thanos-store
sudo chown thanos:thanos /var/lib/thanos-store
sudo chmod 750 /var/lib/thanos-store
sudo systemctl daemon-reload
sudo systemctl enable --now thanos-storeDeploy Thanos Compactor for downsampling and retention
Run exactly one Compactor instance against the bucket. It handles vertical compaction, downsampling to 5m and 1h resolutions, and retention enforcement.
[Unit]
Description=Thanos Compactor
After=network.target
[Service]
User=thanos
Group=thanos
ExecStart=/usr/local/bin/thanos compact \
--data-dir=/var/lib/thanos-compact \
--objstore.config-file=/etc/thanos/bucket.yml \
--http-address=127.0.0.1:10907 \
--retention.resolution-raw=30d \
--retention.resolution-5m=180d \
--retention.resolution-1h=730d \
--wait \
--compact.concurrency=2
Restart=on-failure
[Install]
WantedBy=multi-user.targetsudo mkdir -p /var/lib/thanos-compact
sudo chown thanos:thanos /var/lib/thanos-compact
sudo chmod 750 /var/lib/thanos-compact
sudo systemctl daemon-reload
sudo systemctl enable --now thanos-compactGenerate TLS certificates for gRPC communication
All Thanos gRPC traffic between Sidecar, Query, and Store Gateway should be encrypted with mutual TLS in production. Use a private CA rather than self-signed certs per node.
openssl genrsa -out /etc/thanos/certs/ca-key.pem 4096
openssl req -x509 -new -nodes -key /etc/thanos/certs/ca-key.pem -sha256 -days 3650 \
-out /etc/thanos/certs/ca.pem -subj "/CN=thanos-internal-ca"
openssl genrsa -out /etc/thanos/certs/query-client-key.pem 2048
openssl req -new -key /etc/thanos/certs/query-client-key.pem \
-out /etc/thanos/certs/query-client.csr -subj "/CN=thanos-query"
openssl x509 -req -in /etc/thanos/certs/query-client.csr -CA /etc/thanos/certs/ca.pem \
-CAkey /etc/thanos/certs/ca-key.pem -CAcreateserial -out /etc/thanos/certs/query-client.pem -days 825 -sha256sudo chown -R thanos:thanos /etc/thanos/certs
sudo chmod 600 /etc/thanos/certs/*-key.pem
sudo chmod 644 /etc/thanos/certs/*.pemIf you already manage internal PKI, see Set up Vault as a PKI certificate authority for issuing and rotating these certificates automatically instead of managing openssl by hand.
Put Query behind a reverse proxy with authentication
Thanos Query's HTTP UI and API have no built-in authentication. Front it with a reverse proxy that enforces TLS and basic auth or OAuth2 for anyone outside the internal network.
server {
listen 443 ssl;
server_name thanos.example.com;
ssl_certificate /etc/letsencrypt/live/thanos.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/thanos.example.com/privkey.pem;
auth_basic "Thanos Query";
auth_basic_user_file /etc/nginx/.htpasswd-thanos;
location / {
proxy_pass http://127.0.0.1:10904;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}For certificate automation, follow Implement Nginx SSL certificate automation with Let's Encrypt using Certbot.
Monitoring Thanos Query and Compactor performance
Each Thanos component exposes its own Prometheus metrics on its HTTP address. Scrape them from a dedicated monitoring Prometheus instance separate from the ones being federated.
scrape_configs:
- job_name: thanos-query
static_configs:
- targets: ['203.0.113.30:10904']
- job_name: thanos-compact
static_configs:
- targets: ['203.0.113.20:10907']
- job_name: thanos-store
static_configs:
- targets: ['203.0.113.20:10906']Key metrics to alert on: thanos_compact_group_compactions_failures_total for failed compactions, thanos_query_concurrent_gate_queries_max for query concurrency saturation, and thanos_compact_halted which is 1 when the compactor has hit a fatal block error and stopped processing. Build alert rules following the pattern in Configure Prometheus alerting with AlertManager notifications and webhook integration.
Verify your setup
sudo systemctl status thanos-sidecar thanos-query thanos-store thanos-compact
curl -s http://127.0.0.1:10904/-/healthy
curl -s http://127.0.0.1:10904/api/v1/stores | python3 -m json.toolOpen the Query UI at https://thanos.example.com/stores and confirm both sidecars and the store gateway show as UP. Run a test query spanning both Prometheus instances to confirm the global view merges results correctly.
mc ls myminio/thanos-metrics/ | head
mc ls myminio/thanos-metrics/debug/ 2>/dev/nullCommon issues
| Symptom | Cause | Fix |
|---|---|---|
| Query shows store as down | gRPC TLS handshake failure between Query and Sidecar | Verify CA cert matches on both sides with openssl verify -CAfile ca.pem sidecar.pem |
| Duplicate data points in graphs | Missing or mismatched replica label | Set unique external_labels.replica per Prometheus and pass --query.replica-label=replica |
| Compactor halted with block overlap error | Two compactors ran against the same bucket concurrently | Stop the duplicate process, manually inspect overlapping blocks with thanos tools bucket verify, run repair if needed |
| Sidecar not uploading blocks | Prometheus min/max block duration set to default (2h auto-compact), or wrong bucket credentials | Confirm both duration flags are equal and check sidecar logs for S3 auth errors |
| Store gateway high memory usage | Index cache too large for available RAM | Tune --index-cache-size and --chunk-pool-size to match available memory |
| Query returns 503 under load | Too many concurrent queries against limited gate | Increase --query.max-concurrent carefully and add caching layer in front |
Next steps
- Set up Thanos Receiver for remote write scalability with Prometheus integration
- Configure Thanos Receiver clustering for high availability and load distribution
- Configure Thanos Ruler for distributed alerting across multiple Prometheus clusters
- Implement Thanos multi-cluster federation for global Prometheus metrics aggregation
- Configure Prometheus long-term storage with Thanos for unlimited data retention
- Setup MinIO monitoring with Prometheus and Grafana dashboards
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# ---------------------------------------------------------------------------
# Thanos component installer: Sidecar, Query, Store Gateway, Compactor
# ---------------------------------------------------------------------------
VERSION="0.36.1"
THANOS_USER="thanos"
THANOS_GROUP="thanos"
INSTALL_DIR="/opt/thanos-${VERSION}.linux-amd64"
CONFIG_DIR="/etc/thanos"
CERTS_DIR="${CONFIG_DIR}/certs"
DATA_DIR="/var/lib/thanos"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
log() { echo -e "${GREEN}$*${NC}"; }
warn() { echo -e "${YELLOW}$*${NC}"; }
err() { echo -e "${RED}$*${NC}" >&2; }
usage() {
cat <<EOF
Usage: $0 --role <sidecar|query|store|compactor> [options]
Common options:
--bucket-endpoint <host:port> MinIO/S3 endpoint (required for all roles except query)
--access-key <key> S3 access key
--secret-key <secret> S3 secret key
--bucket-name <name> Bucket name (default: thanos-metrics)
Role-specific:
sidecar: --prometheus-url <url> --tsdb-path <path>
query: --store-addrs "host1:port,host2:port" (comma separated gRPC endpoints)
Example:
$0 --role sidecar --bucket-endpoint 203.0.113.20:9000 --access-key thanos-svc \\
--secret-key 'secret' --prometheus-url http://127.0.0.1:9090 --tsdb-path /var/lib/prometheus
EOF
exit 1
}
# ---------------------------------------------------------------------------
# Defaults
# ---------------------------------------------------------------------------
ROLE=""
BUCKET_ENDPOINT=""
ACCESS_KEY=""
SECRET_KEY=""
BUCKET_NAME="thanos-metrics"
PROM_URL="http://127.0.0.1:9090"
TSDB_PATH="/var/lib/prometheus"
STORE_ADDRS=""
while [ $# -gt 0 ]; do
case "$1" in
--role) ROLE="$2"; shift 2 ;;
--bucket-endpoint) BUCKET_ENDPOINT="$2"; shift 2 ;;
--access-key) ACCESS_KEY="$2"; shift 2 ;;
--secret-key) SECRET_KEY="$2"; shift 2 ;;
--bucket-name) BUCKET_NAME="$2"; shift 2 ;;
--prometheus-url) PROM_URL="$2"; shift 2 ;;
--tsdb-path) TSDB_PATH="$2"; shift 2 ;;
--store-addrs) STORE_ADDRS="$2"; shift 2 ;;
-h|--help) usage ;;
*) err "Unknown argument: $1"; usage ;;
esac
done
[ -z "$ROLE" ] && { err "Missing --role"; usage; }
case "$ROLE" in
sidecar|query|store|compactor) ;;
*) err "Invalid role: $ROLE"; usage ;;
esac
if [ "$ROLE" != "query" ] && { [ -z "$BUCKET_ENDPOINT" ] || [ -z "$ACCESS_KEY" ] || [ -z "$SECRET_KEY" ]; }; then
err "Roles other than 'query' require --bucket-endpoint, --access-key, --secret-key"
usage
fi
if [ "$(id -u)" -ne 0 ]; then
err "This script must be run as root or with sudo."
exit 1
fi
# ---------------------------------------------------------------------------
# Distro detection
# ---------------------------------------------------------------------------
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian) PKG_MGR="apt"; PKG_INSTALL="apt install -y" ;;
almalinux|rocky|centos|rhel|ol|fedora) PKG_MGR="dnf"; PKG_INSTALL="dnf install -y" ;;
amzn) PKG_MGR="yum"; PKG_INSTALL="yum install -y" ;;
*) err "Unsupported distro: $ID"; exit 1 ;;
esac
else
err "/etc/os-release not found - cannot detect distro"
exit 1
fi
NOLOGIN_SHELL="/usr/sbin/nologin"
[ "$PKG_MGR" != "apt" ] && NOLOGIN_SHELL="/sbin/nologin"
TOTAL_STEPS=7
CURRENT=0
step() { CURRENT=$((CURRENT + 1)); echo -e "${GREEN}[${CURRENT}/${TOTAL_STEPS}]${NC} $*"; }
# ---------------------------------------------------------------------------
# Rollback on failure
# ---------------------------------------------------------------------------
SERVICE_NAME="thanos-${ROLE}"
cleanup_on_error() {
err "Installation failed. Rolling back..."
systemctl stop "${SERVICE_NAME}" 2>/dev/null || true
systemctl disable "${SERVICE_NAME}" 2>/dev/null || true
rm -f "/etc/systemd/system/${SERVICE_NAME}.service"
systemctl daemon-reload 2>/dev/null || true
exit 1
}
trap cleanup_on_error ERR
# ---------------------------------------------------------------------------
# 1. Prerequisites
# ---------------------------------------------------------------------------
step "Checking prerequisites and installing curl/tar..."
if [ "$PKG_MGR" = "apt" ]; then
apt update
fi
$PKG_INSTALL curl tar >/dev/null
# ---------------------------------------------------------------------------
# 2. Create system user
# ---------------------------------------------------------------------------
step "Creating dedicated thanos system user..."
if ! id "$THANOS_USER" >/dev/null 2>&1; then
useradd --system --no-create-home --shell "$NOLOGIN_SHELL" "$THANOS_USER"
log "Created user ${THANOS_USER}"
else
warn "User ${THANOS_USER} already exists, skipping"
fi
# ---------------------------------------------------------------------------
# 3. Download and install Thanos binary
# ---------------------------------------------------------------------------
step "Downloading and installing Thanos v${VERSION}..."
if [ ! -x /usr/local/bin/thanos ]; then
TMP_TARBALL="$(mktemp /tmp/thanos.XXXXXX.tar.gz)"
curl -fL -o "$TMP_TARBALL" \
"https://github.com/thanos-io/thanos/releases/download/v${VERSION}/thanos-${VERSION}.linux-amd64.tar.gz"
tar -xzf "$TMP_TARBALL" -C /opt
rm -f "$TMP_TARBALL"
ln -sf "${INSTALL_DIR}/thanos" /usr/local/bin/thanos
else
warn "thanos binary already installed, skipping download"
fi
# ---------------------------------------------------------------------------
# 4. Config, cert, and data directories with correct ownership/permissions
# ---------------------------------------------------------------------------
step "Setting up config, cert, and data directories..."
mkdir -p "$CERTS_DIR" "$DATA_DIR"
chown -R "${THANOS_USER}:${THANOS_GROUP}" "$CONFIG_DIR" "$DATA_DIR"
chmod 755 "$CONFIG_DIR" "$CERTS_DIR" "$DATA_DIR"
# Object storage config file - only needed for sidecar/store/compactor
if [ "$ROLE" != "query" ]; then
BUCKET_FILE="${CONFIG_DIR}/bucket.yml"
cat > "$BUCKET_FILE" <<EOF
type: S3
config:
bucket: "${BUCKET_NAME}"
endpoint: "${BUCKET_ENDPOINT}"
access_key: "${ACCESS_KEY}"
secret_key: "${SECRET_KEY}"
insecure: false
signature_version2: false
http_config:
tls_config:
ca_file: ${CERTS_DIR}/minio-ca.pem
EOF
chown "${THANOS_USER}:${THANOS_GROUP}" "$BUCKET_FILE"
chmod 640 "$BUCKET_FILE"
fi
# ---------------------------------------------------------------------------
# 5. Build the ExecStart line per role
# ---------------------------------------------------------------------------
step "Generating systemd unit for thanos-${ROLE}..."
case "$ROLE" in
sidecar)
EXEC_LINE="/usr/local/bin/thanos sidecar \\
--tsdb.path=${TSDB_PATH} \\
--prometheus.url=${PROM_URL} \\
--grpc-address=0.0.0.0:10901 \\
--http-address=127.0.0.1:10902 \\
--objstore.config-file=${CONFIG_DIR}/bucket.yml \\
--grpc-server-tls-cert=${CERTS_DIR}/sidecar.pem \\
--grpc-server-tls-key=${CERTS_DIR}/sidecar-key.pem \\
--grpc-server-tls-client-ca=${CERTS_DIR}/ca.pem"
# Sidecar needs to read Prometheus data directory
usermod -aG prometheus "$THANOS_USER" 2>/dev/null || warn "prometheus group not found, skipping group add"
;;
query)
[ -z "$STORE_ADDRS" ] && { err "--store-addrs required for query role"; usage; }
IFS=',' read -ra ADDR_ARR <<< "$STORE_ADDRS"
STORE_FLAGS=""
for a in "${ADDR_ARR[@]}"; do
STORE_FLAGS+=" --store=${a}"
done
EXEC_LINE="/usr/local/bin/thanos query \\
--grpc-address=0.0.0.0:10901 \\
--http-address=0.0.0.0:10904 \\
--query.replica-label=replica${STORE_FLAGS} \\
--grpc-client-tls-cert=${CERTS_DIR}/query.pem \\
--grpc-client-tls-key=${CERTS_DIR}/query-key.pem \\
--grpc-client-tls-ca=${CERTS_DIR}/ca.pem"
;;
store)
EXEC_LINE="/usr/local/bin/thanos store \\
--data-dir=${DATA_DIR}/store \\
--grpc-address=0.0.0.0:10901 \\
--http-address=127.0.0.1:10905 \\
--objstore.config-
Review the script before running. Execute with: bash install.sh