Back to posts
5 min read
The Case of the Phantom 404: Debugging Nginx in WSL2

Picture this: you’ve got a fresh PHP project ready to go. You’ve set up what looks like a perfectly reasonable nginx config. You type the URL into your browser, hit enter, and… 404 Not Found.

Sound familiar? Join me on a debugging journey that turned into a masterclass in WSL2 networking.

The Problem

Here’s what my initial nginx config looked like:

server {
    listen 80;
    server_name my_project.loc;
    location / {
        root /var/www/html/my-project/src;
        index index.html index.htm index.php;
    }
}

Looks fine, right? Wrong.

The site loaded, but every PHP file returned that dreaded 404. Static files worked perfectly, but PHP was being treated like any other file, served as plain text rather than executed.

Fix #1: PHP Wasn’t Being Processed

The Issue: Nginx is brilliant at serving static files, but it doesn’t magically know what to do with .php files. Without explicit instructions, it just serves them as text files.

The Fix: We need to tell nginx to hand off PHP files to PHP-FPM for processing:

server {
    listen 80;
    server_name my_project.loc;
   
    root /var/www/html/my-project/src;
    index index.php index.html index.htm;
   
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
   
    # This is the critical part!
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

The key additions:

  • location ~ \.php$ block to handle PHP files
  • fastcgi_pass to communicate with PHP-FPM via Unix socket
  • try_files directive for pretty URLs

Fix #2: The Mysterious WSL2 Localhost Issue

Great! Added the PHP-FPM config, restarted nginx, and still getting 404 from my Windows browser.

But here’s where it gets weird: curl http://my_project.loc from the WSL terminal worked perfectly. The site was running fine inside WSL, but Windows couldn’t see it.

First Attempt: Missing Hosts Entry

I checked the WSL hosts file:

# WSL /etc/hosts
127.0.0.1 my_project.loc

Present and correct. Still 404 from Windows. Why?

The WSL2 Networking Gotcha

Here’s the revelation: WSL2 and Windows are two different machines on the same network. When you use 127.0.0.1 in Windows, you’re referring to Windows localhost, not WSL2 localhost.

The solution: Windows needs the domain in its own hosts file pointing to the WSL2 IP:

# Windows C:\Windows\System32\drivers\etc\hosts
xxx.xx.xx.xxx my_project.loc

Find your WSL2 IP with:

ip addr show eth0 | grep "inet " | awk '{print $2}' | cut -d/ -f1

Success! The site finally worked from Windows. But this raised another question…

Fix #3: Why Do My Other Sites Use 127.0.0.1?

All my other projects worked perfectly with 127.0.0.1 in the Windows hosts file:

127.0.0.1 my_shop.loc
127.0.0.1 mdev.loc

Why did my_project.loc need special treatment?

The SSL Discovery

I checked the working configs and found the difference:

server {
    listen 80;
    listen 443 ssl;  # ← This is the difference!
    server_name my_shop.loc;
   
    ssl_certificate /etc/nginx/ssl/nginx-selfsigned.crt;
    ssl_certificate_key /etc/nginx/ssl/nginx-selfsigned.key;
    # ... rest of config
}

WSL2 networking quirk: When nginx listens on port 443 (SSL), Windows can access it via 127.0.0.1. Without SSL, you need the actual WSL2 IP address.

Final Config

Here’s the complete, working configuration:

server {
    listen 80;
    listen 443 ssl;
    server_name my_project.loc;

    ssl_certificate /etc/nginx/ssl/nginx-selfsigned.crt;
    ssl_certificate_key /etc/nginx/ssl/nginx-selfsigned.key;
   
    root /var/www/html/my-project/src;
    index index.php index.html index.htm;
   
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
   
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
   
    location ~ /\.ht {
        deny all;
    }
}

Now the Windows hosts file could use:

127.0.0.1 my_project.loc

Everything worked perfectly!

Bonus: Expired SSL Certificate

After getting everything working, Chrome greeted me with:

NET::ERR_CERT_DATE_INVALID

The self-signed certificate had expired. Regenerate it with:

sudo openssl req -x509 -nodes -days 365 \
  -newkey rsa:2048 \
  -keyout /etc/nginx/ssl/nginx-selfsigned.key \
  -out /etc/nginx/ssl/nginx-selfsigned.crt \
  -subj "/C=US/ST=State/L=City/O=Organization/CN=localhost"

sudo service nginx restart

Verify the expiration date:

openssl x509 -in /etc/nginx/ssl/nginx-selfsigned.crt -noout -dates

Quick Reference

Complete Setup Script

# 1. Add to WSL hosts
echo "127.0.0.1 my_project.loc" | sudo tee -a /etc/hosts

# 2. Add to Windows hosts (run in PowerShell as Admin)
Add-Content C:\Windows\System32\drivers\etc\hosts "127.0.0.1 my_project.loc"

# 3. Create nginx config
sudo nano /etc/nginx/sites-available/my_project.loc
# Paste the final config above

# 4. Enable site
sudo ln -sf /etc/nginx/sites-available/my_project.loc /etc/nginx/sites-enabled/

# 5. Test and reload
sudo nginx -t
sudo service nginx restart

Debugging Checklist

  • PHP-FPM running? sudo service php8.1-fpm status
  • PHP socket exists? ls -la /var/run/php/php8.1-fpm.sock
  • Nginx syntax valid? sudo nginx -t
  • Domain in /etc/hosts? grep my_project /etc/hosts
  • Domain in Windows hosts? Check C:\Windows\System32\drivers\etc\hosts
  • Listening on 443? sudo ss -tlnp | grep nginx | grep :443
  • Check logs: tail -f /var/log/nginx/my_project.log

Key Takeaways

  1. Nginx doesn’t process PHP - You must explicitly configure PHP-FPM handling
  2. WSL2 + Windows = Two hosts files - Both environments need domain entries
  3. SSL enables 127.0.0.1 in WSL2 - Without SSL, use the WSL2 IP in Windows hosts

Sometimes the most frustrating debugging sessions teach you the most valuable lessons. This 404 error turned into a deep dive that clarified how WSL2 networking actually works.