Dev Servers on the Host
roji discovers containers on its own. A dev server you start on the host
itself — vite dev, wrangler dev, rails s — is not a container, so
nothing discovers it. roji can still give such a server a hostname, HTTPS
included, next to your containers.
There are two ways to do it:
roji runstarts the dev server for you on a free port. Nothing to configure, and no port to pickproxy_sitesin the config file names a port you chose. The hostname is there whether or not the server is running
roji run: no port to pick
roji run app -- npx vite --port {port}
# roji: https://app.dev.localhost -> localhost:41873roji run <name> -- <command> picks a free port, routes
https://<name>.<domain> to it, and runs the command. The route lasts as long
as the command does.
Every project gets a port nothing else is using, so two dev servers that both default to 5173 or 8787 stop colliding, and no project needs a port written into its config.
Passing the port to the command
The command has to listen on the port roji chose. It gets it two ways:
{port}anywhere in the arguments is replaced with the port- The
PORTenvironment variable is set to it
Which one a tool reads is the tool’s business:
| Tool | How |
|---|---|
| Vite | roji run app -- npx vite --port {port} |
| Wrangler | roji run api -- npx wrangler dev --port {port} |
Next.js, Rails, most others that read PORT | roji run web -- npm run dev |
Vite and Wrangler ignore PORT. Started without --port {port} they open
their usual port, the URL roji printed shows an error page, and after 20
seconds roji run says that nothing is listening on the port it chose.
Passing the hostname to the command
Some servers have to know the address they are reached at: to build OAuth metadata, absolute redirects, or a list of allowed hosts. The hostname depends on the base domain, which is a setting of each machine, so it cannot be written into a script that is checked in. The command gets it the same two ways as the port:
| In the arguments | Environment variable | Value |
|---|---|---|
{host} | ROJI_HOST | app.dev.localhost |
{url} | ROJI_URL | https://app.dev.localhost |
The URL includes the port when roji’s https_port is not 443. Both values are
the ones the running roji answered with.
The variable is not HOST, because many dev servers read HOST as the address
to listen on.
A Worker that looks at the origin of the request needs both the hostname and
the protocol, since wrangler dev otherwise presents the request as plain
http:
roji run api -- npx wrangler dev --port {port} --host {host} --upstream-protocol httpsThe -- is required. Everything after it belongs to the command, which is
what keeps its flags from being read as roji’s.
What roji run does and does not do
- roji has to be running.
roji runregisters the route with it first, and does not start the command if it cannot - The name follows the rule for
hostbelow. A name the config file declares underproxy_sitesorstatic_sitesis refused, as is one anotherroji runis using - A container routed to the same hostname is hidden while the command runs,
and
roji runsays so - The command’s exit code is
roji run’s; a command stopped by a signal is reported as 128 plus the signal number, as a shell does (130 for Ctrl-C). Ctrl-C andkillreach the command and everything it started, sonpxwrappers do not leave a server behind - The route can only be registered from the machine roji runs on, whatever
bindis set to - If
roji runitself is killed outright (kill -9), the route goes away within 30 seconds. The dev server it started is not stopped - The route is not published through the
tunnel. Use
proxy_siteswithtunnel: truefor that
proxy_sites: a port you chose
Add entries to ~/.config/roji/config.yaml:
proxy_sites:
- host: app # -> app.dev.localhost
target: http://localhost:5173 # where the dev server listens
- host: api
target: http://127.0.0.1:8787Start the dev server as usual and open https://app.dev.localhost.
Hostname
host follows the same rule as
static sites:
- No dot: expanded to
{host}.{domain}(app→app.dev.localhost) - With dot: used as written
Keep the hostname one level under the base domain. roji’s certificate covers
*.{domain} and nothing deeper, so a browser rejects a.b.dev.localhost as
it does a name on another domain. roji doctor reports the ones that fall
outside.
The dashboard’s hostname (roji.dev.localhost by default) and the base domain
itself cannot be used: the dashboard answers on them first, so a site declared
there is skipped with a warning.
Target
target is a URL, scheme included. http is the only scheme: roji terminates
TLS itself.
target: 8787andtarget: 127.0.0.1:8787are not accepted. There is noport:shorthand either, so that a site has one spelling- A path in the target is put in front of every request:
target: http://localhost:3000/basesends/itemsto/base/items - Prefer
http://localhost:PORTwhen you are unsure which address the server binds. Depending on the tool and the Node.js version, “localhost” means127.0.0.1or::1, and roji tries both - A target on another machine works, with a validation warning: requests to it leave this machine unencrypted
What the dev server receives
The same as a container does:
- The
Hostheader is the hostname the browser asked for (app.dev.localhost), not the target’s. Dev servers that check the host, such as Vite’sserver.allowedHosts, see that name. Vite allows.localhostnames by default X-Forwarded-Proto: https,X-Forwarded-ForandX-Real-IP- WebSocket (Vite’s HMR), Server-Sent Events and gRPC pass through
When the server is not running
A proxy site whose dev server has not been started is the normal state of
things, so roji registers the site without checking the target. A request then
gets a 502 page naming the target address and saying what to start. roji
cannot start the server for you: unlike a Compose project, it knows nothing
about the process.
roji doctor lists the proxy sites nothing is listening behind, as a warning.
Precedence over Docker routes
A proxy site holds its hostname, like a static site does. If a container is
routed to the same hostname, the proxy site answers and roji logs which Docker
route it hides. A hostname declared twice in the config file — under
proxy_sites, static_sites, or one of each — goes to the later declaration,
proxy_sites being read after static_sites.
A proxy site takes the whole hostname. There is no path prefix for it.
Applying changes
No restart needed. Either:
roji config reloadOr click Reload Config in the dashboard. This is also how to point a site at a different port while roji is running.
The command lists every declared site and says which were registered. A site that was left out — a target that is not a URL, a hostname that is the dashboard’s — is marked with the reason; the reload still applies the rest.
Authentication
proxy_sites:
- host: api
target: http://127.0.0.1:8787
auth:
basic:
user: admin
pass: secret
realm: API # optionalSee the Basic Authentication guide.
Publishing through the tunnel
Receiving a webhook on a dev server is what the
tunnel is for. Opt the site in with tunnel: true:
proxy_sites:
- host: hook
target: http://127.0.0.1:8787
tunnel: true # reachable as https://hook.example.comThe public name is the local one with the base domain swapped for the tunnel’s, so only a hostname under the base domain can be published.
A site without the key stays private. When the dev server is not running, a request through the tunnel gets the same bare 404 as every other refusal there, not the page described above.