mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 01:54:10 +00:00
Open a fresh install from the LAN, not only from the machine it runs on
The stock compose file served https://localhost and nothing else. Reached by address or hostname, Caddy had no certificate to offer and ended the handshake, which a browser reports as an SSL protocol error with no way to proceed. A headless server could not be set up without first setting SERVER_NAME. The default now answers every name and address with the self-signed localhost certificate (default_sni/fallback_sni, and `:443` in the default SERVER_NAME), so the browser shows its usual warning. Live updates follow: compose.yaml's https://localhost hub address is treated as the placeholder it is and gives way to the public address stored during setup. Any other MERCURE_PUBLIC_URL, and any SERVER_NAME an operator sets, is left alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
34f6ec5951
commit
a8aea33874
12 files changed
+141
-47
No files matched your search
@@ -8,6 +8,16 @@ The published image tags: `latest` follows the most recent release below,
|
||||
|
||||
## Unreleased
|
||||
|
||||
**Fixed: a fresh install could only be opened on the machine it runs on.** The stock `compose.yaml`
|
||||
served `https://localhost` and nothing else. Opened from another machine by address or hostname, the
|
||||
browser reported an SSL protocol error, which unlike a certificate warning cannot be clicked
|
||||
through, so a headless server could not be set up at all without first setting `SERVER_NAME`. The
|
||||
default now answers on every name and address with the same self-signed certificate, and the
|
||||
browser shows its usual warning. Live updates follow as well: the hub address is taken from the
|
||||
public address chosen during setup, where the stock file used to pin it to `localhost`. An install
|
||||
that sets `SERVER_NAME` or `MERCURE_PUBLIC_URL` itself is unchanged. See
|
||||
[Docker → Starting it](docs/install/docker.md#starting-it).
|
||||
|
||||
**Mail can be marked as spam with a button.** It sits beside Delete: among a row's buttons, in an
|
||||
open conversation's toolbar and above a selection. On one conversation it opens a short menu — move
|
||||
it to Spam, or move it and also create a filter that sends everything from this sender, or from the
|
||||
|
||||
@@ -108,7 +108,9 @@ cd pl_mail
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Open [https://localhost](https://localhost) and create your account.
|
||||
Open [https://localhost](https://localhost) and create your account. From another machine, open
|
||||
`https://` and the server's address instead; the browser warns about the self-signed certificate
|
||||
once and lets you continue.
|
||||
|
||||
### TrueNAS
|
||||
|
||||
|
||||
+7
-1
@@ -89,7 +89,13 @@ services:
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
<<: *app-env
|
||||
SERVER_NAME: ${SERVER_NAME:-localhost}, php:80
|
||||
# `localhost` is the name Caddy makes its self-signed certificate for.
|
||||
# `:443` is every other name and address the machine is reached at: the
|
||||
# same site, answered with that same certificate (frankenphp/Caddyfile
|
||||
# says how), so an install on a headless box opens from the LAN behind
|
||||
# the usual certificate warning. Both go when you set SERVER_NAME — a
|
||||
# real domain is then the only name served, and ":80" is plain HTTP.
|
||||
SERVER_NAME: ${SERVER_NAME:-localhost, :443}, php:80
|
||||
MAILER_DSN: ${MAILER_DSN:-null://null}
|
||||
APP_CONTAINER_NAME: web
|
||||
volumes:
|
||||
|
||||
@@ -117,10 +117,24 @@ declare(strict_types=1);
|
||||
// Derived here rather than configured: it is one less thing to fill in, and
|
||||
// it cannot drift from the public URL. An explicit MERCURE_PUBLIC_URL still
|
||||
// wins, for the install whose hub really does live somewhere else.
|
||||
//
|
||||
// "Is it set" is not quite the test, for the reason it is not for the
|
||||
// database URL above: on the stock compose file it is always set.
|
||||
// compose.yaml defaults it to https://localhost/.well-known/mercure, which
|
||||
// is right for the install opened at https://localhost and for no other —
|
||||
// one set up from another machine on the LAN told every browser to
|
||||
// subscribe on its own loopback, and the lists never refreshed. That one
|
||||
// value is the placeholder nobody chose, so a public address stored during
|
||||
// setup replaces it. With no address stored it stays, and an install from
|
||||
// before setup asked the question keeps working where it always did.
|
||||
$publicUrl = trim((string) ($_SERVER['APP_PUBLIC_URL'] ?? $_ENV['APP_PUBLIC_URL'] ?? ''));
|
||||
$hubUrl = trim((string) ($_SERVER['MERCURE_PUBLIC_URL'] ?? $_ENV['MERCURE_PUBLIC_URL'] ?? ''));
|
||||
|
||||
if (false === $isSet('MERCURE_PUBLIC_URL') && '' !== $publicUrl) {
|
||||
$put('MERCURE_PUBLIC_URL', rtrim($publicUrl, '/').'/.well-known/mercure');
|
||||
if ('' !== $publicUrl && ('' === $hubUrl || 'https://localhost/.well-known/mercure' === $hubUrl)) {
|
||||
$derived = rtrim($publicUrl, '/').'/.well-known/mercure';
|
||||
|
||||
$_SERVER['MERCURE_PUBLIC_URL'] = $derived;
|
||||
$_ENV['MERCURE_PUBLIC_URL'] = $derived;
|
||||
}
|
||||
|
||||
// The subscriber cookie takes the `__Secure-` prefix where a browser will
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- translated-from: install/configuration.md sha1:cb3491e232974eac33e32f1e9ef2fd9298c8a108 -->
|
||||
<!-- translated-from: install/configuration.md sha1:71e9bc427171913eea3f957a3abc1d2b01c32940 -->
|
||||
# Konfigurationsreferenz
|
||||
|
||||
Jede Umgebungsvariable, die plMail liest, was sie bewirkt, welchen Vorgabewert sie hat und was
|
||||
@@ -60,7 +60,7 @@ Das sind die Variablen aus `.env`, in der Reihenfolge, in der sie dort stehen.
|
||||
| `APP_DEMO_IMPRESSUM_EMAIL` | Kontaktadresse auf derselben Seite, als `mailto:`-Link. | leer | Wie oben | Wie oben. |
|
||||
| `APP_DEMO_PRIVACY_HOST` | Das Unternehmen, das den Server betreibt; wird in der Datenschutzerklärung der Demo (`/datenschutz`) als Auftragsverarbeiter genannt. | leer | Für eine öffentliche Demo: ja | Leer, und die Seite zeigt statt eines Namens eine sichtbare Warnung. Wer die Maschine betreibt, verarbeitet in deinem Auftrag die IP-Adressen der Besucher und muss genannt werden; prüfe, ob ein Auftragsverarbeitungsvertrag besteht — die Seite sagt, dass es einen gibt. Wird nur gelesen, wenn `APP_DEMO_MODE` an ist. |
|
||||
| `MERCURE_URL` | Die Hub-Adresse, an die die **Anwendung** innerhalb des Docker-Netzes veröffentlicht. | `http://mercure/.well-known/mercure` | Ja | Falsch, und nichts wird veröffentlicht: keine Live-Aktualisierungen, kein sichtbarer Fehler auf der Seite. |
|
||||
| `MERCURE_PUBLIC_URL` | Die Hub-Adresse, die der **Browser** abonniert. | `https://localhost/.well-known/mercure`, sowohl in `.env` als auch in `compose.yaml` | Ja | Falsch, und der Browser öffnet einen Stream dorthin, wo er nicht hinkommt — Maillisten hören auf, sich selbst zu aktualisieren, während der Rest der Anwendung funktioniert. Wird nur dann aus `APP_PUBLIC_URL` abgeleitet, wenn sie nicht gesetzt oder leer ist, was die Standard-`compose.yaml` verhindert. Siehe [Hinter einem Reverse-Proxy](reverse-proxy.md). |
|
||||
| `MERCURE_PUBLIC_URL` | Die Hub-Adresse, die der **Browser** abonniert. | `https://localhost/.well-known/mercure`, sowohl in `.env` als auch in `compose.yaml` | Ja | Falsch, und der Browser öffnet einen Stream dorthin, wo er nicht hinkommt — Maillisten hören auf, sich selbst zu aktualisieren, während der Rest der Anwendung funktioniert. Wird aus `APP_PUBLIC_URL` abgeleitet, wenn sie nicht gesetzt, leer oder noch der Vorgabewert der Standard-`compose.yaml` ist. Siehe [Hinter einem Reverse-Proxy](reverse-proxy.md). |
|
||||
| `MERCURE_JWT_SECRET` | Signiert die Publisher- und Subscriber-JWTs. | leer — 32 zufällige Bytes, hexadezimal, beim ersten Start erzeugt | Ja, aber erzeugt | Anwendung und Hub müssen denselben Wert halten. Weichen sie voneinander ab, weist der Hub jeden Subscriber ab — aus Sicht des Browsers lautlos. |
|
||||
| `MERCURE_COOKIE_NAME` | Der Name des Cookies, mit dem der Browser abonnieren darf. | leer — plMail wählt `__Secure-mercure_access_token`, wenn `APP_PUBLIC_URL` https ist, und sonst `mercure_access_token`, weil ein Browser ein `__Secure-`-Cookie verwirft, das nicht über HTTPS kam | Nein | Nur setzen, wenn du einen eigenen Namen willst; die Anwendung setzt das Cookie und der Hub liest es, beide aus dieser einen Variablen. Gewählt wird beim Start des Web-Containers, eine geänderte öffentliche Adresse wirkt also ab dem nächsten Neustart. |
|
||||
| `MERCURE_UPSTREAM` | Wohin der Webserver `/.well-known/mercure` weiterreicht: der Container, in dem der Hub läuft, als `host:port`. | `mercure:80` | Nein | Nur für einen Stack, der diesen Container nicht `mercure` nennt. Falsch, und der Stream des Browsers antwortet mit 502 — die Anzeige in der oberen Leiste bleibt rot, während Mail weiter ankommt. `MERCURE_URL` muss denselben Container nennen. |
|
||||
@@ -96,7 +96,7 @@ sie direkt, wenn er `DATABASE_URL` zusammensetzt.
|
||||
|
||||
| Variable | Was sie bewirkt | Vorgabe | Wenn sie falsch ist |
|
||||
|---|---|---|---|
|
||||
| `SERVER_NAME` | Der Hostname, den Caddy im `php`-Container bedient. | `localhost, php:80` | Caddy entscheidet daran, ob es TLS terminiert. `:80` liefert schlichtes HTTP aus, was du hinter einem Reverse-Proxy willst. Ein Hostname bringt Caddy dazu, ein Zertifikat dafür zu beschaffen. |
|
||||
| `SERVER_NAME` | Der Hostname, den Caddy im `php`-Container bedient. | `localhost, :443, php:80` | Caddy entscheidet daran, ob es TLS terminiert. Der Vorgabewert bedient jeden Namen und jede Adresse, unter der die Maschine erreicht wird, mit einem selbstsignierten Zertifikat. `:80` liefert schlichtes HTTP aus, was du hinter einem Reverse-Proxy willst. Ein Hostname bringt Caddy dazu, ein Zertifikat dafür zu beschaffen, und ist dann der einzige Name, der bedient wird. |
|
||||
| `HTTP_PORT` | Host-Port, der auf Container-Port 80/tcp abgebildet wird. | `80` | Ein bereits belegter Port hindert den `php`-Container am Start. |
|
||||
| `HTTPS_PORT` | Host-Port, der auf Container-Port 443/tcp abgebildet wird. | `443` | Wie oben. |
|
||||
| `HTTP3_PORT` | Host-Port, der auf Container-Port 443/udp abgebildet wird. | `443` | Nur relevant, wenn du HTTP/3 direkt ausliefern willst. |
|
||||
@@ -239,11 +239,12 @@ Prüfung des Entrypoints lautet deshalb "trägt diese DSN ein Passwort?" und nic
|
||||
Eine DSN, die du *mit* Passwort mitgibst, wird als Absicht gewertet und vollständig in Ruhe
|
||||
gelassen.
|
||||
|
||||
**`MERCURE_PUBLIC_URL` hat einen Vorgabewert, der nicht abgeleitet ist.**
|
||||
`config/bootstrap_generated_secrets.php` baut ihn aus `APP_PUBLIC_URL` — aber nur, wenn er nicht
|
||||
gesetzt oder leer ist, und `compose.yaml` setzt ihn bedingungslos auf
|
||||
`https://localhost/.well-known/mercure`. Auf jeder Installation, die nicht unter `https://localhost`
|
||||
erreicht wird, setze ihn ausdrücklich.
|
||||
**`MERCURE_PUBLIC_URL` folgt `APP_PUBLIC_URL`, solange du sie nicht auf etwas anderes setzt.**
|
||||
`config/bootstrap_generated_secrets.php` baut sie aus `APP_PUBLIC_URL`, wenn sie nicht gesetzt, leer
|
||||
oder `https://localhost/.well-known/mercure` ist — der Wert, den `compose.yaml` auf jeder
|
||||
Installation setzt und der deshalb nichts darüber sagt, was du wolltest. Jeder andere Wert gilt als
|
||||
Absicht und bleibt unangetastet. Eine Installation ohne gespeicherte öffentliche Adresse behält den
|
||||
`localhost`-Wert.
|
||||
|
||||
**Ein leeres `GMAIL_PUBSUB_VERIFICATION_TOKEN` bedeutet "alles ablehnen", nicht "alles annehmen".**
|
||||
Für einen aus dem Internet erreichbaren Endpunkt ist das die richtige Vorgabe, aber es bedeutet:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- translated-from: install/docker.md sha1:c5abef947d51031887eb536d6c09b3784406aa96 -->
|
||||
<!-- translated-from: install/docker.md sha1:6a1b5ac4bb8a1c96bf13c943ef14a18ab8ce0cbf -->
|
||||
# Installation mit Docker Compose
|
||||
|
||||
Der unterstützte Weg, von Anfang bis Ende: was du brauchst, was `docker compose up` tatsächlich tut,
|
||||
@@ -57,6 +57,12 @@ docker compose up -d
|
||||
|
||||
Öffne dann [https://localhost](https://localhost).
|
||||
|
||||
Auf einer Maschine ohne eigenen Browser öffnest du von einer anderen aus `https://` und die Adresse
|
||||
oder den Hostnamen dieser Maschine — etwa `https://192.168.1.20`. In beiden Fällen hat der Container
|
||||
das Zertifikat selbst signiert; der Browser warnt also einmal und lässt dich weitermachen. Für ein
|
||||
Zertifikat, dem Browser vertrauen, setz `SERVER_NAME` auf eine Domain, die auf diese Maschine zeigt,
|
||||
oder stell einen [Reverse-Proxy](reverse-proxy.md) davor.
|
||||
|
||||
**Schließe die Einrichtung ab, bevor du irgendetwas anderes machst.** Solange es noch keinen Nutzer
|
||||
gibt, steht `/install` jedem offen, der die Instanz erreicht — genau so legst du dein eigenes Konto
|
||||
an, und genau so wird jemand anderes zum Administrator, wenn er schneller da ist. Das ist der
|
||||
@@ -85,7 +91,9 @@ die du kennen solltest:
|
||||
anschließend ein VAPID-Schlüsselpaar sowie das JMAP-JWT-Schlüsselpaar, falls diese fehlen.
|
||||
5. **Caddy nimmt den Betrieb auf.** Der `php`-Container veröffentlicht 80/tcp, 443/tcp und 443/udp —
|
||||
umlegbar über `HTTP_PORT`, `HTTPS_PORT` und `HTTP3_PORT` — und bedient die Namen aus
|
||||
`SERVER_NAME`, das `compose.yaml` auf `localhost, php:80` setzt. Außerdem leitet er
|
||||
`SERVER_NAME`, das `compose.yaml` auf `localhost, :443, php:80` setzt: `localhost` ist der Name,
|
||||
für den das selbstsignierte Zertifikat ausgestellt wird, und `:443` beantwortet jeden anderen
|
||||
Namen und jede andere Adresse mit demselben Zertifikat. Außerdem leitet er
|
||||
`/.well-known/mercure*` an den Hub-Container weiter, sodass der Browser den Hub auf derselben
|
||||
Origin erreicht und kein CORS nötig ist.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- translated-from: install/reverse-proxy.md sha1:0c7b41ef1eee378b74f5810cd9189eab728c4a1a -->
|
||||
<!-- translated-from: install/reverse-proxy.md sha1:72aecc70f1035f186e2322b3152b8dd4e0ad00f4 -->
|
||||
# Hinter einem Reverse-Proxy
|
||||
|
||||
Alles, was von außerhalb des eigenen Netzes erreichbar ist, will ein echtes Zertifikat davor, und
|
||||
@@ -21,7 +21,7 @@ services:
|
||||
SERVER_NAME: ":80" # FrankenPHP serves plain HTTP; no certificate here
|
||||
```
|
||||
|
||||
`SERVER_NAME` steht standardmäßig auf `localhost, php:80`, und Caddy entscheidet im Container anhand
|
||||
`SERVER_NAME` steht standardmäßig auf `localhost, :443, php:80`, und Caddy entscheidet im Container anhand
|
||||
dieses Namens, ob es TLS selbst terminiert. `:80` sagt ihm, dass es das nicht tun soll. Veröffentliche
|
||||
dann nur den HTTP-Port — `HTTP_PORT` bildet den Host-Port auf den Container-Port 80 ab — und lass
|
||||
den Proxy ihn erreichen. `truenas.compose.yaml` macht genau das und veröffentlicht `30080`.
|
||||
@@ -35,8 +35,8 @@ MERCURE_PUBLIC_URL=https://mail.example.com/.well-known/mercure
|
||||
```
|
||||
|
||||
**Der typische Fehlerfall ist, `SERVER_NAME` hinter einem Proxy auf dem Vorgabewert zu belassen.**
|
||||
Caddy versucht dann, `localhost` auszuliefern, während der Proxy es über Containernamen oder
|
||||
IP-Adresse anspricht — und die Anfrage passt auf keinen Site-Block.
|
||||
Caddy beantwortet die schlichte HTTP-Anfrage des Proxys dann mit einer Weiterleitung auf HTTPS, und
|
||||
der Browser dreht sich im Kreis.
|
||||
|
||||
## `APP_PUBLIC_URL`
|
||||
|
||||
@@ -121,11 +121,11 @@ Zwei Variablen, und sie zeigen in entgegengesetzte Richtungen:
|
||||
Adresse plus `/.well-known/mercure` sein.
|
||||
|
||||
`config/bootstrap_generated_secrets.php` leitet die zweite aus `APP_PUBLIC_URL` ab, damit es eine
|
||||
Sache weniger auszufüllen gibt — aber **nur, wenn `MERCURE_PUBLIC_URL` nicht gesetzt oder leer
|
||||
ist**, und die Standard-`compose.yaml` setzt sie bedingungslos auf
|
||||
`https://localhost/.well-known/mercure`. Auf einer Installation hinter einem Proxy setze sie also
|
||||
ausdrücklich. `truenas.compose.yaml` lässt den eigenen Wert genau deshalb leer, damit die Ableitung
|
||||
stattfinden kann.
|
||||
Sache weniger auszufüllen gibt. Das geschieht, wenn `MERCURE_PUBLIC_URL` nicht gesetzt, leer oder
|
||||
noch `https://localhost/.well-known/mercure` ist, der Wert, den die Standard-`compose.yaml` auf
|
||||
jeder Installation setzt. Jeder andere Wert ist deiner und bleibt unangetastet. Eine Installation
|
||||
hinter einem Proxy braucht also `APP_PUBLIC_URL` und sonst nichts — aus der Umgebung wie oben oder
|
||||
aus dem Einrichtungsbildschirm.
|
||||
|
||||
Nicht nur der Host zählt, sondern auch das Schema: Symfony leitet das `secure`-Flag des
|
||||
Subscriber-Cookies aus dieser URL ab, ein `https`-Wert auf einer Installation mit schlichtem HTTP
|
||||
@@ -239,11 +239,11 @@ Anbieter als Zustellfehler wertet und mit Backoff wiederholt.
|
||||
|
||||
## Fallstricke
|
||||
|
||||
**`MERCURE_PUBLIC_URL` wird bei der Standard-Compose-Datei nicht abgeleitet.** Die Ableitung aus
|
||||
`APP_PUBLIC_URL` findet nur statt, wenn die Variable nicht gesetzt oder leer ist, und `compose.yaml`
|
||||
setzt sie auf `https://localhost/.well-known/mercure`. Das ist der mit Abstand wahrscheinlichste
|
||||
Grund dafür, dass Live-Aktualisierungen in der Entwicklung funktionieren und in der Produktion
|
||||
nicht.
|
||||
**`MERCURE_PUBLIC_URL` wird erst abgeleitet, wenn es eine öffentliche Adresse gibt.** Ohne
|
||||
`APP_PUBLIC_URL` in der Umgebung und ohne eine vom Einrichtungsbildschirm gespeicherte bleibt das
|
||||
`https://localhost/.well-known/mercure` der Standard-Compose-Datei stehen, und Live-Aktualisierungen
|
||||
funktionieren unter `https://localhost` und nirgends sonst. Admin → Adresse zeigt, was gespeichert
|
||||
ist.
|
||||
|
||||
**Eine fehlgeschlagene Push-Registrierung ist kein Fehlerzustand.** Die Registrierung wird stündlich
|
||||
von `app:calendar:push` wiederholt, statt an den Klick gebunden zu sein, mit dem ein Kalender
|
||||
|
||||
@@ -56,7 +56,7 @@ These are the variables in `.env`, in the order they appear there.
|
||||
| `APP_DEMO_IMPRESSUM_EMAIL` | Contact address on the same page, rendered as a `mailto:` link. | blank | As above | As above. |
|
||||
| `APP_DEMO_PRIVACY_HOST` | The company operating the server, named in the demo's privacy notice (`/datenschutz`) as a processor. | blank | For a public demo, yes | Blank and the page renders a visible warning instead of a name. Whoever runs the machine processes visitors' IP addresses on your behalf and has to be named; check you have a data processing agreement with them, because the notice says you do. Read only when `APP_DEMO_MODE` is on. |
|
||||
| `MERCURE_URL` | The hub address the **app** publishes to, inside the Docker network. | `http://mercure/.well-known/mercure` | Yes | Wrong and nothing publishes: no live updates, no visible error on the page. |
|
||||
| `MERCURE_PUBLIC_URL` | The hub address the **browser** subscribes to. | `https://localhost/.well-known/mercure` in both `.env` and `compose.yaml` | Yes | Wrong and the browser opens a stream to somewhere it cannot reach — mail lists stop updating by themselves while the rest of the app works. Derived from `APP_PUBLIC_URL` only when this is unset or empty, which the stock `compose.yaml` prevents. See [Behind a reverse proxy](reverse-proxy.md). |
|
||||
| `MERCURE_PUBLIC_URL` | The hub address the **browser** subscribes to. | `https://localhost/.well-known/mercure` in both `.env` and `compose.yaml` | Yes | Wrong and the browser opens a stream to somewhere it cannot reach — mail lists stop updating by themselves while the rest of the app works. Derived from `APP_PUBLIC_URL` when this is unset, empty, or still the stock `compose.yaml` default. See [Behind a reverse proxy](reverse-proxy.md). |
|
||||
| `MERCURE_JWT_SECRET` | Signs the publisher and subscriber JWTs. | blank — 32 random bytes, hex, generated on first start | Yes, but generated | The app and the hub must hold the same value. They disagree and the hub rejects every subscriber, silently, from the browser's point of view. |
|
||||
| `MERCURE_COOKIE_NAME` | The name of the cookie that lets the browser subscribe. | blank — plMail picks `__Secure-mercure_access_token` when `APP_PUBLIC_URL` is https and `mercure_access_token` otherwise, since a browser drops a `__Secure-` cookie that did not arrive over HTTPS | No | Set only to use a name of your own; the app sets it and the hub reads it, both from this one variable. The choice is made when the web container starts, so it follows a changed public address at the next restart. |
|
||||
| `MERCURE_UPSTREAM` | Where the web server proxies `/.well-known/mercure` to: the container running the hub, as `host:port`. | `mercure:80` | No | Only for a stack that does not call that container `mercure`. Wrong and the browser's stream answers 502 — the indicator in the top bar stays red while mail still arrives. `MERCURE_URL` has to name the same container. |
|
||||
@@ -92,7 +92,7 @@ reads them directly when it assembles `DATABASE_URL`.
|
||||
|
||||
| Variable | What it does | Default | If it is wrong |
|
||||
|---|---|---|---|
|
||||
| `SERVER_NAME` | The hostname Caddy serves, inside the `php` container. | `localhost, php:80` | Caddy decides whether to terminate TLS from this. `:80` serves plain HTTP, which is what you want behind a reverse proxy. A hostname makes Caddy try to obtain a certificate for it. |
|
||||
| `SERVER_NAME` | The hostname Caddy serves, inside the `php` container. | `localhost, :443, php:80` | Caddy decides whether to terminate TLS from this. The default serves every name and address the machine is reached at with a self-signed certificate. `:80` serves plain HTTP, which is what you want behind a reverse proxy. A hostname makes Caddy try to obtain a certificate for it, and is then the only name served. |
|
||||
| `HTTP_PORT` | Host port mapped to container port 80/tcp. | `80` | A port already in use stops the `php` container from starting. |
|
||||
| `HTTPS_PORT` | Host port mapped to container port 443/tcp. | `443` | As above. |
|
||||
| `HTTP3_PORT` | Host port mapped to container port 443/udp. | `443` | Only matters if you serve HTTP/3 directly. |
|
||||
@@ -227,10 +227,11 @@ cache warmup during the image build with "could not find driver". The entrypoint
|
||||
therefore "does this DSN carry a password", not "is it set". A DSN you supply *with* a password is
|
||||
taken as intent and left completely alone.
|
||||
|
||||
**`MERCURE_PUBLIC_URL` has a default that is not derived.** `config/bootstrap_generated_secrets.php`
|
||||
builds it from `APP_PUBLIC_URL` — but only when it is unset or empty, and `compose.yaml` sets it to
|
||||
`https://localhost/.well-known/mercure` unconditionally. On any install not reached at
|
||||
`https://localhost`, set it explicitly.
|
||||
**`MERCURE_PUBLIC_URL` follows `APP_PUBLIC_URL` unless you set it to something else.**
|
||||
`config/bootstrap_generated_secrets.php` builds it from `APP_PUBLIC_URL` when it is unset, empty, or
|
||||
`https://localhost/.well-known/mercure` — the value `compose.yaml` sets on every install, which
|
||||
therefore says nothing about what you wanted. Any other value is taken as intent and left alone.
|
||||
An install with no public address stored keeps the `localhost` one.
|
||||
|
||||
**`GMAIL_PUBSUB_VERIFICATION_TOKEN` blank means "reject everything", not "accept everything".**
|
||||
That is the right default for an endpoint reachable from the internet, but it means an operator who
|
||||
|
||||
@@ -52,6 +52,12 @@ docker compose up -d
|
||||
|
||||
Then open [https://localhost](https://localhost).
|
||||
|
||||
On a machine with no browser of its own, open `https://` and that machine's address or hostname
|
||||
from another one — `https://192.168.1.20`, say. Either way the certificate is one the container
|
||||
signed for itself, so the browser warns once and lets you continue. To serve a certificate browsers
|
||||
trust, set `SERVER_NAME` to a domain that resolves to this machine, or put a
|
||||
[reverse proxy](reverse-proxy.md) in front.
|
||||
|
||||
**Finish the setup screen before you go anywhere else.** Until the first user exists, `/install` is
|
||||
open to whoever reaches it — that is how you create your own account, and it means anybody who
|
||||
reaches the instance first becomes its administrator instead. This is the usual bargain for
|
||||
@@ -77,7 +83,9 @@ knowing about:
|
||||
if they are missing.
|
||||
5. **Caddy serves.** The `php` container publishes 80/tcp, 443/tcp and 443/udp — remappable with
|
||||
`HTTP_PORT`, `HTTPS_PORT` and `HTTP3_PORT` — and serves the names in `SERVER_NAME`, which
|
||||
`compose.yaml` sets to `localhost, php:80`. It also proxies `/.well-known/mercure*` to the hub
|
||||
`compose.yaml` sets to `localhost, :443, php:80`: `localhost` is the name the self-signed
|
||||
certificate is made for, and `:443` answers every other name and address with that same
|
||||
certificate. It also proxies `/.well-known/mercure*` to the hub
|
||||
container, so the browser reaches the hub same-origin and needs no CORS.
|
||||
|
||||
`docker compose up -d --wait` blocks until the healthchecks pass, which is the better form in a
|
||||
|
||||
@@ -19,7 +19,7 @@ services:
|
||||
SERVER_NAME: ":80" # FrankenPHP serves plain HTTP; no certificate here
|
||||
```
|
||||
|
||||
`SERVER_NAME` defaults to `localhost, php:80`, and Caddy inside the container decides from that name
|
||||
`SERVER_NAME` defaults to `localhost, :443, php:80`, and Caddy inside the container decides from that name
|
||||
whether to terminate TLS itself. `:80` tells it not to. Publish only the HTTP port then — `HTTP_PORT`
|
||||
maps the host port to container port 80 — and let the proxy reach it. `truenas.compose.yaml` does
|
||||
exactly this and publishes `30080`.
|
||||
@@ -32,9 +32,8 @@ TRUSTED_PROXIES=127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||
MERCURE_PUBLIC_URL=https://mail.example.com/.well-known/mercure
|
||||
```
|
||||
|
||||
**The failure mode is leaving `SERVER_NAME` at its default behind a proxy.** Caddy then tries to
|
||||
serve `localhost` while the proxy addresses it by container name or IP, and the request does not
|
||||
match a site block.
|
||||
**The failure mode is leaving `SERVER_NAME` at its default behind a proxy.** Caddy then
|
||||
answers the proxy's plain-HTTP request with a redirect to HTTPS, and the browser goes round in a loop.
|
||||
|
||||
## `APP_PUBLIC_URL`
|
||||
|
||||
@@ -113,10 +112,10 @@ Two variables, and they point in opposite directions:
|
||||
`/.well-known/mercure`.
|
||||
|
||||
`config/bootstrap_generated_secrets.php` derives the second from `APP_PUBLIC_URL` so there is one
|
||||
less thing to fill in — but **only when `MERCURE_PUBLIC_URL` is unset or empty**, and the stock
|
||||
`compose.yaml` sets it unconditionally to `https://localhost/.well-known/mercure`. On a proxied
|
||||
install, set it explicitly. `truenas.compose.yaml` leaves its own value blank precisely so the
|
||||
derivation can happen.
|
||||
less thing to fill in. It does so when `MERCURE_PUBLIC_URL` is unset, empty, or still
|
||||
`https://localhost/.well-known/mercure`, the value the stock `compose.yaml` sets on every install.
|
||||
Any other value is yours and is left alone. So a proxied install needs `APP_PUBLIC_URL` and nothing
|
||||
more — from the environment as above, or from the setup screen.
|
||||
|
||||
The scheme matters as well as the host: Symfony derives the subscriber cookie's `secure` flag from
|
||||
this URL, so an `https` value on a plain-HTTP install mints a cookie the browser will not send back.
|
||||
@@ -221,10 +220,10 @@ failure and retries with backoff.
|
||||
|
||||
## Things that bite
|
||||
|
||||
**`MERCURE_PUBLIC_URL` is not derived on the stock compose file.** The derivation from
|
||||
`APP_PUBLIC_URL` only happens when the variable is unset or empty, and `compose.yaml` sets it to
|
||||
`https://localhost/.well-known/mercure`. This is the single most likely reason live updates work in
|
||||
development and not in production.
|
||||
**`MERCURE_PUBLIC_URL` is only derived once a public address exists.** With no `APP_PUBLIC_URL` in
|
||||
the environment and none stored by the setup screen, the stock compose file's
|
||||
`https://localhost/.well-known/mercure` stays, and live updates work at `https://localhost` and
|
||||
nowhere else. Admin → Address shows what is stored.
|
||||
|
||||
**Push registration failing is not an error state.** Registration is retried hourly by
|
||||
`app:calendar:push` rather than being tied to the click that connected a calendar, so an install
|
||||
|
||||
@@ -10,6 +10,25 @@
|
||||
# the same reason (BackgroundProcesses); browsers reconnect.
|
||||
grace_period 5s
|
||||
|
||||
# The certificate a browser gets when it asked for a name this server
|
||||
# holds none for — `default_sni` for a handshake that names nothing,
|
||||
# which is every one to an IP address, `fallback_sni` for a name
|
||||
# nobody listed, such as the machine's own hostname on a LAN.
|
||||
#
|
||||
# Without them Caddy has nothing to offer and ends the handshake. The
|
||||
# browser reports that as a protocol error, not a certificate error,
|
||||
# and a protocol error has no "proceed anyway": an install on a
|
||||
# headless machine could not be opened from any other machine, and the
|
||||
# README's "open https://localhost" was the only address that worked.
|
||||
# With them the answer is the self-signed `localhost` certificate and
|
||||
# the warning every browser lets a person click through.
|
||||
#
|
||||
# Inert wherever there is no such certificate to hand out: an install
|
||||
# serving a real domain never issues one, and SERVER_NAME ":80" does
|
||||
# no TLS at all. compose.yaml's default SERVER_NAME is the other half.
|
||||
default_sni localhost
|
||||
fallback_sni localhost
|
||||
|
||||
{$CADDY_GLOBAL_OPTIONS}
|
||||
frankenphp {
|
||||
{$FRANKENPHP_CONFIG}
|
||||
|
||||
@@ -88,6 +88,32 @@ final class GeneratedSecretsBootstrapTest extends TestCase
|
||||
self::assertSame('(unset)', $this->mercurePublicUrlSeenBy([]));
|
||||
}
|
||||
|
||||
/**
|
||||
* The stock compose file sets the hub address to the `localhost` one on
|
||||
* every install, so it says nothing about what the operator wanted. An
|
||||
* install set up from another machine on the LAN kept it, and every browser
|
||||
* was told to subscribe on its own loopback.
|
||||
*/
|
||||
public function testTheStockComposePlaceholderGivesWayToTheAddressChosenDuringSetup(): void
|
||||
{
|
||||
file_put_contents($this->secretsFile, "APP_PUBLIC_URL=https://192.168.2.2\n");
|
||||
|
||||
self::assertSame(
|
||||
'https://192.168.2.2/.well-known/mercure',
|
||||
$this->mercurePublicUrlSeenBy(['MERCURE_PUBLIC_URL' => 'https://localhost/.well-known/mercure']),
|
||||
);
|
||||
}
|
||||
|
||||
public function testThePlaceholderStaysWhereNoAddressWasEverStored(): void
|
||||
{
|
||||
// An install older than the setup question has no address on file and
|
||||
// is reached at https://localhost, where this value is the right one.
|
||||
self::assertSame(
|
||||
'https://localhost/.well-known/mercure',
|
||||
$this->mercurePublicUrlSeenBy(['MERCURE_PUBLIC_URL' => 'https://localhost/.well-known/mercure']),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The subscriber cookie's prefix follows the public address, because a
|
||||
* browser only keeps a `__Secure-` cookie that arrived over HTTPS. An
|
||||
|
||||
Reference in new issue
Block a user