Getting started with Nginx VTS
Nginx has been my personal goto webserver for a long time already. It is very flexible and high performing overall and there are also pretty good tools to visualize some insights on visits on your different vhosts. But sometimes you want even more info or more detailed info and then you are coming into the territory of changing the nginx logging to get more info and then go and parse that info. At some point if you have more nginx servers running this still becomes somewhat cumbersome. Luckily there is a project out there to get more insights in the use of the vhosts and get metrics form nginx: nginx-module-vts aka “Nginx virtual host traffic status module”.
In this blogpost I want to discover the interesting features of nginx-module-vts and try to figure out how to use it and ingest the metrics from it.
Goals
- installation of vts module
- initial configuration
- advanced configuration to add more metrics
- scraping the metrics to centralize overview
Installation
For the sake of comparison I’ll use Debian since that will compare directly to my work environment.
Since there are no packages in the debian official repo’s I choose to build nginx from scratch with the vts module as well. The Nginx version used is taken from the debian package found in bookworm.
apt-get update && apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
curl \
git \
libpcre3-dev \
zlib1g-dev \
libssl-dev \
export NGINX_VERSION=1.22.1
export VTS_VERSION=0.2.5
cd /tmp
curl -fSL https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz | tar -xz
curl -fSL https://github.com/vozlt/nginx-module-vts/archive/refs/tags/v${VTS_VERSION}.tar.gz | tar -xz
cd nginx-${NGINX_VERSION}
./configure \
--prefix=/etc/nginx \
--sbin-path=/usr/sbin/nginx \
--modules-path=/usr/lib/nginx/modules \
--conf-path=/etc/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--pid-path=/var/run/nginx.pid \
--lock-path=/var/run/nginx.lock \
--http-client-body-temp-path=/var/cache/nginx/client_temp \
--http-proxy-temp-path=/var/cache/nginx/proxy_temp \
--http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \
--http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
--http-scgi-temp-path=/var/cache/nginx/scgi_temp \
--with-threads \
--with-file-aio \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_realip_module \
--with-http_stub_status_module \
--with-http_gzip_static_module \
--add-module=/tmp/nginx-module-vts-${VTS_VERSION}
make -j$(nproc)
make install
Since this is now just for testing this method is fine. For serious use we should get or build a package.
GetPageSpeed has a nice setup guide: https://apt-nginx-extras.getpagespeed.com/apt-setup/.
After adding their repo and updating the cache, nginx and nginx-module-vts can be installed with apt.
apt install nginx nginx-module-vts
Info
The Debian installation mentiones building from source, while writing this up I found out there are pre-built packages available from https://www.getpagespeed.com. Any further mention of Debian is related to the version built from source, not a prebuilt package.
Initial configuration
I’ll be using a custom nginx.conf as starting point which already reflects my production environment more.
nginx.conf
# /etc/nginx/nginx.conf
user nginx;
worker_processes auto;
pcre_jit on;
# Configures default error logger.
error_log /dev/stderr warn;
events {
worker_connections 20480;
}
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
map_hash_bucket_size 128;
map_hash_max_size 8192;
server_names_hash_max_size 16384;
server_names_hash_bucket_size 256;
variables_hash_bucket_size 384;
variables_hash_max_size 24576;
# server_name_in_redirect off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# SSL Settings
##
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
gzip on;
# gzip_vary on;
# gzip_proxied any;
# gzip_comp_level 6;
# gzip_buffers 16 8k;
# gzip_http_version 1.1;
# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# VTS configuration
vhost_traffic_status_zone;
vhost_traffic_status_filter_by_host on;
##
# Virtual Host Configs
##
real_ip_header X-Forwarded-For;
client_max_body_size 0;
include /etc/nginx/vhosts.conf;
}
Addition to non VTS nginx
# VTS configuration
vhost_traffic_status_zone;
vhost_traffic_status_filter_by_host on;
With the above section added in the http block in the existing nginx.conf
VTS is in use. Because of this we can get status information related to the
configured vhosts.
VTS status page
To get the status page, there is a vhost needed or a /status path for example.
Below a catch all vhost with a /status path where the VTS status page is displayed.
server {
server_name nginx-debian-vts.internal.herecura.eu;
listen 80;
listen [::]:80;
return 301 https://$server_name$request_uri;
}
server {
server_name nginx-debian-vts.internal.herecura.eu;
listen 443 ssl;
listen [::]:443 ssl http2;
ssl_certificate /etc/nginx/certs/wildcard.crt;
ssl_certificate_key /etc/nginx/certs/wildcard.key;
# Targeted sslTlsMin: 120
ssl_ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:!aNULL:!CAMELLIA:!DES:!eNULL:!EXPORT:!MD5:!PSK:!RC4;
ssl_prefer_server_ciphers on;
location /status {
vhost_traffic_status_display;
vhost_traffic_status_display_format html;
}
# Catch all paths and return HTML
location / {
default_type text/html;
return 200 '<!DOCTYPE html><html><head><title>Nginx Debian VTS</title></head><body><h1>Nginx Debian VTS</h1><p>Server: $host</p><p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris mi tellus, lacinia ut cursus non, vehicula cursus nunc. Donec ac feugiat quam. Nullam placerat, ipsum nec pretium semper, magna nisi mattis nisl, id sollicitudin purus eros non eros. Nulla euismod augue mauris, sed imperdiet nunc egestas in. Pellentesque interdum ullamcorper dui, id condimentum arcu molestie eget. Donec id aliquam orci. Vestibulum molestie consequat tellus, at lobortis lorem maximus at. Donec molestie lacus risus, ut pretium libero lacinia ac. Mauris consequat purus at libero posuere, quis mollis elit bibendum. Nunc vulputate gravida est at sodales. Ut aliquet eleifend condimentum. Sed vel semper turpis. Pellentesque blandit, enim at maximus aliquam, orci orci bibendum augue, at porttitor eros orci non risus.</p><p>In rutrum molestie ante, nec bibendum purus faucibus vitae. Duis venenatis erat ac turpis sagittis, id egestas erat dapibus. Vivamus fringilla id turpis tempus suscipit. In eu turpis ut sem efficitur semper. Maecenas nec condimentum leo. Vivamus tincidunt odio et massa gravida pellentesque. Nullam a lacus a nisi rhoncus vestibulum vitae eget lectus. Quisque ac elit nec massa tincidunt pulvinar. Morbi venenatis, dolor sit amet fringilla interdum, neque neque scelerisque ligula, congue accumsan arcu nulla ac massa.</p><p>Morbi maximus placerat augue eu tincidunt. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer aliquam leo at dui vestibulum commodo. Donec tincidunt ipsum id purus gravida tincidunt. Nunc sodales quam in lobortis vulputate. In posuere interdum enim ultricies tempor. Integer luctus dapibus mi sit amet posuere. Integer sit amet interdum dolor. Curabitur et maximus ante. Pellentesque mollis vestibulum libero lobortis hendrerit. Praesent id odio gravida, hendrerit purus in, feugiat ipsum. Vestibulum et cursus ipsum, quis scelerisque nibh. Aenean magna purus, eleifend vel volutpat nec, convallis nec justo.</p><p>Praesent pellentesque mattis tortor id iaculis. Quisque vitae volutpat est, vel dapibus felis. Sed pulvinar, justo feugiat ornare ornare, diam ex hendrerit lacus, quis ullamcorper tellus velit nec justo. Duis eget ultricies nisi. Suspendisse a mollis dolor, et fringilla leo. Cras sed pharetra lectus, non vestibulum risus. Mauris egestas, mauris id volutpat fermentum, mi metus feugiat nunc, in accumsan velit sapien et mauris. In sed consequat lorem, sit amet rhoncus augue. Vivamus augue tortor, rutrum vel bibendum sed, suscipit ut sapien. Curabitur ut dolor feugiat lacus ultrices pulvinar a et neque. Duis gravida est sit amet dui egestas, a vestibulum dolor lacinia. Morbi posuere vitae elit sed vestibulum.</p><p>Path: $uri</p><p>Time: $time_local</p></body></html>';
}
}
Info
The vhost configuration is for Nginx 1.22, 1.28 will state the http2 addition to listen is deprecated
This vhost returns what we call a synthetic response, no documentroot, everything here is contained in the Nginx configuration.
When the /status endpoint of this vhost is visited we get the VTS html status page:

