Outline runs as an unprivileged user in the image (``nodejs`` since 1.10.0, ``root`` up to 1.6.1) while the datastore is provisioned by ``root``. Without realignment the application cannot write its ``uploads``, ``public`` and ``avatars`` buckets, so every attachment upload fails with "Permission denied writing to ... Check the host machine file system permissions". This was seen on elabore.coop after the 1.6.1 to 1.10.0 upgrade. The ``init`` hook now reads the image's ``Config.User`` and chowns the datastore to that user, skipping root-based images. It is idempotent and version-agnostic: the realignment also covers buckets added by future Outline versions.
134 lines
5.0 KiB
Org Mode
134 lines
5.0 KiB
Org Mode
# -*- ispell-local-dictionary: "english" -*-
|
||
|
||
* Info
|
||
|
||
From: https://docs.getoutline.com/s/hosting/doc/docker-7pfeLP5a8t
|
||
|
||
|
||
* Usage
|
||
|
||
Config info: https://github.com/outline/outline/blob/main/.env.sample
|
||
|
||
Odoo config: if you configure odoo OIDC connector, the callback url
|
||
should be like this : https://<YOUR_OUTLINE>:443/auth/oidc.callback
|
||
|
||
|
||
#Requires a =smtp-server= provider to be functional, you can use
|
||
#=smtp-stub= charm to provide information to externally managed =SMTP=.
|
||
|
||
#+begin_src yaml
|
||
outline:
|
||
options:
|
||
sender-email: #the sender email (beware the conf of your SMTP server)
|
||
oidc-client-id: #the client id of your OIDC provider
|
||
oidc-client-secret: #the client
|
||
oidc-auth-uri: #the host of your OIDC provider
|
||
oidc-token-uri: #the token uri of your OIDC provider
|
||
oidc-user-info-uri: #the user info uri of your OIDC provider
|
||
oidc-logout-uri: #the login uri of your OIDC provider
|
||
|
||
#smtp-stub:
|
||
# options:
|
||
# host: smtp.myhost.com
|
||
# port: 465
|
||
# connection-security: "ssl/tls"
|
||
# auth-method: password #IMPORTANT: if not present login password doesn’t work
|
||
# login: myuser
|
||
# password: myp4ssw0rd
|
||
|
||
#+end_src
|
||
|
||
** Odoo 14
|
||
|
||
We monkey-patch odoo in order to make it work, be sure to use latest version in 14.0 of galicea openIDConnection module
|
||
|
||
* Database ownership alignment
|
||
|
||
The =pre_deploy= hook ensures that every object of the database
|
||
(tables, sequences, views, materialized views, standalone types,
|
||
functions, procedures) is owned by the application role before the
|
||
container starts and runs its migrations.
|
||
|
||
Historical provisioning or restores executed as the =postgres=
|
||
superuser leave objects owned by =postgres=, which makes any later
|
||
=ALTER= on these objects fail with "must be owner of ..." and puts
|
||
outline in a crash-loop at migration time. This was seen on
|
||
2026-09-11 when upgrading elabore.coop from 1.6.1 to 1.10.0:
|
||
migration =20260714000000-add-mcp-to-search-queries-source.js=
|
||
failed on =enum_search_queries_source=. The same drift was found
|
||
on every managed server (lokavaluto.fr, lagemme.org, moneko.org).
|
||
|
||
Extensions are excluded from the realignment (they are managed by
|
||
the =postgres= charm). The hook is idempotent and silent when
|
||
there is no drift, and blocks the deployment (=exit 1=) if the
|
||
realignment fails, so the problem is visible at deploy time instead
|
||
of as a cryptic crash-loop.
|
||
|
||
* Datastore ownership alignment
|
||
|
||
The =init= hook aligns the ownership of the service datastore with
|
||
the user the Outline container runs as. Since 1.10.0 the image runs
|
||
as the unprivileged =nodejs= user (older images, up to 1.6.1, ran as
|
||
=root=), while the datastore is provisioned by =root=. Without
|
||
realignment the application cannot write its =uploads=, =public= and
|
||
=avatars= buckets and every attachment upload fails with "Permission
|
||
denied writing to ... Check the host machine file system
|
||
permissions". This was seen on 2026-09-11 on elabore.coop after the
|
||
1.6.1 to 1.10.0 upgrade, on every existing datastore.
|
||
|
||
The hook reads the user from the image's =Config.User=, so it stays
|
||
version-agnostic: images running as =root= are left untouched, and
|
||
re-running the hook on an already aligned datastore is a no-op.
|
||
|
||
* Building a new image
|
||
|
||
We use the official image with an added patch due to 2 bugs:
|
||
- https://github.com/outline/outline/issues/6859
|
||
- second was not reported yet
|
||
|
||
Note that a PR was pushed with a fix on the first bug. But this was not yet tested.
|
||
|
||
The fix are on 1.6.1
|
||
|
||
** Fix
|
||
|
||
Upon calling "/oidc" url, outline will return "Set-Cookie" header
|
||
with a "domain:" value that is incorrect (still the inner docker
|
||
domain: "outline" instead of the outer proxy domain from the frontend.)
|
||
|
||
Fortunately we can simply remove the value "domain" from the cookie by
|
||
commenting only 2 lines in ~build/server/utils/passport.js~.
|
||
|
||
The patches will change the "build/" files, so this is a very temporary and brittle fix.
|
||
|
||
|
||
#+begin_src bash
|
||
IMAGE=docker.0k.io/outline:1.6.1-elabore
|
||
|
||
echo 'apt update && apt install patch' | dupd -u "$IMAGE" -- -u 0
|
||
cat <<'EOF1' | dupd -u "$IMAGE" -- -u 0
|
||
patch -p 1 <<'EOF2'
|
||
--- a/build/server/utils/passport.js.orig
|
||
+++ b/build/server/utils/passport.js
|
||
@@ -56,7 +56,7 @@
|
||
const state = buildState(host, token, client);
|
||
ctx.cookies.set(this.key, state, {
|
||
expires: (0, _dateFns.addMinutes)(new Date(), 10),
|
||
- domain: (0, _domains.getCookieDomain)(ctx.hostname, _env.default.isCloudHosted)
|
||
+ //domain: (0, _domains.getCookieDomain)(ctx.hostname, _env.default.isCloudHosted)
|
||
});
|
||
callback(null, token);
|
||
});
|
||
@@ -73,7 +73,7 @@
|
||
// Destroy the one-time pad token and ensure it matches
|
||
ctx.cookies.set(this.key, "", {
|
||
expires: (0, _dateFns.subMinutes)(new Date(), 1),
|
||
- domain: (0, _domains.getCookieDomain)(ctx.hostname, _env.default.isCloudHosted)
|
||
+ //domain: (0, _domains.getCookieDomain)(ctx.hostname, _env.default.isCloudHosted)
|
||
});
|
||
if (!token || token !== providedToken) {
|
||
return callback((0, _errors.OAuthStateMismatchError)(), false, token);
|
||
EOF2
|
||
EOF1
|
||
#+end_src
|