Resolving Nginx Caching Issues with Symlinked Deployment Directories: PHP Script Execution Problems


2 views

Many deployment systems use timestamped directories with symlinks (like a "current" pointer) for atomic deployments. While this works perfectly with Apache, Nginx handles symlinks differently due to its caching behavior. Here's what's happening in your case:

/var/www/example.com/
├── 20230815-143000/  # Old deployment
│   └── public/
│       └── index.php
├── 20230816-093000/  # New deployment
│   └── public/
│       └── index.php
└── current -> 20230815-143000  # Should point to 20230816-093000 after deployment

Nginx caches the realpath of files when they're first accessed. Even after you update the "current" symlink, Nginx may continue serving files from the old directory. This happens because:

  • Nginx resolves symlinks during the first request
  • The resolved path gets cached for performance
  • PHP-FPM receives the cached path from Nginx

Here's the fixed server configuration with key modifications:

server {
    listen 80;
    server_name ~^(www\.)?(?.+?).testing.domain.com$;
    
    # Critical change: Use $realpath_root instead of $document_root
    root /var/www/$sname/current/public;
    
    index index.html index.htm index.php;

    location / {
        try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~* \.(jpg|jpeg|gif|png|bmp|ico|pdf|flv|swf|exe|html|htm|txt|css|js)$ {
        add_header Cache-Control "public, must-revalidate";
        expires 7d;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
        include fastcgi_params;
        
        # These are the crucial parameters for symlink handling:
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        
        # Recommended additional parameters:
        fastcgi_param PATH_TRANSLATED $realpath_root$fastcgi_script_name;
        fastcgi_param SCRIPT_NAME $fastcgi_script_name;
    }
}

For complete symlink handling, add this to your nginx.conf's http block:

http {
    # Disable symlink caching
    disable_symlinks off;
    
    # Forces Nginx to check symlinks on each request (performance impact)
    open_file_cache_valid 0;
    
    # Or alternatively, set a short cache time
    open_file_cache_valid 1s;
}

When using symlink deployments with Nginx:

  1. Always perform a "warmup" request after deployment
  2. Consider using nginx -s reload after major deployments
  3. For zero-downtime, use this sequence:
# 1. Deploy to new directory
rsync -az ./ new_release/

# 2. Atomically switch symlink
ln -sfn new_release current

# 3. Warm up the new deployment
curl -I http://localhost/ >/dev/null 2>&1

# 4. Optional: Cleanup old releases
find /var/www/ -maxdepth 1 -type d -mtime +7 | xargs rm -rf

To confirm the fix is working:

# Check which PHP file is actually being executed
location ~ \.php$ {
    add_header X-File-Path $realpath_root$fastcgi_script_name;
    # ... rest of config
}

Then inspect response headers for the X-File-Path value after deployment.


Many modern deployment systems use timestamped directories with symlinked "current" pointers for atomic deployments. While this works seamlessly with Apache, Nginx requires special configuration to properly handle symlink changes without serving stale content.

The issue stems from Nginx's handling of $realpath_root and $document_root variables. When PHP-FPM processes requests, it may cache the resolved path of the symlink rather than re-evaluating it on each request.

Here's the corrected server block configuration that properly handles symlink changes:

server {
    listen 80;
    server_name ~^(www\.)?(?.+?).testing.domain.com$;

    # Critical change: Use $realpath_root instead of $document_root
    root /var/www/$sname/current/public;
    
    index index.html index.htm index.php;

    location / {
        try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
        include fastcgi_params;
        
        # These are the crucial parameters for symlink resolution
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        fastcgi_param PATH_TRANSLATED $realpath_root$fastcgi_script_name;
    }
}

For complete symlink handling, consider these additional measures:

# Disable opcache for development (optional)
opcache.enable=0

# Or configure proper revalidation in production
opcache.revalidate_freq=0
opcache.validate_timestamps=1

After implementing these changes:

# Check symlink resolution
ls -la /var/www/your_app/current

# Reload Nginx and PHP-FPM
sudo service nginx reload
sudo service php7.1-fpm reload

# Clear any existing cache
find /var/www/your_app -type f -name ".user.ini" -exec rm {} \;

While this solution ensures proper symlink handling, be aware that using $realpath_root has a minor performance impact as Nginx must resolve the symlink for each request. In high-traffic environments, consider:

  • Implementing proper cache headers
  • Using Nginx's open_file_cache directive
  • Optimizing PHP opcache settings