Skip to content
DevOps devops webservers 5 min read

Running Nginx in Front of Apache

Sometimes you don’t have to pick between Nginx and Apache — you run both. A very common production pattern puts Nginx (a fast, lightweight web server) at the front on ports 80 and 443, handling TLS (Transport Layer Security, the encryption behind HTTPS) and serving static files, while Apache (a flexible, older web server) sits quietly behind it on port 8080 doing the actual application work. This page explains how that hybrid setup works, why teams build it, and exactly how to wire it up on Ubuntu — including the important port change Apache needs.

What the pattern actually is

In this setup, Nginx acts as a reverse proxy (a server that sits in front of your app and forwards incoming requests to it). The flow looks like this:

  1. A browser connects to your server on port 443 (HTTPS) or 80 (HTTP).
  2. Nginx terminates TLS — meaning it does the decryption work, so the browser talks encrypted HTTPS to Nginx.
  3. Nginx serves any static files (images, CSS, JavaScript) directly, because it is very fast at that.
  4. For dynamic requests (anything your application code must generate), Nginx forwards them to Apache, which is now listening on the internal port 8080.
  5. Apache runs your application (for example a PHP app using mod_php, or a legacy app with lots of .htaccess rules), generates the response, and hands it back to Nginx, which passes it to the browser.

The key idea: only Nginx is exposed to the public internet. Apache is hidden on 127.0.0.1:8080 (localhost only) and never talks to the outside world directly.

Why teams do this

GoalWhy the hybrid helps
Fast static deliveryNginx serves static files with far less memory per connection than Apache.
Keep .htaccess and modulesMany legacy and PHP apps rely on Apache’s .htaccess rules or modules like mod_rewrite; rewriting them for Nginx is risky.
Single TLS endpointYou configure HTTPS certificates once, in Nginx, instead of in every app.
Handle slow clientsNginx buffers slow connections so Apache’s heavier worker processes aren’t tied up waiting.
Gradual migrationYou can move routes from Apache to Nginx one at a time without a big-bang rewrite.

When NOT to do this: If you’re building a brand-new app and don’t depend on Apache modules or .htaccess, skip Apache entirely. Running two web servers means two configs, two sets of logs, and more to break. Use the hybrid only when Apache earns its place.

Step 1 — Change Apache’s port to 8080

By default both Nginx and Apache try to grab port 80. Only one program can hold a port at a time, so we move Apache to 8080 and bind it to localhost only.

Edit Apache’s ports file:

sudo nano /etc/apache2/ports.conf

Change the Listen line so Apache only listens on localhost port 8080:

# /etc/apache2/ports.conf
Listen 127.0.0.1:8080

Now update the default virtual host (a virtual host is one website definition inside Apache) to match the new port. Edit /etc/apache2/sites-available/000-default.conf:

# /etc/apache2/sites-available/000-default.conf
<VirtualHost 127.0.0.1:8080>
    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/html

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

Test the Apache config and restart it:

sudo apache2ctl configtest
sudo systemctl restart apache2

Output:

Syntax OK

Confirm Apache is now on 8080 and nothing else is on 80:

sudo ss -ltnp | grep -E ':80|:8080'

Output:

LISTEN 0  511  127.0.0.1:8080  0.0.0.0:*  users:(("apache2",pid=2411,fd=4))

Step 2 — Configure Nginx as the front

Now create an Nginx server block (Nginx’s term for one website config) that serves static files and proxies everything else to Apache. Create /etc/nginx/sites-available/example.com:

# /etc/nginx/sites-available/example.com
server {
    listen 80;
    server_name example.com www.example.com;

    root /var/www/html;
    index index.php index.html;

    # Serve static assets directly from Nginx — fast, no Apache needed
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2|svg)$ {
        expires 30d;
        access_log off;
        try_files $uri =404;
    }

    # Everything else goes to Apache on 8080
    location / {
        proxy_pass http://127.0.0.1:8080;
        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;
    }
}

Those proxy_set_header lines matter. Without X-Forwarded-For, Apache’s logs would record every visitor’s IP as 127.0.0.1 (Nginx’s address) instead of the real client. And X-Forwarded-Proto $scheme tells Apache whether the original request was HTTP or HTTPS, which apps need to build correct links.

Enable the site, test, and reload:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Output:

nginx: the configuration file /etc/nginx/conf.d/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/conf.d/nginx.conf test is successful

Step 3 — Lock down the firewall

Only Nginx should be reachable from outside. Use ufw (Uncomplicated Firewall, Ubuntu’s simple firewall tool) to allow web traffic and SSH only:

sudo ufw allow 'Nginx Full'
sudo ufw allow OpenSSH
sudo ufw enable
sudo ufw status

Output:

To                         Action      From
--                         ------      ----
Nginx Full                 ALLOW       Anywhere
OpenSSH                    ALLOW       Anywhere

Because Apache is bound to 127.0.0.1:8080, it isn’t reachable from the internet even without a firewall rule — but keeping ufw tight is good defense in depth.

Gotcha: Make Apache record real client IPs by enabling the remoteip module: sudo a2enmod remoteip, then add RemoteIPHeader X-Forwarded-For to your Apache config and reload. Otherwise your access logs and any IP-based blocking inside the app are useless.

When this is worth it vs. not

SituationRecommendation
Legacy PHP app with heavy .htaccessUse the hybrid — keep Apache, front it with Nginx.
High static-file traffic but app needs ApacheHybrid wins; Nginx offloads the static load.
Brand-new app, no Apache dependencySkip Apache; use Nginx + PHP-FPM or a Node/Python backend.
Tiny low-traffic siteJust use one server; the hybrid is overkill.

Best Practices

  • Bind Apache to 127.0.0.1:8080 only — never expose the back end to the public internet.
  • Terminate TLS once, in Nginx, and add HTTPS with Certbot rather than configuring certs in Apache too.
  • Always forward X-Real-IP, X-Forwarded-For, and X-Forwarded-Proto, and enable Apache’s remoteip module to log real visitors.
  • Let Nginx serve static assets directly so Apache only handles dynamic requests.
  • Keep ufw restricted to Nginx and SSH; rely on localhost binding as a second layer.
  • Run nginx -t and apache2ctl configtest before every reload so a typo never takes the site down.
Last updated June 15, 2026
Was this helpful?