Back to Blog
José Manuel Requena Plens

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.

Cover image for Mastering Virtual Files in Nginx: A Complete Guide

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:

Location Block Priority (Highest to Lowest)
PriorityModifierTypeExample
1=Exact matchlocation = /robots.txt
2^~Prefix (stops regex search)location ^~ /static/
3~Case-sensitive regexlocation ~ \.php$
4~*Case-insensitive regexlocation ~* \.(jpg|png)$
5(none)Prefix (longest match)location /api/

Practical example

Consider this configuration:

location-priority.conf
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

root vs alias vs try_files
DirectiveBehaviorBest For
rootAppends URI to pathStandard document roots
aliasReplaces matched prefix with pathServing files from non-standard locations
try_filesChecks files in order, falls back to lastSPAs, conditional file serving

Visual comparison

root (appends URI)
location /images/ {
    root /var/www;
}

# Request: /images/photo.jpg
# Serves:  /var/www/images/photo.jpg
#          ^^^^^^^^ root + URI
alias (replaces prefix)
location /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:

try_files example
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:

  1. $uri — Try the exact file path
  2. $uri/ — Try as a directory with index
  3. @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

return Directive Patterns
PatternDescriptionExample
return CODE;Return status onlyreturn 204;
return CODE TEXT;Return status with bodyreturn 200 “OK”;
return CODE URL;Redirect (301, 302, 307, 308)return 301 https://…;

Inline content example

return examples
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

named-location.conf
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:

spa-fallback.conf
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:

internal-location.conf
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:

humans.txt location
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.

ads.txt location
# 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.

favicon handling
# 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).

mobile-icons.conf
# 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:

security.txt
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-challenge.conf
# 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:

mobile-associations.conf
# 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

Common .well-known URIs
PathPurposeContent-Type
/.well-known/change-passwordRedirect to password change pageRedirect (302)
/.well-known/webfingerUser discovery (ActivityPub, email)application/jrd+json
/.well-known/openid-configurationOpenID Connect discoveryapplication/json
/.well-known/matrix/clientMatrix client discoveryapplication/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):

/​etc/​nginx/​njs/​virtual.js
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 };
njs-location.conf
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

health-endpoint.conf
location = /health {
    access_log off;  # Don't spam logs
    default_type application/json;
    return 200 '{"status":"healthy"}';
}

Kubernetes Probes

k8s-probes.conf
# 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):

stub_status.conf
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

performance.conf
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

caching.conf
# 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

docker-nginx.conf
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

k8s-ingress-nginx.conf
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:

/​etc/​nginx/​sites-available/​example.com.conf
# 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

# Test exact match curl -v https://example.com/robots.txt # Check response headers curl -I https://example.com/health # Follow redirects curl -L https://example.com/old-page

Validate configuration

# Syntax check sudo nginx -t # Reload configuration sudo nginx -s reload # Check error logs tail -f /var/log/nginx/error.log
Common Issues
IssueCauseSolution
404 for virtual fileLocation not matchingCheck location modifier (= vs prefix)
Wrong content servedAnother location matching firstReview priority; use = for exact
Wrong Content-TypeMissing type headerAdd default_type or add_header
alias path issuesMissing trailing slashEnsure 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:

Nginx Scripting Options Comparison
LanguagePerformanceUse CaseRecommendation
njs (JavaScript)HighDynamic 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.
PerlMediumLegacy integration, text processing.Avoid unless maintaining legacy systems.

Deep Dive

Official Documentation

Tutorials

Standards