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:
- A browser connects to your server on port 443 (HTTPS) or 80 (HTTP).
- Nginx terminates TLS — meaning it does the decryption work, so the browser talks encrypted HTTPS to Nginx.
- Nginx serves any static files (images, CSS, JavaScript) directly, because it is very fast at that.
- For dynamic requests (anything your application code must generate), Nginx forwards them to Apache, which is now listening on the internal port 8080.
- Apache runs your application (for example a PHP app using
mod_php, or a legacy app with lots of.htaccessrules), 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
| Goal | Why the hybrid helps |
|---|---|
| Fast static delivery | Nginx serves static files with far less memory per connection than Apache. |
Keep .htaccess and modules | Many legacy and PHP apps rely on Apache’s .htaccess rules or modules like mod_rewrite; rewriting them for Nginx is risky. |
| Single TLS endpoint | You configure HTTPS certificates once, in Nginx, instead of in every app. |
| Handle slow clients | Nginx buffers slow connections so Apache’s heavier worker processes aren’t tied up waiting. |
| Gradual migration | You 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
remoteipmodule:sudo a2enmod remoteip, then addRemoteIPHeader X-Forwarded-Forto 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
| Situation | Recommendation |
|---|---|
Legacy PHP app with heavy .htaccess | Use the hybrid — keep Apache, front it with Nginx. |
| High static-file traffic but app needs Apache | Hybrid wins; Nginx offloads the static load. |
| Brand-new app, no Apache dependency | Skip Apache; use Nginx + PHP-FPM or a Node/Python backend. |
| Tiny low-traffic site | Just use one server; the hybrid is overkill. |
Best Practices
- Bind Apache to
127.0.0.1:8080only — 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, andX-Forwarded-Proto, and enable Apache’sremoteipmodule to log real visitors. - Let Nginx serve static assets directly so Apache only handles dynamic requests.
- Keep
ufwrestricted to Nginx and SSH; rely on localhost binding as a second layer. - Run
nginx -tandapache2ctl configtestbefore every reload so a typo never takes the site down.