Skip to content

Reverse Proxy Routes

A reverse proxy route publishes a service running on your server under one of the domains CorePanel already manages. Requests matching a path are forwarded to a host:port you choose; every other path keeps being served from the domain’s document root as usual.

https://app.example.com/api/orders ──▶ 127.0.0.1:8080 (your service)
https://app.example.com/index.html ──▶ ~/public_html (the docroot)

The service does not need a public port, a certificate, or a hostname of its own. It listens on the loopback interface, and the domain — with everything CorePanel already puts in front of it — becomes its front door.

What you get by proxying instead of exposing a port

Section titled “What you get by proxying instead of exposing a port”

Running a service on :8080 and telling people to visit http://example.com:8080 works, but it is naked. Behind a CorePanel route, the same service gets:

  • Automatic HTTPS. The domain’s certificate, issued and renewed by CorePanel. Your service keeps speaking plain HTTP on loopback.
  • The WAF and the firewall. Requests are filtered before they reach your service, and the service’s own port never has to be open to the internet.
  • Traffic statistics and logs. Proxied requests are counted on the dashboard’s Web Traffic panel like any other.
  • A real hostname. app.example.com instead of an IP and a port number.

Typical cases:

CaseRoute
An API next to a normal website/api/*127.0.0.1:8080, docroot keeps serving the rest
A whole app (Node, Go, Python, Java) on its own domain or subdomain/*127.0.0.1:3000
A single-page app with a separate backendSPA files in the docroot, /api/* → the backend with strip prefix
A service you installed yourself (Gitea, a metrics dashboard, a webhook receiver)/* → its local port
A gradual rewritemove one path at a time to the new service, leave everything else on the old site
A service on another machine in your private network/*10.0.0.20:8080

Before you start: CorePanel does not run your service

Section titled “Before you start: CorePanel does not run your service”

This is the one thing to be clear about.

The supported way to keep a service alive on a RHEL-family server is a systemd unit. A minimal one, in /etc/systemd/system/myapp.service:

[Unit]
Description=My application
After=network.target
[Service]
User=myaccount
WorkingDirectory=/opt/userapps/myaccount/myapp
ExecStart=/opt/userapps/myaccount/myapp/myapp
Restart=always
[Install]
WantedBy=multi-user.target
Terminal window
systemctl daemon-reload
systemctl enable --now myapp

With that in place the route is stable: systemd keeps the process up, CorePanel keeps the door open.

The shortcut: proxy a domain as you create it

Section titled “The shortcut: proxy a domain as you create it”

Most addon domains and subdomains created for an application want the whole hostname pointed at that application, so the Add domain and Add subdomain dialogs offer it directly: switch on Serve from a local service, give the host:port, and the site is published through your service the moment it exists. That is exactly equivalent to adding a /* route afterwards, and the route shows up in the Reverse proxy section like any other.

The option is not offered for a domain alias: an alias is served by the domain it points at and has no configuration of its own, so the route belongs on that domain instead.

If the domain is created but the route fails (a typo in the port, for instance), the domain is kept — you get a warning saying the route is still pending, and you add it from the section below.

The editor lives in the account’s workspace, under Reverse proxy. It is administrator-only — see Why administrators only.

  1. Pick the site the route belongs to. Every primary domain, addon domain and subdomain of the account is listed. (Domain aliases are not: they have no configuration of their own and are served by the domain they point at, so the route goes on that domain.)
  2. Click Add route and fill in four fields:
    • Path — what the route matches: a prefix (/api/*), an exact path (/health) or /* for the whole site.
    • Upstream — where the requests go, as host:port. Never a URL.
    • Strip the path prefix — off by default. See below.
    • Connect over HTTPS — only if your service speaks TLS on that port.
  3. Use Test to dial the upstream before saving. It tells you whether anything is listening there right now.
  4. Save. The route applies immediately; no restart, no downtime for the rest of the site.

Once saved, each route shows whether its target is Responding (with the round-trip time) or gives No response and the reason. That indicator is the difference between “my site is broken” and “my service is down”, which from the outside are the same 502.

The matcher is literal, and /api and /api/* are not the same thing:

MatcherMatchesDoes not match
/api/*/api/, /api/orders, /api/v1/x/api, /apidocs
/api/api exactly/api/orders
/*everything

If your service should own both /api and everything under it, add the two routes.

By default the upstream receives the path exactly as the visitor typed it: a request to /api/orders arrives at your service as /api/orders. With strip prefix on, the matcher’s prefix is removed and the same request arrives as /orders.

Turn it on when your service is written as if it owned the root — which most frameworks are, unless you configure a base path. Turn it off when the service already expects to live under /api. It is ignored for a /* matcher, where there is nothing to strip.

The panel lists the site’s whole route table in the order the web server evaluates it, not just the routes you added. The first match wins, so the order is what decides which route answers a request — hiding the rest would make a shadowed route look like a bug. CorePanel keeps proxy routes ordered most-specific-first, so a catch-all never swallows a more specific route.

Rows you cannot edit are shown for context:

  • Automatic — maintained by CorePanel. The most common one is the docroot fallback: as soon as a site has an explicit route table, something has to keep serving the files, and that entry is it. It appears with your first proxy route and disappears with your last one.
  • Manual — a route written by hand in the site’s configuration file. CorePanel displays it and leaves it alone.

The same operations, from corepanel:

Terminal window
# See the whole table, in evaluation order
corepanel domain proxy list app.example.com
# ...and dial each target while you are at it
corepanel domain proxy list app.example.com --check
# Publish a service under /api, which the service sees as /
corepanel domain proxy add app.example.com '/api/*' 127.0.0.1:8080 --strip-prefix
# Put the whole domain in front of an app
corepanel domain proxy add app.example.com '/*' 127.0.0.1:3000
# Back to serving the docroot
corepanel domain proxy remove app.example.com '/api/*'

add probes the upstream right after applying the route, so a typo in the port shows up as a warning instead of as a 502 in someone’s browser. Quote the path: your shell would otherwise expand the *.

Full flag reference in the CLI documentation.

Reverse-proxy routes are restricted to super-administrators, and that is a security boundary rather than a policy about who is trusted with the UI.

Whoever creates a route chooses the upstream. Pointed at a service running on the server, a route publishes it on the public internet under the domain’s name — including services that are only safe because they listen on loopback. Handing this to an account would mean handing it the ability to expose the panel’s own API under its hostname.

CorePanel filters the obvious cases on top of that:

  • The web server’s own ports (80 and 443) on loopback are refused outright: a site proxying to its own server is an endless request loop.
  • Well-known local service ports (the CorePanel API, MySQL, PostgreSQL, Redis, Memcached, SSH, SMTP) require an explicit confirmation, because the overwhelmingly likely explanation for 127.0.0.1:3306 in that field is a mistake.

Neither filter applies to remote hosts: proxying to 10.0.0.20:3306 is your network’s business.

A proxied path can only fail in a few ways, and they are easy to tell apart:

SymptomWhere to look
502 on the proxied path, docroot fine elsewhereYour service is not listening. Check the route’s status indicator in the panel, or corepanel domain proxy list <domain> --check.
Your service will not startsystemctl status <unit> and journalctl -u <unit>. A 203/EXEC means systemd could not execute the binary — see the SELinux note above.
The service works from the shell but not under systemdAlmost always SELinux or a ProtectHome/ProtectSystem setting in the unit.
The upstream answers, but with the wrong pathsThe prefix strip. /api/orders reaches your service as /orders with it on, /api/orders with it off.

For anything SELinux-shaped, the audit log is /var/log/audit/audit.log:

Terminal window
ausearch -m AVC -ts recent
grep denied /var/log/audit/audit.log | tail # when ausearch comes back empty

Run both. ausearch filters by time and event type, and it will occasionally report nothing for a denial that is sitting in the log file — so a plain grep on the raw log is the check that settles it.

  • A route survives web-mode changes, optimization changes and certificate renewals. It does not survive deleting and recreating the domain.
  • Up to 50 proxy routes per site.
  • Removing a route never touches the service behind it. The path simply goes back to being served from the document root.

Proxy routes are the exposure half of running an application on a CorePanel server. The other half — CorePanel creating the systemd unit, keeping the process alive, applying resource limits, showing you its logs and deploying new versions — is the application runtime, and it is available: see Applications. Use a proxy route for a service you want to supervise yourself, and an application for one you would rather CorePanel supervised.

You may also see an upstream that is not a host:port at all:

unix:/run/corepanel/apps/<account>/<app>.sock

That is how the application runtime publishes an application, and it is worth knowing why before you meet one. A port on 127.0.0.1 is reachable by every process on the server, another customer’s account included — so on a shared machine one account’s traffic could reach another’s backend behind the web server, the WAF, and whatever authentication you put in front. A socket file cannot be reached that way: it is owned by the account, its group is the web server’s, its mode is 0660, and the directory above it is closed to everyone else. The permissions are the isolation, and you can check them with ls.

Only that exact layout is accepted — one account directory, one .sock file. No other socket on the server can be named, so the route editor can never be pointed at a system service. In practice CorePanel writes these routes itself; you will see them in the table rather than type them.

Your own service does not have to speak unix sockets to benefit from this: the runtime puts a small forwarder in front of it, so the application keeps listening on whatever port it already listens on, inside a network namespace where nothing else can reach it.