Nginx gzip_static Behavior: Why Uncompressed Files Are Required Despite Pre-compressed Versions


2 views

When implementing gzip_static on in Nginx configurations, many developers encounter a puzzling requirement: the uncompressed version of a file must exist alongside its .gz counterpart, even when you explicitly want to serve only pre-compressed content.

Nginx's standard behavior with gzip_static on requires both file versions because:

  • It performs client capability checking (Accept-Encoding headers)
  • Needs fallback for clients that don't support gzip
  • Validates file existence before serving compressed content

Here's a typical working configuration:

location /assets/ {
  gzip_static on;
  gunzip on;
  try_files $uri =404;
}

Despite documentation suggesting gzip_static always should work without uncompressed files, in practice it often doesn't. This is because:

location /static/ {
  gzip_static always;  # Should work without uncompressed files
  gunzip on;           # Required for decompression when needed
  try_files $uri.gz =404;  # Alternative approach
}

If you absolutely need to serve only .gz files:

Option 1: Modify try_files directive

location ~* \.(css|js)$ {
  try_files $uri.gz @nogzip;
  gzip_static on;
}

location @nogzip {
  # Handle cases where .gz doesn't exist
}

Option 2: Use a map directive

map $uri $gzip_uri {
  default $uri.gz;
}

server {
  location / {
    try_files $gzip_uri $uri @fallback;
  }
}

While maintaining both compressed and uncompressed versions consumes disk space, the benefits include:

  • Automatic client capability handling
  • Simplified deployment pipelines
  • Better compatibility with CDN edge cases

When troubleshooting:

# Check file permissions:
ls -la /path/to/assets/

# Verify Nginx can see both files:
sudo -u www-data ls /path/to/assets/

# Test with curl:
curl -I -H "Accept-Encoding: gzip" http://yoursite.com/assets/file.css
curl -I http://yoursite.com/assets/file.css

When configuring Nginx's gzip_static module (particularly in version 1.4.4), many developers encounter a puzzling requirement: the server insists on having both compressed (.gz) and uncompressed versions of static files available, even when we clearly want to serve only pre-compressed assets.

Here's a typical working setup where both file versions exist:

http {
  gzip on;
  gzip_http_version 1.0;
  gzip_proxied any;
  
  server {
    location ^~ /assets/ {
      gzip_static on;
      expires max;
      add_header Cache-Control public;
      try_files $uri =404;
    }
  }
}

When testing with these files:

/assets/app-abc123.css      # Dummy content
/assets/app-abc123.css.gz   # Real compressed CSS

Nginx correctly serves the compressed version when gzip_static on. However, removing the uncompressed version causes 404 errors, contrary to what some might expect from the documentation.

The gzip_static module actually performs these checks in order:

  1. Checks if client accepts gzip (Accept-Encoding header)
  2. Verifies existence of .gz version
  3. Also verifies existence of original file

For cases where you truly want only .gz files:

location ^~ /assets/ {
  gzip_static always;  # Bypass client capability check
  gunzip on;           # For clients that don't support gzip
  try_files $uri.gz =404;  # Explicitly look for .gz files
}

In real-world deployments, maintaining both versions offers these advantages:

  • Fallback for non-gzip clients
  • Easier debugging without compression
  • Consistent behavior with other static files

For modern frontend builds, consider this pattern:

# During deployment:
find /assets/ -name '*.gz' | while read f; do
  orig=${f%.gz}
  touch "$orig"  # Create empty original file
  chmod 644 "$orig"
done