The problem
I work on a project where the frontend is a single-page app and the backend runs in a Windows VM via Parallels. There’s also a deployed version of the backend hosted in Azure. Depending on what I’m working on, I need to hit one or the other—and switching between them was getting tedious.
The frontend needs HTTPS—things like camera access and mixed content rules were causing issues over plain HTTP in the local dev setup. The VM backend serves plain HTTP, so I needed something to put HTTPS in front of it.
On top of that, the API URL gets baked into the frontend bundle at build time. Changing it means restarting the dev server. So even if I could just point at a different backend URL, I’d be restarting the dev server every time I switched.
I needed a local proxy that could serve HTTPS, sit at a stable URL the frontend never has to change, and let me swap the upstream behind it with minimal effort.
Caddy as the gateway
Caddy handles this perfectly. It automatically handles HTTPS certificates for localhost, so the frontend always talks HTTPS to localhost:9443 and Caddy forwards traffic to whichever backend is active.
I set up two Caddyfiles—one pointing at the Azure environment and one pointing at the VM:
| |
| |
Both versions set CORS headers and handle preflight OPTIONS requests. The VM version also strips any CORS headers the upstream might already set, so Caddy’s own headers are the only ones the browser sees.
Switching is one command:
caddy reload --config ~/project/Caddyfile.azure
direnv for scoped shortcuts
Typing that reload command every time is fine, but I wanted something snappier. I created two tiny scripts in a .bin directory:
| |
| |
Now, I could add ~/project/.bin to my PATH in .zshrc. But that means every terminal session gets these project-specific commands on the path, even when I’m nowhere near this project. That’s where direnv comes in.
direnv watches for .envrc files as you cd around your filesystem. When you enter a directory that has one, it loads the environment. When you leave, it unloads it. One line in an .envrc is all it takes:
PATH_add .bin
From any terminal under ~/project, I get caddy-azure and caddy-vm as commands. Step outside that tree and they disappear. No shell pollution, no remembering to clean up.
The workflow
Day to day it looks like this:
caddy-azure— starts Caddy with the Azure config (or reloads if it’s already running)- Start the frontend:
npm start - Work against the Azure backend
- Need to test against the VM?
caddy-vm - Done with the VM?
caddy-azure
The frontend doesn’t care—it always hits https://localhost:9443. The proxy does the routing.
One gotcha
Clear your session after switching. If the app caches auth tokens or session data (and it probably does), log out before switching backends. Stale tokens from one environment won’t work against the other, and the errors aren’t always obvious.
