Skip to content

monoceros share

Exposes an app and the workbench’s HTTP services to the local network - your phone, a tablet, another laptop - over HTTPS, so any device on the same Wi-Fi can open them. The proxy’s http://<name>.localhost URLs only resolve on the host machine; share is how you reach things from elsewhere. It is a foreground process: Ctrl+C stops sharing.

Terminal window
monoceros share <name> <app>
  • <name> - the workbench you created with monoceros init.
  • <app> - the app to expose: a folder under projects/ that has a .monoceros/launch.json. Every target in that file that declares a port is shared, on the same port on the host’s network interface (0.0.0.0). Without a launch config share warns and goes on with the services alone.
  • --forward-ports <host:container>,... (optional) - publish busy ports under different host ports when a port is already taken. See When a port is already in use.

Services come along on their own, without being named: see Services are shared too.

Your workbench is acme, the app lives in projects/web and delegates login to a Keycloak service you added with monoceros add-service acme keycloak. Start the app’s dev servers, then share them:

Terminal window
monoceros start acme web # bring the app's dev servers up
monoceros share acme web # expose them to the LAN over HTTPS

share stays in the foreground and lists every address a device can use, per target and per service. The app’s web target comes from its launch config; Keycloak appears next to it because its catalog entry declares httpPort: 8080, which is what marks a service as reachable from outside the container:

Sharing acme/web on the local network:
web
https://192.168.1.10:5173
https://your-host.local:5173
keycloak (service)
https://192.168.1.10:8080
https://your-host.local:8080
Trust the local CA once (first device) for warning-free HTTPS:
~/.monoceros/ca/rootCA.pem
Press Ctrl+C to stop sharing.

Both addresses are offered because devices differ: some resolve the .local name, others need the IP. Open the web URL on your phone, on the same Wi-Fi. On a device you have not used before, trust the certificate once so the page loads without a warning. Press Ctrl+C when you are done - nothing stays exposed in the background.

share serves HTTPS, not plain HTTP, because a bare LAN IP or .local name over http is an insecure context in the browser - and there crypto.subtle (so OIDC/PKCE logins) and service workers (so installable PWAs) are switched off. HTTPS gives the device a real secure context, so a phone-first PWA actually works.

TLS is terminated by the share process using a certificate authority that lives only on your machine (~/.monoceros/ca/rootCA.pem); its private key never leaves the host. Each device that opens a share URL trusts that CA once - see Trusting the local certificate for the per-platform steps (macOS, Windows, Linux, iOS, iPadOS, Android). Because it terminates HTTP, share also forwards X-Forwarded-Proto/X-Forwarded-Host, so a backend behind it (an OIDC issuer, say) builds correct https:// URLs.

  • All configured ports, from the launch config - not just the default targets, and not only the ones currently running. The launch config is the source of truth, so a target you start later (e.g. a Storybook on its own port) is reachable the moment it comes up. A forward to a not-yet-started target simply refuses connections until it is up.
  • Targets without a port are skipped (there is nothing to expose).
  • The backend rides the frontend. If your frontend dev server proxies the API under a relative path (Vite server.proxy, Angular proxy.conf.json, CRA setupProxy.js), the browser only ever talks to the frontend origin - so the one frontend port is enough.

A phone that logs in needs to reach the login server, not only the app. So every service in the workbench yml that carries an httpPort is shared alongside the app, under its own name in the banner and on that port:

acme.yml
services:
- name: keycloak
image: quay.io/keycloak/keycloak:26.7
port: 8080 # in-container port, used by `monoceros tunnel`
httpPort: 8080 # the port that may leave the container

The catalog fills that line in for the services where it makes sense, and leaves it out where it does not. Keycloak declares 8080, Mailpit declares 8025 (the web inbox, not the 1025 your app sends mail to), RustFS declares 9001 (the console, not the S3 API on 9000), Caddy declares 81. A database has no httpPort and is never shared: share speaks HTTP, so a Postgres behind it would answer nonsense. Reach those with monoceros tunnel instead.

Both ways to change what goes out live in the yml and take effect on the next share, with no apply in between: delete the httpPort line to keep a service to itself, or set a different port to move it. If you picked that port yourself, as you do for a reverse proxy, change the service’s own config to the same number.

A dev server has to be configured so the network (and the proxy) can reach it. The briefing tells your AI agent to do this, but if you wire it by hand:

  • Listen on 0.0.0.0, not 127.0.0.1.
  • Accept the host’s name - Vite server.allowedHosts, Angular --allowed-hosts, CRA DANGEROUSLY_DISABLE_HOST_CHECK. (Bare IPs are allowed by most dev servers without this; a .local name needs it.)
  • Do not pin the HMR / live-reload socket to a fixed host or port - let it follow the page URL (for Vite, leave server.hmr unset). Then HMR works over <name>.localhost on the host and over the LAN, same config.

These are dev-server-only settings; a production build ignores them.

The last one flips when a reverse proxy of your own sits in front of the dev server. The browser then arrives on the proxy’s port, while the dev server still advertises its own for the reload socket, and live reload goes nowhere while the page itself works. There you do pin the client port (Vite: server.ws.clientPort) to the port the browser used.

share probes every port before it touches Docker, stops on a busy one, and says who holds it.

A container published it. The message names it, image included. The usual culprit is a share of the same workbench still running in another terminal, and its name says so: the terminator is called monoceros-share-<workbench>-<app>. Stop it there with Ctrl+C, or remove the container by that name.

Cannot share acme/web: host port 8080 already in use.
Published by a running container:
8080 monoceros-share-acme-web (caddy:2.11.4)

Nothing published it. Then it is your IDE: attached over Remote-SSH, VS Code, Codium and JetBrains auto-forward the container’s ports to 127.0.0.1, which docker cannot see and which cannot be reliably switched off. In the PORTS panel, right-click the port and choose “Stop Forwarding Port”. It stays gone across reconnects, and share then binds the original port unchanged.

Port 80 is a special case. That belongs to monoceros-proxy, the machine-wide Traefik that serves every workbench with ports, and it stays. A service that wants 80 gets a port of its own instead: change its httpPort in the yml and its own config to the same number.

Whatever holds the port, you can also publish around it. Re-run with --forward-ports, a comma-separated list of host:container pairs in Docker -p order:

Terminal window
monoceros share acme web --forward-ports 15173:5173,18000:8000

A service is named explicitly when a bare port would be ambiguous, which happens when an app target and a service use the same number:

Terminal window
monoceros share acme web --forward-ports 18080:keycloak:8080

The busy ports are reachable on the host ports you chose (https://your-host.local:15173); every port you do not list keeps its own number. The bind stays on 0.0.0.0, so other devices reach it exactly as before.

  • Foreground. The forwards live only while the command runs; Ctrl+C closes every one. Nothing stays exposed in the background.
  • One app per host address. A device reaches the app by the host’s IP (or .local name); that address routes to one workbench at a time. Share one app, not several at once.
  • HTTP apps only. share terminates TLS and reverse-proxies HTTP, so it is for browser-facing apps and services. For a database client or other non-HTTP tool, use monoceros tunnel, which is a raw-TCP forward.
  • Nothing to share is an error. If the app declares no port and no service declares an httpPort, share stops and names both places you could fill.