Proxy sites

A proxy site gives a dev server a real hostname. Your Vite, Next.js, Rails or Bun process keeps running on localhost:3000 exactly as it does now; Apache sits in front of it and answers for myapp.test, on 80 and — if you want it — on 443.

This is what you want whenever the thing serving your project is a process rather than a folder.

Why bother

Cookies and storage behave. localhost:3000 and localhost:5173 share an origin for some purposes and not others. Separate hostnames make each project a real, separate origin.

HTTPS without touching your dev server. Apache terminates TLS and forwards plain HTTP to your process. Nothing in your project needs a certificate, a proxy flag or a --experimental-https switch.

Secure-context APIs work. Service workers, the clipboard API, camera and microphone, and Secure cookies all want a secure origin. https://myapp.test is one.

OAuth callbacks and webhooks get a stable URL that does not change when a port does.

Creating one

Press New, set the ServerName, choose Proxy, and give it the port your dev server listens on. That is the whole form — no document root, because Apache never touches disk for this site.

Start order does not matter. Apache will return 503 for as long as nothing is listening on the port, and start forwarding the moment your dev server comes up.

What gets written

<VirtualHost *:80>
    ServerName myapp.test

    ProxyPreserveHost On
    ProxyRequests Off

    RewriteEngine On
    RewriteCond %{HTTP:Upgrade} websocket [NC]
    RewriteCond %{HTTP:Connection} upgrade [NC]
    RewriteRule ^/?(.*) "ws://localhost:3000/$1" [P,L]

    ProxyPass / http://localhost:3000/
    ProxyPassReverse / http://localhost:3000/

    ProxyTimeout 300

    LogLevel warn
    ErrorLog "/opt/homebrew/var/log/httpd/myapp_error.log"
    CustomLog "/opt/homebrew/var/log/httpd/myapp_access.log" common
</VirtualHost>

ProxyPreserveHost On forwards the original Host header, so your framework sees myapp.test and generates links with it rather than localhost.

The two RewriteCond lines catch WebSocket upgrades and hand them to mod_proxy_wstunnel, so hot module reload keeps working. This is the part people usually get wrong by hand.

ProxyTimeout 300 is five minutes, which is enough for a slow first compile or a long streaming response.

Modules it needs

Proxying needs four Apache modules: mod_proxy, mod_proxy_http, mod_proxy_wstunnel and mod_rewrite. The Apache panel shows which are loaded and turns the missing ones on — see Apache and services.

If a proxy site returns 500 and the error log mentions an invalid ProxyPass or an unknown RewriteRule, a module is commented out.

HTTPS in front of a proxy

Enabling HTTPS adds a :443 block that terminates TLS and forwards to the same port, with one addition:

RequestHeader set X-Forwarded-Proto "https"

Read that header in your framework so it knows to generate https:// URLs and to mark cookies Secure. Most frameworks need to be told to trust it — Laravel's TrustProxies, Express's trust proxy, Next.js behind a proxy, and so on.

The connection between Apache and your dev server stays plain HTTP on localhost, which is what you want: your dev server never needs a certificate.

When the upstream is missing

Vhostly watches for this specifically. If a proxy site is enabled and nothing is listening on its port, you get a warning naming the site and the port —

myapp.test forwards to localhost:3000, but nothing is listening there

It is a notification, not an error: most of the time it just means you have not started the dev server yet. Turn it off under Settings → Tell me when a proxy host points at a port nothing is listening on if you would rather not hear about it.

The Ports panel is where you check the other direction: what is actually holding a port right now. See ports.