"ProxyPass not allowed here": .htaccess vs virtual host in Apache
Updated 4 November 2026 · By Brent Alexis
Short answer: Apache logs ProxyPass not allowed here and returns a 500 error because ProxyPass only works in the main server config or a <VirtualHost>, never in .htaccess, even with AllowOverride All. Move the proxy lines into a virtual host file (tested templates below). If you're on shared hosting and can't edit the server config, use mod_rewrite's [P] flag in .htaccess instead, the one form of proxying .htaccess allows.
Every error message and template on this page was checked on Apache 2.4.68 with a real backend and PHP-FPM 8.3. How it's tested
- What you'll see
- Why .htaccess can't do it
- Wrong place, missing module, or ignored file?
- Fix 1: move it into a virtual host
- Fix 2: shared hosting, the [P] flag
- Tested virtual host templates
- Test before you reload
What you'll see
The browser shows 500 Internal Server Error on every page in that folder, and Apache's error log has a line like this:
[core:alert] ... /var/www/example.com/.htaccess: ProxyPass not allowed here
The catch: apachectl configtest still says Syntax OK. It only checks the main config files and never reads .htaccess, so the problem only shows up when a real request hits the folder.
Why .htaccess can't do it
A .htaccess file is per-directory config. Apache reads it on each request, after it has already decided which site the request is for, accepted the connection and set up HTTPS. Anything that controls those earlier steps, including where whole requests get proxied to, can only live in the server config or a virtual host.
These all fail in .htaccess with the same "not allowed here" error:
| Directive | What it does | Where it goes |
|---|---|---|
ProxyPass, ProxyPassReverse | Send requests to a backend app and fix its redirects | Virtual host |
ProxyPreserveHost | Pass the visitor's Host header to the app | Virtual host |
SSLEngine and the certificate lines | Turn on HTTPS | Virtual host on port 443 |
ServerName | Bind the site to your domain | Virtual host |
Protocols h2 http/1.1 | Turn on HTTP/2 | Virtual host or main config |
Listen | Open a port | Main config only |
What .htaccess can do, as long as AllowOverride lets it: redirects (Redirect, RewriteRule), headers, access rules, index files, custom error pages and the usual WordPress permalink rules.
Wrong place, missing module, or ignored file?
Three different symptoms look alike at first. Check which one you have:
ProxyPass not allowed here- Wrong place. The directive is in
.htaccess. Use one of the fixes below. Invalid command 'ProxyPass', perhaps misspelled or defined by a module not included in the server configuration- Missing module. mod_proxy isn't loaded, so Apache doesn't know the directive at all (the name in quotes is whichever proxy directive comes first, such as
ProxyPreserveHost). On Debian/Ubuntu runsudo a2enmod proxy proxy_http; on RHEL, Alma and Rocky they're normally loaded already fromconf.modules.d/00-proxy.conf. Load them together: mod_proxy_http without mod_proxy stops Apache withCannot load modules/mod_proxy_http.so ... undefined symbol. - No error, nothing happens
- Ignored file. The folder has
AllowOverride None, so Apache never reads the.htaccessat all. That's the default on many installs.
Fix 1: move it into a virtual host
If you can edit the server config (a VPS or your own server), this is the proper fix:
- Delete the proxy lines from
.htaccess. - Create the site file:
/etc/apache2/sites-available/example.com.confon Debian/Ubuntu, or/etc/httpd/conf.d/example.com.confon RHEL, Alma and Rocky. - Paste the reverse proxy template below and change the domain and backend address.
- Debian/Ubuntu only:
sudo a2enmod proxy proxy_http headers rewrite ssl http2thensudo a2ensite example.com. - Test, then reload.
Or fill in your own details in the Config Generator's Apache virtual host mode, which writes this file for you.
Fix 2: shared hosting, the [P] flag
If .htaccess is all you have, mod_rewrite's proxy flag is the one way to proxy from it:
RewriteEngine On
RewriteRule ^/?(.*) http://127.0.0.1:3000/$1 [P,L]
It works, with two catches:
- Your app sees the backend's address as the Host (
127.0.0.1:3000), not your domain, becauseProxyPreserveHostisn't allowed here. The original domain is still sent in theX-Forwarded-Hostheader, which most frameworks can be told to trust. - There's no
ProxyPassReverse, so if your app sends a redirect with its own address, the visitor can be sent to127.0.0.1:3000. Set your app's public URL to your real domain to avoid it.
It also needs mod_proxy, mod_proxy_http and mod_rewrite enabled on the server. If the rule gives a 500 or 404, ask your host whether they allow it. The Config Generator in .htaccess mode writes this version for you.
Tested virtual host templates
Replace example.com, the paths and the backend address with yours. Each one assumes a Let's Encrypt certificate already exists at the path shown (the SSL Generator gives you the Certbot command) and keeps renewals working on plain HTTP.
Reverse proxy (Node, Python, Docker app) with HTTPS
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
# Let's Encrypt renewals (certbot --webroot -w /var/www/html) stay on plain HTTP
DocumentRoot /var/www/html
<Directory /var/www/html/.well-known/acme-challenge>
Require all granted
</Directory>
# Everything else goes to HTTPS
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>
<VirtualHost *:443>
ServerName example.com
ServerAlias www.example.com
Protocols h2 http/1.1
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto "https"
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
</VirtualHost>
Checked: HTTP redirects to HTTPS, the Let's Encrypt check file is still served over HTTP, requests reach the app with their path and query string, the app sees your domain as Host and X-Forwarded-Proto: https, and HTTPS uses HTTP/2. Modules: proxy, proxy_http, headers, rewrite, ssl, http2.
Static site with HTTPS
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/example.com
<Directory /var/www/example.com/.well-known/acme-challenge>
Require all granted
</Directory>
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>
<VirtualHost *:443>
ServerName example.com
ServerAlias www.example.com
Protocols h2 http/1.1
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
DocumentRoot /var/www/example.com
<Directory /var/www/example.com>
Options -Indexes
AllowOverride None
Require all granted
</Directory>
DirectoryIndex index.html
</VirtualHost>
Checked: HTTP redirects to HTTPS, renewals still work, the site is served over HTTPS, and folder listings are off (403). Modules: rewrite, ssl, http2.
WordPress or any PHP-FPM app with HTTPS
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/example.com
<Directory /var/www/example.com/.well-known/acme-challenge>
Require all granted
</Directory>
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>
<VirtualHost *:443>
ServerName example.com
ServerAlias www.example.com
Protocols h2 http/1.1
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
DocumentRoot /var/www/example.com
<Directory /var/www/example.com>
Options -Indexes +FollowSymLinks
# WordPress writes its own .htaccess for permalinks; these rules work until it does
AllowOverride All
Require all granted
RewriteEngine On
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</Directory>
DirectoryIndex index.php index.html
# PHP-FPM socket: Debian/Ubuntu /run/php/php8.3-fpm.sock, RHEL/Alma/Rocky /run/php-fpm/www.sock
<FilesMatch \.php$>
SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost/"
</FilesMatch>
</VirtualHost>
Checked end to end with PHP-FPM 8.3 over TCP (SetHandler "proxy:fcgi://127.0.0.1:9000"): PHP runs, pretty permalinks like /about/ reach index.php, and real files are still served directly. The socket form above passed apachectl configtest. Modules: proxy, proxy_fcgi, rewrite, ssl, http2.
Test before you reload
For a virtual host, test first and reload only if the test passes:
sudo apachectl configtest && sudo systemctl reload apache2 # httpd on RHEL/Alma/Rocky
For .htaccess there's nothing to reload and configtest won't check it. Load a page straight away, and if you get a 500, the error log names the line Apache rejected. More errors and their fixes are in the FAQ.
Generate your Apache virtual host →
How to use ProxyForge · FAQ: common errors · Guides · What's new · About · Privacy