diff --git a/README.md b/README.md index 52a4d13e..f6aebfc1 100644 --- a/README.md +++ b/README.md @@ -145,28 +145,29 @@ agents edit the same pages, and the pages stay designed for people. ## Quick start -Oxynote runs as one container next to a PostgreSQL database you already -have. It needs to know two things: the address people will open in the -browser, and how to reach that database. +Oxynote is one container plus a PostgreSQL database. With a database +already running, this is all it takes: -```console -$ docker run -d --name oxynote \ +```sh +docker run -d --name oxynote \ -p 8080:8080 \ - -e OXYNOTE_PUBLIC_URL=http://localhost:8080 \ - -e OXYNOTE_DB_DSN='postgresql://oxynote:change-me@postgres.example.com/oxynote?sslmode=disable' \ + -e OXYNOTE_DB_DSN='postgresql://dbusername:dbpass@postgres.example.com/oxynote?sslmode=disable' \ -v oxynote_data:/oxynote/data \ ghcr.io/oxynote/oxynote:latest ``` -Open the address and sign up. Until you configure an email sender, the -verification link shows up in the container's logs instead of your inbox. -Keep the data volume: it holds your uploads and the secrets that keep -everyone signed in and every data source connected. For a real domain, put a -TLS-terminating proxy in front and set the public URL to the `https://` -address. +Open http://localhost:8080 and sign up. Until you configure an email sender, +the verification link shows up in the container's logs instead of your +inbox. Keep the data volume: it holds your uploads and the secrets that keep +everyone signed in and every data source connected. The container assumes it +can be reached at http://localhost:8080. If you want to open it anywhere +else, set `OXYNOTE_PUBLIC_URL` to that address, and put a TLS-terminating +proxy in front for the domain. + +No PostgreSQL yet? The compose file below starts one next to Oxynote.
-Docker Compose with PostgreSQL, Meilisearch for search and Mailpit for mail +Docker Compose with PostgreSQL, Meilisearch for search and Mailpit for email ```yaml name: oxynote @@ -177,7 +178,6 @@ services: ports: - "8080:8080" environment: - - OXYNOTE_PUBLIC_URL=http://localhost:8080 - OXYNOTE_DB_DSN=postgresql://oxynote:change-me-db@postgres/oxynote?sslmode=disable - OXYNOTE_MEILISEARCH_URL=http://meilisearch:7700 - OXYNOTE_MEILISEARCH_MASTER_KEY=change-me-meili-key diff --git a/docker/prod/README.md b/docker/prod/README.md index 60373d92..15572793 100644 --- a/docker/prod/README.md +++ b/docker/prod/README.md @@ -10,7 +10,7 @@ relay and changedetection.io are optional and also external. ```sh cp docker/prod/docker-compose.example.yaml my-deployment.yaml -# replace every change-me value, set OXYNOTE_PUBLIC_URL +# replace every change-me value; for a real domain set OXYNOTE_PUBLIC_URL docker compose -f my-deployment.yaml up -d ``` @@ -21,9 +21,10 @@ supported) and runs this example on `http://localhost:8080`, with at `http://localhost:8025` instead of being logged. `make prod-stop` stops it. -The image serves plain HTTP on port **8080**. For a real domain, put a -TLS-terminating proxy in front and set `OXYNOTE_PUBLIC_URL` to the public -`https://` origin. +The image serves plain HTTP on port **8080** and assumes it is reached at +`http://localhost:8080` unless told otherwise, so publish the container as +`8080:8080` for a local run. For a real domain, put a TLS-terminating proxy +in front and set `OXYNOTE_PUBLIC_URL` to the public `https://` origin. ## Configuration @@ -35,9 +36,14 @@ fails the boot with an error naming it. | Variable | Meaning | | --- | --- | -| `OXYNOTE_PUBLIC_URL` | the origin users open in the browser, e.g. `https://notes.example.com`. Scheme + host only — every public URL, the cookie domain, and the CORS rules derive from it. | | `OXYNOTE_DB_DSN` | PostgreSQL DSN, e.g. `postgresql://user:pass@host/db?sslmode=disable`. One database serves the whole product; migrations run automatically at boot. | +### Public address + +| Variable | Meaning | +| --- | --- | +| `OXYNOTE_PUBLIC_URL` | the origin users open in the browser, e.g. `https://notes.example.com`. Scheme + host only — every public URL, the cookie domain, and the CORS rules derive from it. Defaults to `http://localhost:8080`, which is right only while the container's port 8080 is published as host port 8080; set it for a domain or any other host port. | + ### Optional features Each feature is keyed on one variable; leaving it unset disables the feature diff --git a/docker/prod/docker-compose.example.yaml b/docker/prod/docker-compose.example.yaml index 0911e320..ef4b5b17 100644 --- a/docker/prod/docker-compose.example.yaml +++ b/docker/prod/docker-compose.example.yaml @@ -1,8 +1,8 @@ # the canonical production deployment: the all-in-one image plus the # backing services it needs. Copy this file, replace every change-me value, -# set OXYNOTE_PUBLIC_URL to the origin users will open in the browser, and -# put a TLS-terminating proxy in front for a real domain (the image itself -# speaks plain HTTP on 8080). +# and for a real domain put a TLS-terminating proxy in front and set +# OXYNOTE_PUBLIC_URL to the origin users will open in the browser (the image +# itself speaks plain HTTP on 8080). # # `make prod-run` from the repository root builds the image and runs this # file against it, with docker-compose.local.yaml layered on top to add the @@ -18,7 +18,8 @@ services: - "8080:8080" environment: # the public origin; every URL, cookie domain and CORS rule the - # components need derives from this one value. + # components need derives from this one value. Optional: this is + # the default, and it matches the 8080:8080 port mapping above. - OXYNOTE_PUBLIC_URL=http://localhost:8080 - OXYNOTE_DB_DSN=postgresql://oxynote:change-me-db@postgres/oxynote?sslmode=disable # optional: full-text search. diff --git a/docker/prod/launcher/src/env.test.ts b/docker/prod/launcher/src/env.test.ts index 369a2791..58f1e1f5 100644 --- a/docker/prod/launcher/src/env.test.ts +++ b/docker/prod/launcher/src/env.test.ts @@ -52,6 +52,23 @@ describe("loadConfig", () => { }) describe("public URL", () => { + it("defaults to localhost on the container's port", ({ + expect, + }) => { + const config = loadConfig( + completeEnv({ OXYNOTE_PUBLIC_URL: undefined }), + ) + + expect(config.publicOrigin).toBe( + "http://localhost:8080", + ) + expect(config.publicHostPort).toBe("localhost:8080") + expect(config.cookieDomain).toBe("localhost") + expect(config.publicWebSocketOrigin).toBe( + "ws://localhost:8080", + ) + }) + it("derives the host forms and the WebSocket origin", ({ expect, }) => { diff --git a/docker/prod/launcher/src/env.ts b/docker/prod/launcher/src/env.ts index 3c69e167..32178fca 100644 --- a/docker/prod/launcher/src/env.ts +++ b/docker/prod/launcher/src/env.ts @@ -107,6 +107,12 @@ function count() { return z.string().regex(/^\d+$/, "must be a whole number").optional() } +// what the public origin falls back to. Caddy listens on 8080 inside the +// container, so a container published as 8080:8080 is reachable here with +// no variable set; anything else (a domain, another host port) needs +// OXYNOTE_PUBLIC_URL. +const defaultPublicUrl = "http://localhost:8080" + // the public origin every URL derives from. A path would silently break the // caddy routing, which is root-anchored. const publicOrigin = httpUrl().refine( @@ -327,7 +333,7 @@ const aiAssistantKeys = [ // with OXYNOTE_ is a boot error, so a typo — or an internal component // variable — fails loudly instead of being silently ignored. const baseSchema = z.object({ - OXYNOTE_PUBLIC_URL: publicOrigin, + OXYNOTE_PUBLIC_URL: publicOrigin.optional(), OXYNOTE_DB_DSN: z.string().min(1), OXYNOTE_VALKEY_DSN: valkeyDsn.optional(), OXYNOTE_OBJECT_STORAGE_DSN: objectStorageDsn.optional(), @@ -558,7 +564,7 @@ export function loadConfig(source: Record): Config { } const values = result.data - const url = new URL(values.OXYNOTE_PUBLIC_URL) + const url = new URL(values.OXYNOTE_PUBLIC_URL ?? defaultPublicUrl) return { publicOrigin: url.origin,