More advanced configuration
The same nginx.conf can be reused so below only the VTS specific changes will be listed.
# VTS configuration
vhost_traffic_status_filter_by_host on;
# Increase from default 1MB to 64MB
vhost_traffic_status_zone shared:vhost_traffic_status:64m;
vhost_traffic_status_filter_by_set_key $account_code account_code::*;
vhost_traffic_status_filter_by_set_key $http_user_agent user_agent::*;
Because a non standard variable $account_code is introduced here, all
configured vhosts (server) blocks must contain it to avoid needless warnings
in the logs.
Since this more advanced test was also done with 1000 vhosts there was a need to change the shared memory size. The default is 1m and with the additional filters and more vhosts the following error started popping up in the logs.
2026/03/07 14:05:58 [crit] 12#12: ngx_slab_alloc() failed: no memory in vhost_traffic_status_zone "ngx_http_vhost_traffic_status"
2026/03/07 14:05:58 [error] 12#12: *53164 shm_add_node::ngx_slab_alloc_locked() failed: used_size[900326], used_node[251] while logging request, client: 192.168.7.50, server: vhost-0266-nginx-debian-vts.internal.herecura.eu, request: "GET / HTTP/1.1", host: "vhost-0266-nginx-debian-vts.internal.herecura.eu"
2026/03/07 14:05:58 [error] 12#12: *53164 handler::shm_add_server() failed while logging request, client: 192.168.7.50, server: vhost-0266-nginx-debian-vts.internal.herecura.eu, request: "GET / HTTP/1.1", host: "vhost-0266-nginx-debian-vts.internal.herecura.eu"
The vhosts now also have VTS related configuration to create more fine grained metrics from the config.
server {
server_name nginx-debian-vts.internal.herecura.eu;
listen 80;
listen [::]:80;
return 301 https://$server_name$request_uri;
}
server {
server_name nginx-debian-vts.internal.herecura.eu;
# set account_code since we have setup vts to use it
set $account_code "";
# VTS configuration
vhost_traffic_status_filter_by_set_key $account_code account_code::$host;
vhost_traffic_status_filter_by_set_key $http_user_agent user_agent::$account_code::$host;
listen 443 ssl;
listen [::]:443 ssl http2;
ssl_certificate /etc/nginx/certs/wildcard.crt;
ssl_certificate_key /etc/nginx/certs/wildcard.key;
# Targeted sslTlsMin: 120
ssl_ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:!aNULL:!CAMELLIA:!DES:!eNULL:!EXPORT:!MD5:!PSK:!RC4;
ssl_prefer_server_ciphers on;
location /status {
vhost_traffic_status_display;
vhost_traffic_status_display_format html;
}
# Catch all paths and return HTML
location / {
default_type text/html;
return 200 '<!DOCTYPE html><html><head><title>Nginx Debian VTS</title></head><body><h1>Nginx Debian VTS</h1><p>Server: $host</p><p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris mi tellus, lacinia ut cursus non, vehicula cursus nunc. Donec ac feugiat quam. Nullam placerat, ipsum nec pretium semper, magna nisi mattis nisl, id sollicitudin purus eros non eros. Nulla euismod augue mauris, sed imperdiet nunc egestas in. Pellentesque interdum ullamcorper dui, id condimentum arcu molestie eget. Donec id aliquam orci. Vestibulum molestie consequat tellus, at lobortis lorem maximus at. Donec molestie lacus risus, ut pretium libero lacinia ac. Mauris consequat purus at libero posuere, quis mollis elit bibendum. Nunc vulputate gravida est at sodales. Ut aliquet eleifend condimentum. Sed vel semper turpis. Pellentesque blandit, enim at maximus aliquam, orci orci bibendum augue, at porttitor eros orci non risus.</p><p>In rutrum molestie ante, nec bibendum purus faucibus vitae. Duis venenatis erat ac turpis sagittis, id egestas erat dapibus. Vivamus fringilla id turpis tempus suscipit. In eu turpis ut sem efficitur semper. Maecenas nec condimentum leo. Vivamus tincidunt odio et massa gravida pellentesque. Nullam a lacus a nisi rhoncus vestibulum vitae eget lectus. Quisque ac elit nec massa tincidunt pulvinar. Morbi venenatis, dolor sit amet fringilla interdum, neque neque scelerisque ligula, congue accumsan arcu nulla ac massa.</p><p>Morbi maximus placerat augue eu tincidunt. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer aliquam leo at dui vestibulum commodo. Donec tincidunt ipsum id purus gravida tincidunt. Nunc sodales quam in lobortis vulputate. In posuere interdum enim ultricies tempor. Integer luctus dapibus mi sit amet posuere. Integer sit amet interdum dolor. Curabitur et maximus ante. Pellentesque mollis vestibulum libero lobortis hendrerit. Praesent id odio gravida, hendrerit purus in, feugiat ipsum. Vestibulum et cursus ipsum, quis scelerisque nibh. Aenean magna purus, eleifend vel volutpat nec, convallis nec justo.</p><p>Praesent pellentesque mattis tortor id iaculis. Quisque vitae volutpat est, vel dapibus felis. Sed pulvinar, justo feugiat ornare ornare, diam ex hendrerit lacus, quis ullamcorper tellus velit nec justo. Duis eget ultricies nisi. Suspendisse a mollis dolor, et fringilla leo. Cras sed pharetra lectus, non vestibulum risus. Mauris egestas, mauris id volutpat fermentum, mi metus feugiat nunc, in accumsan velit sapien et mauris. In sed consequat lorem, sit amet rhoncus augue. Vivamus augue tortor, rutrum vel bibendum sed, suscipit ut sapien. Curabitur ut dolor feugiat lacus ultrices pulvinar a et neque. Duis gravida est sit amet dui egestas, a vestibulum dolor lacinia. Morbi posuere vitae elit sed vestibulum.</p><p>Path: $uri</p><p>Time: $time_local</p></body></html>';
}
}
When checking the /status html page now we get more detailed information:

