Caddy
Curated name caddy. Adds a Caddy reverse proxy to
the workbench, running as its own container next to your workspace and
configured by a Caddyfile you keep in your repo.
What it’s for
Section titled “What it’s for”One address for everything the browser talks to. Without a proxy your app
answers at one address and each service at another, and anything that cares
about the origin has to be worked around: cookies get scoped to the wrong host,
redirects leave the app, and an OIDC issuer no longer matches the page that
started the login. Put Caddy in front and the browser sees a single origin,
which you then split by path: the app on / and the login server on
/realms/.
It is language-agnostic. Caddy sits in its own container and talks to your dev server and the other services over the workbench network, so nothing in your project has to know it is there.
Add it
Section titled “Add it”At init:
monoceros init acme --with-services=caddyOr to a workbench you already have:
monoceros add-service acme caddyMounting your Caddyfile
Section titled “Mounting your Caddyfile”Caddy’s configuration is a file called Caddyfile, and it belongs in your
project repo: it describes how your app is served, so it is versioned with the
app. Keep it at projects/<app>/caddy/Caddyfile and mount the directory it
lives in.
The curated entry ships a commented volumes: scaffold. Uncomment it and
point it at your project. Paths are relative to the container root, where your
repos land under projects/:
services: - name: caddy image: caddy:2 port: 81 httpPort: 81 volumes: - projects/myapp/caddy:/etc/caddy:roThen monoceros apply acme, and Caddy comes up with your configuration.
Edits are live: the service runs with --watch, so Caddy re-reads the file
when you save it. You do not have to apply or restart anything.
Writing the Caddyfile
Section titled “Writing the Caddyfile”Inside the file you address the targets by their network names. workspace is
the container you work in, so that is where your own dev server runs. Every
service is named as in the container yml.
{ servers { trusted_proxies static private_ranges }}
:81 { handle /realms/* { reverse_proxy keycloak:8080 } handle { reverse_proxy workspace:5173 }}The handle blocks are tried from the top. The first one takes the paths
Keycloak owns and sends them to the Keycloak
service on its port 8080. The second has no condition and catches everything
else, which goes to the dev server on its port. Add one block per service you
want under the same origin.
The site address :81 is the port the curated entry declares, as port and
httpPort in the yml. Those three have to agree: change the Caddyfile to
another port and change both yml lines with it, or nothing answers. It is 81
rather than 80 because on the host 80 belongs to the machine-wide Traefik
proxy, and rather than 8080 because that is what Spring Boot, Tomcat and other
app servers take, which would put your proxy and your app on the same number.
Leave the domain name off the site address. Given one, Caddy would try to
obtain a TLS certificate for it, which fails here and is not needed: on the
host you reach the workbench over plain http, and
monoceros share terminates TLS in front
of it with the local CA.
reverse_proxy passes the original host and scheme on to the target. That is
what lets Keycloak build its issuer from the address the browser actually used,
which is why the curated Keycloak entry runs with --proxy-headers=xforwarded.
The trusted_proxies block at the top matters as soon as something sits in
front of Caddy, which is what
monoceros share does: it terminates HTTPS
and forwards to Caddy over plain http, saying so in the headers. Without the
block Caddy discards that and reports http to Keycloak, which then stamps an
http:// issuer while the browser is on https://, and the login fails.
Measured over a real share: http://…/realms/master without it,
https://…/realms/master with it. private_ranges covers the workbench
network the terminator connects from.
One file for the workbench and the pipeline
Section titled “One file for the workbench and the pipeline”A deployment differs from the workbench in two details: the port Caddy listens
on, and the names of the things it proxies to (workspace:5173 here, your app’s
compose service there). Caddy substitutes environment variables anywhere in the
Caddyfile, {$VAR:default} included, so both fit in one file:
:{$CADDY_SITE_PORT:81} { handle /realms/* { reverse_proxy {$KEYCLOAK_HOST:keycloak}:{$KEYCLOAK_PORT:8080} } handle { reverse_proxy {$APP_HOST:workspace}:{$APP_PORT:5173} }}In the workbench nothing is set and every default applies, so this behaves
exactly like the version above. $WORKSPACE_HOST is in the environment too, if
you prefer naming it that way. In a pipeline the compose block Monoceros hands
you in .monoceros/deploy.md supplies CADDY_SITE_PORT, APP_HOST and
APP_PORT, and the file travels unchanged.
Another way is to leave the file alone entirely and map the port outside:
ports: ["80:81"] in the deployment compose, or an ingress that targets 81.
Then nothing about Caddy changes at all.
A third way, if you would rather keep dev values out of the file: a default is
a fallback for both sides, so {$APP_PORT:5173} means a deployment that
forgets APP_PORT does not fail. It proxies to 5173, your dev server’s port,
in production. Write the variables bare instead:
:{$CADDY_SITE_PORT} { handle { reverse_proxy {$APP_HOST}:{$APP_PORT} }}Now the workbench has to supply them as well, and that is what the caddy
service’s env: block is for. The curated entry ships it commented, right
below the volumes. Uncomment it and keep the keys your Caddyfile reads:
services: - name: caddy image: caddy:2 port: 81 httpPort: 81 env: CADDY_SITE_PORT: ${CADDY_SITE_PORT} # values live in acme.env APP_HOST: ${APP_HOST} APP_PORT: ${APP_PORT}The values belong in acme.env next to the yml, the same place every other
service’s do:
CADDY_SITE_PORT=81APP_HOST=workspaceAPP_PORT=5173A forgotten key cannot slip past you: if a service field reads a ${VAR} that
acme.env does not set, monoceros apply stops and names it.
The three keys in the scaffold are a starting point, not the set. Caddy
substitutes any variable your Caddyfile reads, so the block takes whatever
names you chose. Every key is a pair: one line in the yml says the value comes
from the env file, one line in acme.env says what it is. Write
{$API_PORT} in the Caddyfile and it is these two lines:
env: API_PORT: ${API_PORT}API_PORT=3000Rename them, drop the ones you do not use, add the ones you do. That the
catalog leaves the block commented is the same point from the other side: a
Caddyfile’s variables are its author’s, so there is no fixed set to prefill the
way there is for Keycloak’s admin user. The three it suggests are the ones the
pipeline block in .monoceros/deploy.md supplies, so a file written against
those travels to a deployment unchanged.
Live reload behind the proxy
Section titled “Live reload behind the proxy”A dev server proxied through Caddy keeps serving the page, but its live reload stops working. The reason: the server tells the browser to open the reload connection on the server’s own port, while the browser arrived through Caddy on another one, so the connection goes nowhere.
Fix it in your app’s config by naming the port the browser used. In Vite that is
server.ws.clientPort (older versions call it server.hmr.clientPort); other
dev servers have an equivalent. Nothing else about the app changes.
An agent in the workbench knows this already: the briefing apply writes into
AGENTS.md carries it, together with the rules for the Caddyfile and the fact
that the mount belongs in the container yml on the host. So you can tell it to
put the app behind the proxy without explaining any of it.
Reaching it from the host
Section titled “Reaching it from the host”The proxy gives Caddy its own address, acme-caddy.localhost, as soon as the
workbench is applied. Nothing to start, nothing to keep running: open it in the
browser.
The name is the workbench plus the service, because the Traefik proxy is shared
by every workbench on the machine and two of them running Caddy would otherwise
claim the same name. monoceros status acme prints the address on the service’s
row.
For another device on your network, take
monoceros share, which serves the same
Caddy over HTTPS. And for something that is not HTTP there is still
monoceros tunnel, a raw-TCP forward:
monoceros tunnel acme caddySee Proxy and tunnels.
Remove it
Section titled “Remove it”monoceros remove-service acme caddyOr delete the service’s block from acme.yml by hand and re-run
monoceros apply acme - the yml is the source of truth.
The yml entry
Section titled “The yml entry”services: - name: caddy # host name in the container image: caddy:2 port: 81 # in-container port (feeds `monoceros tunnel`) httpPort: 81 # the port that may leave the container command: caddy run --config /etc/caddy/Caddyfile --adapter caddyfile --watch # volumes: # uncomment + edit to mount your own files # - projects/<app>/caddy:/etc/caddy:ro # env: # uncomment + add the keys your config references # CADDY_SITE_PORT: ${CADDY_SITE_PORT} # APP_HOST: ${APP_HOST} # APP_PORT: ${APP_PORT}Both ports are 81 and both mean something different. port is what
monoceros tunnel acme caddy forwards to, httpPort is what may leave the
container: it is the port monoceros share
offers to other devices. Your Caddyfile’s site address makes three, and all
three carry the same number.
The command is what makes edits live: caddy run keeps the server in the
foreground, --adapter caddyfile says the mounted file is a Caddyfile and not
Caddy’s own JSON format, and --watch re-reads it on change. There is no data
directory, because Caddy only stores certificate state and TLS terminates
elsewhere, and no connection env in the workspace, because the traffic runs the
other way: Caddy calls your app, not your app Caddy.
The service starts after your repos are cloned, so the Caddyfile is on disk when Caddy boots. Until you mount your own, Caddy runs the file from its image, which serves a welcome page on port 80 and therefore answers on none of the addresses above.