Mastering Virtual Files in Nginx: A Complete Guide
Learn advanced Nginx techniques to serve files that don't physically exist on disk. Covers location matching, root vs alias vs try_files, named locations, health endpoints for Kubernetes, and performance optimization for containerized deployments.

When deploying modern web applications—especially containerized ones or services on managed platforms—you often don’t have direct access to the web root. What happens when you need to serve robots.txt, security.txt, or a health check endpoint? You can’t just drop a file into a Docker image.
Nginx is more than a reverse proxy. It’s a powerful tool for generating and serving content that doesn’t physically exist on disk. This technique is essential for:
- Containerized apps where you can’t modify the image
- Managed platforms without filesystem access
- Microservices requiring standardized endpoints
- Security compliance files that change independently of app versions
In this comprehensive guide, we’ll explore Nginx’s powerful directives for serving virtual files, from basic techniques to advanced patterns used in production Kubernetes deployments.
Understanding location matching
Before serving virtual files, you must understand how Nginx selects which location block handles a request. This is the most important concept for effective Nginx configuration.
Location matching priority
Nginx evaluates location blocks in a specific order. The first match by priority wins, not the first in the config file:
| Priority | Modifier | Type | Example |
|---|---|---|---|
| 1 | = | Exact match | location = /robots.txt |
| 2 | ^~ | Prefix (stops regex search) | location ^~ /static/ |
| 3 | ~ | Case-sensitive regex | location ~ \.php$ |
| 4 | ~* | Case-insensitive regex | location ~* \.(jpg|png)$ |
| 5 | (none) | Prefix (longest match) | location /api/ |
Practical example
Consider this configuration:
server {
# Priority 5: Prefix (fallback)
location / {
proxy_pass http://app:3000;
}
# Priority 1: Exact match - WINS for /robots.txt
location = /robots.txt {
return 200 "User-agent: *\nAllow: /\n";
}
# Priority 4: Case-insensitive regex
location ~* \.(jpg|png|gif)$ {
root /var/www/images;
}
# Priority 2: Prefix with ^~ - WINS for /static/*
location ^~ /static/ {
alias /var/www/static/;
}
}Core directives: root, alias, try_files
These three directives determine where Nginx looks for files.
Understanding the difference is crucial.
The difference explained
| Directive | Behavior | Best For |
|---|---|---|
root | Appends URI to path | Standard document roots |
alias | Replaces matched prefix with path | Serving files from non-standard locations |
try_files | Checks files in order, falls back to last | SPAs, conditional file serving |
Visual comparison
location /images/ {
root /var/www;
}
# Request: /images/photo.jpg
# Serves: /var/www/images/photo.jpg
# ^^^^^^^^ root + URIlocation /images/ {
alias /data/photos/;
}
# Request: /images/photo.jpg
# Serves: /data/photos/photo.jpg
# ^^^^^^^^^^^^ alias replaces /images/try_files for fallback chains
The try_files directive is incredibly powerful for virtual files:
location / {
# Check if physical file exists, then directory, then fallback
try_files $uri $uri/ @backend;
}
# Named location for backend fallback
location @backend {
proxy_pass http://app:3000;
}The order of checking is:
$uri— Try the exact file path$uri/— Try as a directory with index@backend— Fall back to named location
The return directive
For small, simple content, the return directive is more efficient than creating physical files.
Syntax variations
| Pattern | Description | Example |
|---|---|---|
return CODE; | Return status only | return 204; |
return CODE TEXT; | Return status with body | return 200 “OK”; |
return CODE URL; | Redirect (301, 302, 307, 308) | return 301 https://…; |
Inline content example
server {
# Simple text response
location = /robots.txt {
default_type text/plain;
return 200 "User-agent: *\nDisallow: /private/\n";
}
# JSON response
location = /api/status {
default_type application/json;
return 200 '{"status":"healthy","version":"1.0.0"}';
}
# Empty success (for pings)
location = /ping {
return 204;
}
# Redirect
location = /old-page {
return 301 /new-page;
}
}Named locations and fallbacks
Named locations (prefixed with @) enable sophisticated fallback patterns without URL rewriting.
The @fallback pattern
server {
root /var/www/html;
location / {
# Try static file first, then directory, then app
try_files $uri $uri/ @app;
}
# Named location - can't be accessed directly
location @app {
proxy_pass http://backend:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}SPA (Single Page Application) pattern
For React, Vue, Angular apps that use client-side routing:
server {
root /var/www/app;
index index.html;
location / {
# Try file, then directory, then index.html for client routing
try_files $uri $uri/ /index.html;
}
# Static assets (bypass try_files for performance)
location ^~ /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}Internal locations
Use the internal directive to prevent direct access:
location /protected-files/ {
internal; # Can only be reached via internal redirect
alias /var/secure/files/;
}
# Option A: Direct internal redirect with secure link and error_page
location /download {
internal;
alias /var/secure/files;
# Secure link check
secure_link $arg_md5,$arg_expires;
# IMPORTANT: Include $arg_file in hash to prevent path manipulation
secure_link_md5 "$secure_link_expires$uri$arg_file$remote_addr secret";
if ($secure_link = "") { return 403; }
if ($secure_link = "0") { return 410; }
# Forward to named location to serve file
error_page 418 = @serve_file;
return 418;
}
location @serve_file {
internal;
alias /var/secure/files/$arg_file;
}
# Option B: Backend sets X-Accel-Redirect header
location /download-via-backend {
# Backend (e.g., Node.js/Python) validates auth and returns:
# X-Accel-Redirect: /protected-files/myfile.pdf
proxy_pass http://backend:3000;
}Standard web files
These virtual files are fundamental for SEO, security, and compliance.
robots.txt
The robots.txt file tells web crawlers which parts of your site to access.
location = /robots.txt {
default_type text/plain;
return 200 "User-agent: *\nAllow: /\nSitemap: https://example.com/sitemap.xml\n";
}location = /robots.txt {
alias /etc/nginx/assets/robots.txt;
default_type text/plain;
}humans.txt
Give credit to your team with humans.txt:
location = /humans.txt {
default_type text/plain;
# Note: Update the "Last update" date when deploying changes
return 200 "/* TEAM */\nLead: Jane Doe\nContact: jane@example.com\n\n/* SITE */\nPowered by: Nginx, Astro\nLast update: 2025-12-18\n";
}ads.txt & app-ads.txt
For advertising transparency and fraud prevention. The IAB Tech Lab Specification requires a specific CSV format: Domain, Publisher ID, Account Type, Certification Authority ID.
# Desktop ads.txt
location = /ads.txt {
default_type text/plain;
return 200 "google.com, pub-0000000000000000, DIRECT, f08c47fec0942fa0\n";
}
# Mobile app-ads.txt
location = /app-ads.txt {
alias /etc/nginx/assets/app-ads.txt;
default_type text/plain;
}favicon.ico
While modern browsers prefer SVG or PNG, favicon.ico is still requested by legacy tools and crawlers. A single .ico file typically bundles 16x16, 32x32, and 48x48 sizes.
# Serve from custom location
location = /favicon.ico {
alias /etc/nginx/assets/favicon.ico;
access_log off;
expires 1y;
}
# Or return empty (no favicon)
location = /favicon.ico {
return 204;
access_log off;
}Reference: How to Favicon: A comprehensive guide
Mobile and touch icons
Modern web applications need icons for mobile home screens and progressive web app (PWA) features.
Apple Touch Icon
iOS uses this when a website is added to the home screen. The standard is a 180x180 PNG file without transparency.
Android & Web App Manifest
Android Chrome uses site.webmanifest to identify app icons (standard sizes are 192x192 and 512x512).
# Apple Touch Icon
location = /apple-touch-icon.png {
alias /var/www/assets/apple-touch-icon.png;
access_log off;
expires 1y;
}
# Web App Manifest
location = /site.webmanifest {
default_type application/manifest+json;
return 200 '{
"name": "My App",
"short_name": "App",
"icons": [
{ "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" },
{ "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" }
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}';
}The .well-known directory
The .well-known directory is a standard location for site-wide metadata.
security.txt (RFC 9116)
Allow security researchers to report vulnerabilities:
location = /.well-known/security.txt {
default_type text/plain;
return 200 "Contact: mailto:security@example.com\nExpires: 2027-12-31T23:59:59.000Z\nPreferred-Languages: en, es\nCanonical: https://example.com/.well-known/security.txt\n";
}
# Also serve at root for compatibility
location = /security.txt {
return 301 /.well-known/security.txt;
}ACME Challenges (Let’s Encrypt)
Handle certificate validation across multiple services:
# ACME Challenges - Choose only ONE of the following two location blocks:
# Option A: Serve from shared directory (for certbot standalone/webroot mode)
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
default_type text/plain;
}
# Option B: Proxy to certbot container (for Docker setups - comment out Option A if using this)
# location ^~ /.well-known/acme-challenge/ {
# proxy_pass http://certbot:80;
# }Mobile app associations
Link your website to mobile apps:
# Android App Links (assetlinks.json)
location = /.well-known/assetlinks.json {
alias /etc/nginx/assets/assetlinks.json;
default_type application/json;
}
# iOS Universal Links (no file extension!)
location = /.well-known/apple-app-site-association {
alias /etc/nginx/assets/apple-app-site-association;
default_type application/json;
}Other .well-known files
| Path | Purpose | Content-Type |
|---|---|---|
/.well-known/change-password | Redirect to password change page | Redirect (302) |
/.well-known/webfinger | User discovery (ActivityPub, email) | application/jrd+json |
/.well-known/openid-configuration | OpenID Connect discovery | application/json |
/.well-known/matrix/client | Matrix client discovery | application/json |
Dynamic content with Nginx JavaScript (njs)
For complex scenarios, the standard for serving virtual files with logic (conditional content, dynamic JSON) is njs (Nginx JavaScript). Unlike return, which only serves static strings, njs allows full programmatic control.
Setup
First, ensure the module is loaded in nginx.conf:
load_module modules/ngx_http_js_module.so;Example: dynamic robots.txt
Serve different robots.txt rules based on the request (e.g., blocking AI bots dynamically):
function robots(r) {
const ua = r.headersIn['User-Agent'] || "";
// Block specific bots dynamically
if (ua.includes("GPTBot") || ua.includes("CCBot")) {
r.return(200, "User-agent: *\nDisallow: /\n");
} else {
r.return(200, "User-agent: *\nAllow: /\n");
}
}
export default { robots };http {
js_import /etc/nginx/njs/virtual.js;
server {
location = /robots.txt {
js_content virtual.robots;
}
}
}Health and monitoring endpoints
Critical for container orchestration and observability.
Basic health check
location = /health {
access_log off; # Don't spam logs
default_type application/json;
return 200 '{"status":"healthy"}';
}Kubernetes Probes
# Liveness probe - is the process alive?
location = /healthz {
access_log off;
return 200 'OK';
}
# Readiness probe - is it ready to receive traffic?
location = /ready {
access_log off;
# Could check backend connectivity here
return 200 'Ready';
}
# Startup probe - has initialization completed?
location = /startup {
access_log off;
return 200 'Started';
}stub_status monitoring
Expose Nginx internal metrics
(requires ngx_http_stub_status_module):
location = /nginx_status {
stub_status on;
access_log off;
# Restrict access to internal networks
allow 127.0.0.1;
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 192.168.0.0/16;
deny all;
}Active connections: 42 server accepts handled requests 12345 12345 98765 Reading: 0 Writing: 3 Waiting: 39
Performance optimization
Maximize efficiency when serving virtual files.
File serving optimization
http {
# Zero-copy file transfers
sendfile on;
# Optimize packet sending
tcp_nopush on;
tcp_nodelay on;
# Keep connections alive
keepalive_timeout 65;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 1000;
}Caching headers
# Static assets - cache aggressively
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Virtual files - short cache or no cache
location = /robots.txt {
expires 1d;
add_header Cache-Control "public";
return 200 "...";
}
# Health endpoints - never cache
location = /health {
add_header Cache-Control "no-store" always;
return 200 '{"status":"ok"}';
}Deployment patterns
Real-world configurations for modern infrastructure.
Docker container pattern
upstream app {
server app:3000;
}
server {
listen 80;
# Virtual files served by Nginx
location = /robots.txt {
default_type text/plain;
return 200 "User-agent: *\nAllow: /\n";
}
location = /health {
access_log off;
return 200 'OK';
}
# Static assets from volume
location /static/ {
alias /var/www/static/;
expires 1y;
}
# Everything else to app container
location / {
proxy_pass http://app;
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;
}
}Kubernetes ingress pattern
server {
listen 80;
# Probes for K8s
location = /healthz { return 200 'OK'; access_log off; }
location = /ready { return 200 'Ready'; access_log off; }
# Standard virtual files
location = /robots.txt {
default_type text/plain;
return 200 "User-agent: *\nDisallow: /api/internal/\n";
}
# ACME for cert-manager (^~ prevents regex override)
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# Service routing
location /api/ {
proxy_pass http://api-service:8080;
}
location / {
proxy_pass http://frontend-service:3000;
}
}Complete example configuration
A production-ready configuration combining all techniques:
# Rate limiting zone (MUST be placed in http { } context, not inside server { } or location { })
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
server {
listen 443 ssl;
http2 on;
server_name example.com;
# SSL configuration
ssl_certificate /etc/ssl/certs/example.com.crt;
ssl_certificate_key /etc/ssl/private/example.com.key;
# Performance
sendfile on;
tcp_nopush on;
# =========================================
# VIRTUAL FILES
# =========================================
# SEO / Crawlers
location = /robots.txt {
default_type text/plain;
return 200 "User-agent: *\nAllow: /\nDisallow: /api/internal/\nSitemap: https://example.com/sitemap.xml\n";
}
# Security
location = /.well-known/security.txt {
default_type text/plain;
return 200 "Contact: mailto:security@example.com\nExpires: 2027-12-31T23:59:59.000Z\n";
}
# Health & Monitoring
# Note: Nginx variables like $date_gmt are NOT interpolated in return body.
# For dynamic values, use add_header or a backend.
location = /health {
access_log off;
default_type application/json;
add_header X-Timestamp $date_gmt always;
return 200 '{"status":"healthy"}';
}
location = /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}
# ACME Challenges
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# Mobile Apps
location = /.well-known/apple-app-site-association {
alias /etc/nginx/assets/apple-app-site-association;
default_type application/json;
}
location = /.well-known/assetlinks.json {
alias /etc/nginx/assets/assetlinks.json;
default_type application/json;
}
# Favicon
location = /favicon.ico {
alias /var/www/static/favicon.ico;
access_log off;
expires 1y;
}
# =========================================
# STATIC ASSETS
# =========================================
location ^~ /static/ {
alias /var/www/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}
# =========================================
# APPLICATION
# =========================================
location / {
limit_req zone=general burst=20 nodelay;
proxy_pass http://app:3000;
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";
}
}
# HTTP to HTTPS redirect
server {
listen 80;
server_name example.com;
# Allow ACME on HTTP
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$server_name$request_uri;
}
}Troubleshooting
Testing virtual files
Validate configuration
| Issue | Cause | Solution |
|---|---|---|
| 404 for virtual file | Location not matching | Check location modifier (= vs prefix) |
| Wrong content served | Another location matching first | Review priority; use = for exact |
| Wrong Content-Type | Missing type header | Add default_type or add_header |
| alias path issues | Missing trailing slash | Ensure both location and alias have trailing / |
Beyond static files: scripting modules
Sometimes you need more than just static strings. Here is how the main scripting options compare for 2025:
| Language | Performance | Use Case | Recommendation |
|---|---|---|---|
| njs (JavaScript) | High | Dynamic content, header manipulation, complex routing. | Recommended for most users. Native integration. |
| Lua (OpenResty) | Very High (JIT) | High-load API gateways, complex business logic, caching. | Best for heavy-duty apps. Requires OpenResty build. |
| Perl | Medium | Legacy integration, text processing. | Avoid unless maintaining legacy systems. |
Deep Dive
Official Documentation
- Nginx Beginner’s Guide
- ngx_http_core_module —
location,root,alias - ngx_http_rewrite_module —
return,rewrite