Due to the additional VTS config in the vhosts it’s getting pretty fine grained to search for info and metrics.
Scraping prometheus metrics
Info
This section requires working setup of:
- victoriametrics
- grafana
Since there is also prometheus output of the VTS status, centralizing the metric information in a single prometheus metrics server becomes a possibility.
With the earlier given setup the prometheus endpoint is
/status/format/prometheus. Due to the fact that everything in the VTS config
is custom there is a need to do some relabeling while scraping.
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'nginx-vts'
static_configs:
- targets: ['https://nginx-debian-vts.internal.herecura.eu']
metrics_path: /status/format/prometheus
metric_relabel_configs:
# 1. Extract filter_type (user_agent or account_code) from first segment
- source_labels: [filter]
regex: '^([^:]+)::.*'
target_label: filter_type
replacement: '${1}'
# 2. Extract account from three-part user_agent filters (user_agent::account::vhost)
- source_labels: [filter_type, filter]
regex: 'user_agent;[^:]+::([^:]+)::[^:]+'
target_label: account
replacement: '${1}'
# 3. For account_code filters, extract account from filter_name (contains "nginxtest1")
- source_labels: [filter_type, filter_name]
regex: 'account_code;(.+)'
target_label: account
replacement: '${1}'
# 4. Extract host from three-part filters (type::account::vhost)
- source_labels: [filter]
regex: '^[^:]+::[^:]+::(.+)$'
target_label: host
replacement: '${1}'
# 5. Extract host from two-part filters (type::vhost-...), only if not already set
- source_labels: [filter, host]
regex: '^[^:]+::([^*]+)$;'
target_label: host
replacement: '${1}'
# 6. Set vhost="*" for wildcard patterns
- source_labels: [filter, host]
regex: '^[^:]+::\*$;'
target_label: host
replacement: '*'
# 7. Copy filter_name to user_agent label for user_agent filters
- source_labels: [filter_type, filter_name]
regex: 'user_agent;(.+)'
target_label: user_agent
replacement: '${1}'
# Optional: Drop original verbose labels to reduce cardinality
- regex: '^(filter|filter_name)$'
action: labeldrop
With the relabeling the information is more or less unified accross the default vhost metrics and the custom filters.
When querying on nginx_vts_filter_requests_total in vmui we can see the relabling doing it’s job
nginx_vts_filter_requests_total {account="nginxtest10",code="2xx",filter_type="account_code",instance="nginx-debian-vts.internal.herecura.eu:443",job="nginx-vts"}
min: 200 median: 13,300 max: 21,600
nginx_vts_filter_requests_total {account="nginxtest10",code="2xx",filter_type="user_agent",host="vhost-0010-nginx-debian-vts.internal.herecura.eu",instance="nginx-debian-vts.internal.herecura.eu:443",job="nginx-vts",user_agent="Grafana k6/1.6.1"}
min: 200 median: 13,300 max: 21,600
nginx_vts_filter_requests_total {account="nginxtest1",code="2xx",filter_type="account_code",instance="nginx-debian-vts.internal.herecura.eu:443",job="nginx-vts"}
min: 200 median: 13,000 max: 21,670
nginx_vts_filter_requests_total {account="nginxtest1",code="2xx",filter_type="user_agent",host="vhost-0001-nginx-debian-vts.internal.herecura.eu",instance="nginx-debian-vts.internal.herecura.eu:443",job="nginx-vts",user_agent="Grafana k6/1.6.1"}
min: 200 median: 13,000 max: 21,670
Based on these metrics I have created a initial overview of some information gathered from nginx with VTS:

