The application host
app.example.com is the address people use. Behind it sit the three application services —
the web app, the REST service and the engine — of which only the web app is reachable. This
page is the HTTPS proxy in front of it, and the three addresses that have to agree for
sign-in to work.
proxy/app.conf
A plain HTTPS reverse proxy to ouroboros-ui, with nothing path-specific. For the proxy
service on the compose page, it is mounted as conf.d/default.conf:
server {
listen 443 ssl;
server_name app.example.com;
ssl_certificate /etc/ssl/ouroboros/app.example.com.pem;
ssl_certificate_key /etc/ssl/ouroboros/app.example.com.key;
location / {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://ui:3000;
}
}
That is the whole configuration. The web app handles everything itself: pages, the API calls
its own server makes, and the sign-in paths it forwards. Nothing here points at the REST
service, and nothing should: a proxy that forwarded /api/ to rest:4000 would also expose
/internal/*, which is the engine's surface.
Terminate TLS here, with a certificate for app.example.com. The app must be served over
https://: the session cookie is a __Secure- cookie, which a browser only accepts from a
secure origin.
The addresses that must agree
Sign-in is the one thing the browser does not go through the app's server for — a click, then
GitHub's redirect back. The app forwards /api/auth/* to the REST service, so the browser
only ever sees app.example.com, and that is why four addresses have to be that one:
| Where | Value |
|---|---|
BETTER_AUTH_URL on the REST service | https://app.example.com |
OURO_UI_URL on the REST service | https://app.example.com |
OURO_CORS_ORIGINS on the REST service | https://app.example.com |
| The GitHub OAuth app's Authorization callback URL | https://app.example.com/api/auth/callback/github |
BETTER_AUTH_URL is the one that surprises people. The repository's
.env.example sets it to the REST service's address, which works in development because
both services are on localhost. In this layout the REST service is internal and the browser
can only reach the app, so it is the app's public address.
On ouroboros-ui, OURO_REST_URL is the opposite: the REST service's
internal address, http://rest:4000. The app calls it from its own server and never sends
it to a browser.
Running the services elsewhere
The compose page puts every service on one host. They need not be:
- The web app can run on more than one host behind the proxy. It keeps no state.
- The REST service can run as more than one replica, if build artifacts go to S3 rather
than a volume — see
OURO_ARTIFACT_STORE. Every replica needs the same secrets. - The engine needs to be reachable from every REST replica at
OURO_ENGINE_URL, and nowhere else.
Whatever the layout, the rule is the same: the web app is the only service with a public address, and the REST service's port is published to nothing outside the network.
Checking it
- The sign-in page loads at
https://app.example.com, over a certificate the browser trusts, with Continue with GitHub. - A full sign-in works. Choose it, authorize on GitHub, and land back in the app. If
GitHub shows The redirect_uri is not associated with this application, the OAuth app's
callback is not
https://app.example.com/api/auth/callback/github. - The cookie is secure. In the browser's developer tools, the session cookie for
app.example.comis named__Secure-better-auth.session_token. - The REST service is not reachable from outside. From a machine outside your network,
curl https://app.example.com/internal/runsis the app's 404, and nothing answers on the REST service's port.
What can go wrong
- Sign-in redirects to
localhost:4000—BETTER_AUTH_URLstill holds the development value. Set it tohttps://app.example.comand restart the REST service. - GitHub refuses the callback — the OAuth app's callback URL does not match. Register it
as
https://app.example.com/api/auth/callback/github, exactly. - Signed in, then immediately signed out again — the app is served over
http://, so the browser drops the__Secure-cookie. Serve it over HTTPS. - Every request after sign-in is refused —
OURO_CORS_ORIGINSdoes not includehttps://app.example.com. - The app shows
502 Bad Gateway— theuicontainer is not running or not healthy.docker compose psanddocker compose logs uisay why.