Six months ago, I wrote about why I was tired of developing on URLs like http://localhost:3000 and built Roxy.
The first version had a simple goal: give every local project a memorable
.roxy domain with trusted HTTPS.
http://localhost:3000
became
https://myapp.roxy
It worked, and I started using it every day.
Today, Roxy 1.1.0 is available, and the project finally has a proper home at roxy.rbas.cz. It still does the same simple thing, but the project around that idea has changed quite a bit:
- Docker Compose services can be discovered automatically
- Linux is supported alongside macOS
- wildcard subdomains get trusted HTTPS
- one domain can route different paths to different services
- configuration changes are applied without restarting the daemon
- static directories have a built-in file browser
- and, most importantly, the daemon no longer runs as root
Here is what changed, why it changed, and what I learned while turning a small experiment into a tool I can rely on.
The daily workflow is still deliberately boring
Roxy is not trying to become a general-purpose reverse proxy. The workflow is
still centered around one command:
roxy register myapp.roxy --route "/=3000"
Open https://myapp.roxy and you are done. Roxy handles local DNS, creates a trusted certificate, and proxies the request to the application on port 3000.
For a full-stack project, several services can share the same domain:
roxy register myapp.roxy \
--route "/=3000" \
--route "/api=3001" \
--route "/admin=8080"
That gives you:
https://myapp.roxy -> frontend on port 3000
https://myapp.roxy/api -> API on port 3001
https://myapp.roxy/admin -> admin app on port 8080
The longest matching path wins, and routes can now be changed after a domain is registered:
roxy route add myapp.roxy /webhooks 9000
roxy route remove myapp.roxy /admin
roxy route list myapp.roxy
The daemon reloads the new configuration immediately. No hand-written proxy configuration and no service restart.
Docker Compose without another routing file
Docker auto-discovery was the biggest addition in Roxy 1.0.
Enable it once in the Roxy configuration:
[docker]
enabled = true
Then start a normal Compose project:
name: myproject
services:
web:
build: .
ports:
- "3000:3000"
api:
build: ./api
ports:
- "8080:8080"
docker compose up -d
Roxy watches Docker events and exposes the services automatically:
https://web.myproject.roxy
https://api.myproject.roxy
There is no roxy register command and no second configuration file describing the same containers. When a container starts or stops, Roxy updates its routing table.
The default convention can be changed with Compose labels when needed:
services:
web:
build: .
ports:
- "3000:3000"
labels:
roxy.domain: "shop.roxy"
roxy.wildcard: "true"
I wanted the default case to require no Roxy-specific configuration while keeping an escape hatch for projects that need a stable domain or a specific port.
Wildcard domains became a real requirement
The original exact-domain model was enough until I used Roxy with a
multi-tenant application. Registering every tenant subdomain individually was clearly the wrong workflow.
Now one registration can cover the base domain and its subdomains:
roxy register myapp.roxy --wildcard --route "/=3000"
These URLs then work immediately:
https://myapp.roxy
https://acme.myapp.roxy
https://globex.myapp.roxy
Roxy generates an exact certificate from TLS SNI when a hostname is first requested and keeps it in an in-memory cache. Per-domain private keys are not written to disk.
Roxy now works on Linux
The first release was macOS-only. Roxy now supports Ubuntu and Debian as well.
The user-facing workflow remains the same, but the system integration is native to each platform:
- macOS uses
/etc/resolverand launchd - Linux uses
systemd-resolvedand systemd
The important part is that the platform-specific work stays behind the same CLI.
Developers should not need to know which service file or DNS configuration Roxy
created to run roxy register.
The daemon no longer runs as root
This is the change in Roxy 1.1 that matters most to me.
Local HTTPS normally wants ports 80 and 443, and binding those ports requires privileges. The early version took the straightforward route: the Roxy daemon ran as root.
It worked, but it became increasingly uncomfortable as Roxy gained more responsibilities. A process that reads project directories, watches Docker, loads user configuration, and handles network traffic should not have unlimited system privileges just because it needs two low-numbered ports.
Roxy 1.1 uses socket activation instead:
launchd/systemd owns ports 80 and 443
|
v
passes open listeners to Roxy
|
v
Roxy runs as your user
The one-time installation still needs elevated privileges because it installs the local CA, DNS integration, and system service:
sudo roxy install
After that, routine commands run without sudo:
roxy register myapp.roxy --route "/=3000"
roxy reload
roxy restart
roxy logs -f
roxy status
Configuration, logs, runtime files, and the CA are owned by the developer account. The operating system owns only the privileged listeners and hands them to the unprivileged Roxy process.
This was not a flashy feature, but it made the security model much easier to explain: Roxy has the privileges it needs, and no more.
Upgrading from an older Roxy installation
If you already use a version with the old root daemon, upgrade Roxy and run:
sudo roxy install
The installer imports registrations and the Root CA from /etc/roxy, stops the old service, creates the user-owned configuration, and installs socket activation. It leaves the old /etc/roxy directory in place as a migration backup.
New installations use the same command:
brew tap rbas/roxy
brew install roxy
sudo roxy install
Then register a project:
roxy register myapp.roxy --route "/=3000"
Restart your browser once after the initial installation so it picks up the local certificate authority.
Try Roxy 1.1
Roxy is open source and written in Rust. It runs on macOS and Linux and ships as a single binary with no external runtime dependencies.
- Website: roxy.rbas.cz
- Source code: github.com/rbas/roxy
- Documentation: Roxy docs
If you try it, I would love to hear what works, what breaks, and which part of your local development setup is still more complicated than it should be.
Top comments (0)