Download the dashboard here Nginx VTS grafana
Download
Here is a download to reproduce the setup used in this blogpost.
Download docker compose with intial config
Download docker compose with more advanced config + scraping
Create a .env file with the following content structure:
NGINX_BASE_HOST="nginx-debian-vts.example.org"
VHOST_COUNT=1000
VHOST_ACCOUNT_QUOTIENT=250
The VHOST_ACCOUNT_QUOTIENT is only used with the advanced setup.
Start:
# Install generator.py dependencies
pacman -S python-jinja2 python-dotenv
# Create .env file and update it
cp .env-example .env
$EDITOR .env
# Generate nginx config files
python3 generator.py
# Build
docker compose -f docker-compose-debian.yml build
# Run
docker compose -f docker-compose-debian.yml up -d
Info
Some Caveats:
- the setup uses host networking so it will conflict with any already running webserver
- create a wildcard cert to run the tests on https and put them in
certs/wildcard.{cert,key} - the
generator.pyscript requires jinja2 and dotenv installed
Conclusion
The nginx-module-vts addition to Nginx looks very promising. Certainly once there are multiple Nginx server involved in your personal/professional hosting. It looks like there is a lot of information that can be exposed with the additional filtering. The main thing to be researched after this is how it will behave if there is a lot of vhosts. That is a topic for another blogpost.