make OXYNOTE_PUBLIC_URL optional

This commit is contained in:
swithek 2026-09-12 12:05:00 +02:00
parent caa3e5d20c
commit 0cac90e5da
5 changed files with 56 additions and 26 deletions

View File

@ -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.
<details>
<summary>Docker Compose with PostgreSQL, Meilisearch for search and Mailpit for mail</summary>
<summary>Docker Compose with PostgreSQL, Meilisearch for search and Mailpit for email</summary>
```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

View File

@ -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

View File

@ -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.

View File

@ -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,
}) => {

View File

@ -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<string, string | undefined>): 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,