Configure Apache as an HTTP and WebSocket reverse proxy
Proxy a private application through Apache with explicit timeouts, forwarded headers, WebSocket upgrades, health checks, and no accidental forward-proxy exposure.
Publish one upstream application safely while keeping the backend private and preserving client and scheme information.
- Apache HTTP Server 2.4.47+
- Ubuntu Server 24.04 LTS
- Console and recovery access Keep an independent console session and a copy of the current Apache configuration before changing listeners, modules, or access rules.
- DNS and network plan Resolve every production hostname to the intended public address and list the exact management and client networks that require access.
getent ahosts {{domain}} && ip route show - Port ownership Confirm that TCP port 443 is free or already belongs to the intended Apache instance.
sudo ss -lntp 'sport = :443' - Rollback artifact Snapshot the host or create an archive of the active configuration and web roots before the first material change.
OneLiners never runs these steps or stores secrets. Review placeholders, versions, current state, and change-control requirements before using a command.
Full guide
What you will build
- An Apache HTTPS virtual host that reverse-proxies ordinary HTTP requests and one explicit WebSocket path to a backend bound only to loopback.
- An idempotent, site-scoped proxy fragment with bounded connect/response timeouts, correct redirect rewriting, reviewed forwarded scheme/port context, and `ProxyRequests Off`.
- A failure-tested service boundary with direct and public health probes, evidence from both log layers, a fast backend-outage response, and a rollback that never exposes the private application listener.
- The backend answers directly on the intended private socket, Apache answers the public hostname, and only `/socket/` permits a WebSocket upgrade before the catch-all route.
- Apache cannot be used as an open forward proxy, the backend receives the intended host and scheme context, and upstream redirects return through the public origin.
- Stopping the controlled backend produces a quick observable 502/503 and recovery succeeds after restart without changing firewall exposure.
Architecture
How the parts fit together
Apache terminates public TLS, selects the named virtual host, and acts as a reverse proxy—not a general client-selected forward proxy. `ProxyPass` routes a narrowly defined WebSocket prefix first and the remaining root path second to a loopback-only backend. `ProxyPassReverse` rewrites selected upstream redirect headers. Apache supplies reviewed scheme and port metadata, while the application decides which proxy hops to trust. Explicit connection and response timeouts bound failure. Apache and application logs retain separate edge and execution evidence.
- A client resolves the public domain, negotiates TLS with Apache, and sends a request to the selected vhost.
- Apache rejects forward-proxy behavior and evaluates specific proxy mappings before the catch-all root mapping.
- For ordinary traffic, Apache opens an HTTP connection to loopback; for `/socket/`, it permits the reviewed WebSocket upgrade.
- The backend receives Host plus explicit scheme/port context, processes the request, and returns a response or redirect.
- Apache rewrites applicable redirect headers to the public origin, records edge evidence, and returns the result.
Assumptions
- Apache already serves valid HTTPS for `{{domain}}` and its virtual-host file is explicit, enabled, backed up, and free of unrelated tenants.
- The backend service is managed by `{{backendService}}`, binds only to `127.0.0.1:{{backendPort}}`, and exposes a non-secret `/health` response plus the documented `/socket/` endpoint.
- The application supports operation behind a reverse proxy and has a defined list of trusted proxy addresses; it does not blindly trust client-supplied forwarding headers.
- The public request-body, header, timeout, rate, authentication, and WebSocket-origin requirements are known before exposure.
- A controlled backend interruption is acceptable during the explicit failure rehearsal; otherwise use a staging instance with identical routing.
Key concepts
- Reverse proxy
- A server-owned mapping from a public route to a predetermined upstream; the client cannot choose arbitrary destinations.
- Forward proxy
- A proxy where clients request external destinations. `ProxyRequests On` can create serious abuse risk and is not needed here.
- ProxyPass ordering
- More specific path mappings must precede a catch-all route so they are not shadowed.
- WebSocket upgrade
- An HTTP protocol transition to a long-lived bidirectional connection, permitted only on the intended route.
- Trusted proxy
- A known hop whose overwritten forwarding metadata the application is configured to accept; arbitrary client headers are untrusted.
- Connection timeout
- The maximum time to establish an upstream connection, distinct from the time allowed for a complete response.
Before you copy
Values used in this guide
{{domain}}Public Apache ServerName and HTTPS origin.
Example: chat.example.com{{backendPort}}Loopback TCP port owned by the private application.
Example: 3000{{backendService}}Exact systemd unit controlled during the failure rehearsal.
Example: chat-app.serviceSecurity and production boundaries
- Keep `ProxyRequests Off`; enabling proxy modules does not by itself create a forward proxy, but an explicit on-state or permissive proxy section can.
- Bind the backend to loopback or a protected service network. Never compensate for a proxy error by exposing its development listener publicly.
- Client-supplied `X-Forwarded-*` values are spoofable unless the edge removes/overwrites them and the application trusts only the edge address.
- Preserving Host is appropriate only when the application validates allowed hosts. Otherwise it can feed host-header attacks into URL generation, password resets, or cache keys.
- WebSocket endpoints need authentication, authorization, Origin policy, message limits, idle limits, and connection monitoring beyond the Apache upgrade route.
- Timeouts and body/header limits reduce resource exhaustion but must reflect legitimate uploads, streaming, and long-lived socket behavior.
Stop before continuing if
- Stop if the backend listens publicly, its health result is not deterministic, the intended systemd unit is unknown, or direct behavior is already unhealthy.
- Do not reload if the include is duplicated, catch-all precedes the WebSocket path, `ProxyRequests` is not Off, or configtest/vhost mapping differs.
- Do not trust forwarding headers until the application trust boundary and edge overwrite behavior are proven.
- Stop if public Host, scheme, redirect, cookie, WebSocket, authentication, or absolute-URL behavior differs from direct application expectations.
- Abort the failure test if the named service has unrelated dependents or the outage exceeds the approved window.
verification
Verify the backend directly
Confirm the private listener, health endpoint, response identity, and WebSocket path before introducing Apache.
Why this step matters
Separating backend health from proxy behavior prevents Apache from hiding an application, listener, route, or service failure that already exists.
What to understand
Use privileged socket inspection to prove address, port, process, and network namespace. A loopback bind is an explicit boundary.
Test `/health`, a normal response, Host behavior, headers, and the actual WebSocket route directly. Health should avoid secrets and expensive dependencies unless its contract says otherwise.
Record backend latency and current service logs so post-proxy regressions have a baseline.
System changes
- No persistent change; reads listener, service, and direct HTTP state.
Syntax explained
ss -lntp 'sport = :…'- Shows the exact listening TCP socket and owner.
curl --fail --silent- Requires an HTTP success response while returning only the health body.
curl --head- Records direct response status and headers without body transfer.
Values stay on this page and are never sent or saved.
sudo ss -lntp 'sport = :{{backendPort}}' && curl --fail --silent http://127.0.0.1:{{backendPort}}/health && curl --head http://127.0.0.1:{{backendPort}}/LISTEN 127.0.0.1:3000 ok HTTP/1.1 200 OK
Checkpoint: Checkpoint: upstream
Continue whenThe intended service alone listens on loopback, health returns its documented marker quickly, and direct HTTP/WebSocket behavior is known.
Stop whenThe listener is public/unknown, direct health fails, response identity differs, or the WebSocket route is undocumented.
If this step fails
No process owns the expected port.
Likely causeThe service is stopped, failed, socket-activated elsewhere, containerized, or configured for another port.
systemctl status chat-app.service --no-pagersudo journalctl -u chat-app.service -n 100 --no-pagersudo ss -lntp
ResolutionRepair and prove the backend lifecycle before configuring Apache.
Security notes
- Do not publish a debug health endpoint containing versions, environment, database state, or secrets.
Alternatives
- Target a protected service-network address when Apache and backend are separate hosts, with mutual network policy and health checks.
Stop conditions
- The listener is public/unknown, direct health fails, response identity differs, or the WebSocket route is undocumented.
command
Back up the target virtual host
Copy the exact active site file and record its checksum before adding proxy directives.
Why this step matters
The virtual host is the routing and TLS boundary, so preserving the exact file, metadata, checksum, and enabled symlink supports fast targeted rollback.
What to understand
Resolve the enabled symlink to confirm the saved available file is the active one and archive any existing include fragments.
Compare owner/mode and checksum, and record current external response before editing.
A file copy does not capture application configuration; retain the backend's independent release rollback.
System changes
- Creates a metadata-preserving copy beside the site configuration and a checksum record.
Syntax explained
cp --archive- Preserves mode, owner, timestamps, and links where applicable.
sha256sum- Provides integrity evidence for the saved site file.
Values stay on this page and are never sent or saved.
sudo cp --archive /etc/apache2/sites-available/{{domain}}.conf /etc/apache2/sites-available/{{domain}}.conf.before-proxy && sudo sha256sum /etc/apache2/sites-available/{{domain}}.conf.before-proxyed90... /etc/apache2/sites-available/example.com.conf.before-proxy
Checkpoint: Checkpoint: backup
Continue whenThe saved file is root-controlled, checksum verifies, and it matches the currently enabled site's pre-proxy state.
Stop whenThe enabled site resolves elsewhere, backup cannot be verified, or generated configuration will overwrite manual edits.
If this step fails
The copied file is not the enabled vhost.
Likely causeThe site symlink points to a differently named/generated file.
readlink -f /etc/apache2/sites-enabled/chat.example.com.confsudo apache2ctl -S
ResolutionBack up the actual active source and document its deployment owner.
Security notes
- Site files can disclose internal endpoints and certificate paths; protect backup access.
Alternatives
- Use a version-controlled declarative vhost and tested deployment rollback when it is the authoritative source.
Stop conditions
- The enabled site resolves elsewhere, backup cannot be verified, or generated configuration will overwrite manual edits.
command
Enable only the proxy modules required
Enable HTTP, WebSocket, and header support; keep forward proxying explicitly disabled.
Why this step matters
Apache needs only the transport and header modules used by the design; module state should be explicit before adding directives.
What to understand
mod_proxy is the core, mod_proxy_http carries HTTP and modern WebSocket upgrades, headers overwrites reviewed scheme context, and proxy_wstunnel remains for compatibility.
Enabling modules changes symlinks but does not yet create routing. Search the whole config for existing proxy behavior before reload.
Keep `ProxyRequests Off` in the site and verify no other global file enables forward proxying.
System changes
- Enables Apache module symlinks for proxy, HTTP transport, WebSocket compatibility, and headers.
Syntax explained
a2enmod proxy- Enables the common proxy framework.
proxy_http- Adds HTTP upstream transport and modern upgrade support.
proxy_wstunnel- Adds legacy/specific WebSocket tunneling compatibility.
headers- Allows controlled request-header overwrite at the edge.
sudo a2enmod proxy proxy_http proxy_wstunnel headers && sudo apache2ctl -M | grep -E 'proxy|headers'proxy_module (shared) proxy_http_module (shared) proxy_wstunnel_module (shared) headers_module (shared)
Checkpoint: Checkpoint: modules
Continue whenRequired modules load once, no unreviewed proxy modules/routes appear, and forward proxying remains disabled.
Stop whenModule enablement changes unrelated dependency state, an existing proxy is undocumented, or config search finds `ProxyRequests On`.
If this step fails
Apache reports an unknown ProxyPass parameter.
Likely causeThe installed Apache version predates the selected upgrade or timeout behavior.
apache2ctl -vsudo apache2ctl configtest
ResolutionUse syntax documented for the installed release or update through the approved distribution path.
Security notes
- Unused proxy protocols expand attack surface; do not enable AJP, FTP, CONNECT, or balancing modules without need.
Alternatives
- On Apache 2.4.47+, omit proxy_wstunnel when testing proves mod_proxy_http handles the required upgrades.
Stop conditions
- Module enablement changes unrelated dependency state, an existing proxy is undocumented, or config search finds `ProxyRequests On`.
config
Add bounded upstream proxying
Create a dedicated included fragment that maps the application root to the loopback backend, preserves Host, bounds connect and response timeouts, and reverses upstream redirects. A sentinel prevents duplicate includes on rerun.
Why this step matters
A dedicated include makes the route atomic and reviewable, while a sentinel prevents reruns from adding repeated Include directives to the site.
What to understand
The upstream is a literal loopback address and reviewed port; never interpolate a client-controlled destination.
Five seconds bounds connection establishment and 60 seconds bounds normal responses. Streaming or long jobs require an intentional route-specific design.
Host is preserved and scheme/port are overwritten by Apache. Configure the application to trust only this proxy hop.
ProxyPassReverse handles selected redirect headers, not cookies, HTML, JavaScript, or application base-URL logic.
System changes
- Creates the site proxy include and inserts one marked IncludeOptional line inside the selected vhost.
Syntax explained
ProxyRequests Off- Disables client-selected forward proxy requests.
ProxyPreserveHost On- Passes the public Host header to the backend.
connectiontimeout=5- Bounds upstream TCP connection establishment.
timeout=60- Bounds response I/O for the mapping.
ProxyPassReverse- Rewrites supported upstream redirect headers to the public proxy origin.
RequestHeader set- Overwrites scheme/port metadata instead of appending spoofable client values.
/etc/apache2/sites-available/{{domain}}.confValues stay on this page and are never sent or saved.
printf 'ProxyRequests Off\nProxyPreserveHost On\nProxyPass / http://127.0.0.1:{{backendPort}}/ connectiontimeout=5 timeout=60\nProxyPassReverse / http://127.0.0.1:{{backendPort}}/\nRequestHeader set X-Forwarded-Proto "https"\nRequestHeader set X-Forwarded-Port "443"\n' | sudo tee /etc/apache2/sites-available/{{domain}}-proxy.inc >/dev/null && sudo grep -q 'ONELINERS PROXY INCLUDE' /etc/apache2/sites-available/{{domain}}.conf || sudo sed -i '\#</VirtualHost>#i\ # ONELINERS PROXY INCLUDE\n IncludeOptional /etc/apache2/sites-available/{{domain}}-proxy.inc' /etc/apache2/sites-available/{{domain}}.confProxyRequests Off ProxyPass / http://127.0.0.1:3000/ connectiontimeout=5 timeout=60
Checkpoint: Checkpoint: proxy config
Continue whenOne site-scoped include contains the exact private upstream, bounded timeouts, reverse mapping, and explicit forward-proxy off-state.
Stop whenThe vhost has multiple trust domains, marker/include already differs, upstream is non-private, or diff touches another site.
If this step fails
The include is outside VirtualHost and affects other sites.
Likely causeThe main site has an unusual structure or the insertion matched an unintended closing block.
sudo grep -n -A3 -B3 'ONELINERS PROXY INCLUDE' /etc/apache2/sites-available/chat.example.com.confsudo apache2ctl -S
ResolutionRestore the saved site and place one reviewed include manually or declaratively in the exact vhost.
Security notes
- The backend must validate Host and accept forwarded context only from Apache's address.
Alternatives
- Manage the complete vhost and included fragment through configuration management instead of an inline insertion.
Stop conditions
- The vhost has multiple trust domains, marker/include already differs, upstream is non-private, or diff touches another site.
config
Route the WebSocket endpoint explicitly
On Apache 2.4.47+, place the known WebSocket path before the catch-all mapping and use upgrade=websocket only for that path instead of permitting arbitrary protocol tunneling.
Why this step matters
A WebSocket route is a distinct long-lived protocol boundary and must precede the root mapping so only the documented path receives upgrade behavior.
What to understand
The idempotency check prevents duplicate path rules. Review trailing slashes because ProxyPass path mapping semantics depend on them.
Test authentication and Origin policy in the application; Apache routing alone does not authorize a connection.
Plan idle timeout, maximum connections, message size, deployment draining, and log/metric visibility.
System changes
- Adds one explicit `/socket/` mapping before the catch-all line in the site proxy include.
Syntax explained
ProxyPass /socket/- Selects only the documented public socket path.
upgrade=websocket- Allows the WebSocket protocol upgrade for this mapping.
grep -q … ||- Makes the insertion safe to rerun when the exact rule already exists.
/etc/apache2/sites-available/{{domain}}.confValues stay on this page and are never sent or saved.
sudo grep -q '^ProxyPass /socket/ .*upgrade=websocket$' /etc/apache2/sites-available/{{domain}}-proxy.inc || sudo sed -i '\#^ProxyPass / http://#i ProxyPass /socket/ http://127.0.0.1:{{backendPort}}/socket/ upgrade=websocket' /etc/apache2/sites-available/{{domain}}-proxy.incProxyPass /socket/ http://127.0.0.1:3000/socket/ upgrade=websocket
Checkpoint: Checkpoint: websocket
Continue whenThe specific WebSocket mapping appears once before `/`, targets the same private backend, and a real handshake returns 101.
Stop whenThe application route differs, catch-all precedes it, arbitrary upgrade is enabled, or authentication/Origin requirements are unknown.
If this step fails
The route works over HTTP polling but never upgrades.
Likely causeRule ordering, path mismatch, missing upgrade support, or backend handshake rejection.
sudo grep -n '^ProxyPass' /etc/apache2/sites-available/chat.example.com-proxy.incsudo tail -n 100 /var/log/apache2/chat.example.com-error.log
ResolutionCorrect the exact ordered path and inspect backend handshake evidence with a WebSocket client.
Security notes
- Long-lived unauthenticated sockets can exhaust workers and bypass ordinary request controls; enforce policy in the application.
Alternatives
- Use the application's supported long-polling mode only when its scaling and timeout behavior are explicitly accepted.
Stop conditions
- The application route differs, catch-all precedes it, arbitrary upgrade is enabled, or authentication/Origin requirements are unknown.
verification
Validate proxy direction and syntax
Confirm syntax, dump the virtual host, and prove ProxyRequests remains Off before reload.
Why this step matters
Syntax, vhost selection, route order, included scope, and forward-proxy state must be proven before the public listener adopts the new mapping.
What to understand
Parse the full include graph, dump vhosts, inspect all ProxyRequests declarations, and print ordered ProxyPass rules.
Search all enabled files, not only the new fragment; an old global `ProxyRequests On` remains dangerous.
Do not treat `ProxyRequests Off` text alone as proof—perform a controlled external negative proxy test after activation.
System changes
- No persistent change; parses and audits enabled Apache configuration.
Syntax explained
apache2ctl configtest- Parses the complete configuration before reload.
apache2ctl -S- Shows which vhost owns the public name/address.
grep -R ProxyRequests- Audits forward-proxy state across enabled files.
sudo apache2ctl configtest && sudo apache2ctl -S && sudo grep -R '^\s*ProxyRequests' /etc/apache2/sites-enabledSyntax OK ProxyRequests Off
Checkpoint: Checkpoint: validate
Continue whenSyntax is valid, one intended vhost includes the fragment, socket route precedes root, and every active ProxyRequests state is Off.
Stop whenParser, vhost map, include scope, route order, or proxy-direction audit differs from expectation.
If this step fails
configtest reports ProxyPass cannot occur in this context.
Likely causeThe Include line landed outside the intended VirtualHost or inside an unsupported section.
sudo grep -n -A5 -B5 'ONELINERS PROXY INCLUDE' /etc/apache2/sites-available/chat.example.com.conf
ResolutionRestore and place the include directly within the correct VirtualHost.
Security notes
- A valid parser cannot determine business authorization or trusted-proxy correctness; those remain explicit review gates.
Alternatives
- Run the same parser and negative proxy tests on a staging host before deployment.
Stop conditions
- Parser, vhost map, include scope, route order, or proxy-direction audit differs from expectation.
warning
Reload and test proxied HTTP
Request the public hostname, confirm application content and forwarded scheme behavior, then inspect both Apache and upstream logs.
Why this step matters
A graceful reload activates the validated route while public probes, backend logs, and Apache logs prove end-to-end host, scheme, status, and health behavior.
What to understand
Test the public origin without bypassing certificate verification. Inspect redirect, cookies, absolute URLs, authentication, and a representative application transaction.
Compare Apache access/error evidence with application logs and a request ID where available.
Run a negative forward-proxy request from an authorized test client and verify Apache rejects it.
System changes
- Reloads Apache and creates normal proxy/application log records and client connections.
Syntax explained
systemctl reload apache2- Applies parsed configuration gracefully where possible.
curl --fail --head- Requires public success and records edge headers.
tail domain-error.log- Reads site-specific proxy errors after activation.
Values stay on this page and are never sent or saved.
sudo systemctl reload apache2 && curl --fail --head https://{{domain}}/ && curl --fail https://{{domain}}/health && sudo tail -n 20 /var/log/apache2/{{domain}}-error.logHTTP/2 200 ok
Checkpoint: Checkpoint: reload
Continue whenHTTPS and health succeed through Apache, application context is correct, WebSocket handshake works, and forward-proxy requests fail.
Stop whenAny tenant, certificate, redirect, cookie, auth, scheme, log, latency, or negative-proxy result differs from baseline.
If this step fails
The application enters an HTTPS redirect loop.
Likely causeIt does not trust the edge scheme or multiple layers overwrite forwarding headers inconsistently.
curl -vkI --max-redirs 0 https://chat.example.com/sudo grep -n RequestHeader /etc/apache2/sites-available/chat.example.com-proxy.inc
ResolutionDefine one trusted edge and one public base URL; remove duplicate header manipulation.
Security notes
- Never use `curl -k` as the acceptance test; certificate failure is a real deployment blocker.
Alternatives
- Canary the vhost on a second address and switch DNS/load-balancer traffic after the same probes pass.
Stop conditions
- Any tenant, certificate, redirect, cookie, auth, scheme, log, latency, or negative-proxy result differs from baseline.
verification
Confirm failure is bounded and observable
During a controlled window, stop the test backend, confirm Apache returns 502/503 quickly rather than hanging, restart it, and verify recovery.
Why this step matters
A proxy is operationally safe only when upstream absence fails quickly, visibly, and reversibly instead of tying up clients and Apache workers.
What to understand
Confirm the exact systemd service and dependent units before stopping it; use staging when shared dependencies make production interruption unsafe.
Measure HTTP status and total time with a 10-second client ceiling, then inspect edge and service logs.
Restart the backend, prove its direct health first, then public health and a WebSocket reconnect.
System changes
- Temporarily stops and starts the named backend service, producing controlled 5xx and recovery evidence.
Syntax explained
systemctl stop/start- Controls the exact reviewed backend lifecycle.
curl --max-time 10- Bounds the client observation during deliberate failure.
--write-out- Records status and elapsed time without response-body ambiguity.
Values stay on this page and are never sent or saved.
sudo systemctl stop {{backendService}} && curl --max-time 10 --output /dev/null --write-out '%{http_code} %{time_total}\n' https://{{domain}}/ || true; sudo systemctl start {{backendService}} && curl --fail https://{{domain}}/health503 0.012 ok
Checkpoint: Checkpoint: failure test
Continue whenFailure returns a quick 502/503, Apache remains healthy, logs identify upstream refusal, and direct/public recovery succeeds.
Stop whenThe service has unreviewed dependents, Apache hangs, other tenants regress, restart fails, or recovery is incomplete.
If this step fails
The controlled stop causes unrelated applications to fail.
Likely causeThe backend unit or port is shared despite the inventory.
systemctl list-dependencies --reverse chat-app.servicesudo ss -lntp 'sport = :3000'
ResolutionRestart immediately, restore service, and redesign isolation before repeating.
Security notes
- Do not weaken timeout or expose the backend to hide failure; preserve a private, observable boundary.
Alternatives
- Inject the failure into an identical staging backend when production interruption is not authorized.
Stop conditions
- The service has unreviewed dependents, Apache hangs, other tenants regress, restart fails, or recovery is incomplete.
Finish line
Verification checklist
sudo apache2ctl configtest && sudo grep -R '^\s*ProxyRequests Off' /etc/apache2/sites-enabledSyntax OK and no forward proxy is enabled.curl --fail https://{{domain}}/healthReturns the backend health marker through Apache.Recovery guidance
Common problems and safe checks
Apache returns 502 immediately.
Likely causeThe backend is stopped, bound to another address/port, or the proxy fragment targets the wrong endpoint.
sudo ss -lntp 'sport = :3000'systemctl status chat-app.service --no-pagercurl -v http://127.0.0.1:3000/healthsudo tail -n 100 /var/log/apache2/chat.example.com-error.log
ResolutionRepair the private service or exact upstream target; keep the listener private and retest directly before reloading Apache.
Requests hang until a client timeout.
Likely causeThe upstream accepts connections but blocks, response timeout is unsuitable, or the application waits on a dependency.
curl --max-time 10 -v http://127.0.0.1:3000/healthsudo journalctl -u chat-app.service -n 100 --no-pagersudo ss -ntp 'dport = :3000'
ResolutionFix the blocked dependency or endpoint; change timeout only from measured legitimate behavior and capacity impact.
The application redirects to localhost or HTTP.
Likely causeIt does not trust the edge scheme/host context, generates absolute URLs from backend transport, or ProxyPassReverse does not match the upstream location.
curl -skI https://chat.example.com/curl -sI http://127.0.0.1:3000/sudo grep -nE 'ProxyPass|ProxyPreserveHost|RequestHeader' /etc/apache2/sites-available/chat.example.com-proxy.inc
ResolutionConfigure one documented proxy trust model and public base URL in the application; do not accept arbitrary client forwarding headers.
WebSocket handshake returns 200/404 instead of 101.
Likely causeThe catch-all route shadows `/socket/`, path differs, upgrade headers are stripped, or the backend does not support that endpoint.
sudo grep -n '^ProxyPass' /etc/apache2/sites-available/chat.example.com-proxy.incsudo apache2ctl configtestsudo journalctl -u chat-app.service -n 100 --no-pager
ResolutionPut the exact socket path before `/`, prove the backend endpoint, then test with a WebSocket-capable client.
An external proxy test can reach arbitrary destinations.
Likely causeAnother included file enables `ProxyRequests On` or grants broad forward-proxy access.
sudo grep -R -n '^\s*ProxyRequests' /etc/apache2sudo apache2ctl -t -D DUMP_RUN_CFGsudo apache2ctl -S
ResolutionDisable forward proxying immediately, inspect access logs for abuse, restrict egress if necessary, and treat unexpected use as an incident.
Reference
Frequently asked questions
Should ProxyPreserveHost always be On?
No. Use it when the application expects and validates the public host; otherwise configure an explicit upstream host and public base URL.
Why put the WebSocket mapping before `/`?
Apache evaluates mappings by configuration order; the catch-all root would otherwise consume that path first.
Does ProxyPassReverse rewrite HTML or JavaScript URLs?
No. It rewrites selected response headers such as Location; application-generated body content needs correct application proxy awareness.
Recovery
Rollback
Restore the saved virtual host and disable proxy modules only when no other site uses them.
- Restore {{domain}}.conf.before-proxy over the active site file.
- Run apache2ctl configtest, reload Apache, and verify the prior site response.
- Disable proxy modules only after apache2ctl -M and a configuration search prove they are unused elsewhere.
- Keep the backend bound to loopback throughout rollback; do not expose it as a shortcut.
Evidence