about summary refs log tree commit diff
path: root/nixos/modules/services/web-apps
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/services/web-apps')
-rw-r--r--nixos/modules/services/web-apps/akkoma.xml4
-rw-r--r--nixos/modules/services/web-apps/discourse.md286
-rw-r--r--nixos/modules/services/web-apps/discourse.xml532
-rw-r--r--nixos/modules/services/web-apps/grocy.md66
-rw-r--r--nixos/modules/services/web-apps/grocy.xml101
-rw-r--r--nixos/modules/services/web-apps/jitsi-meet.md45
-rw-r--r--nixos/modules/services/web-apps/jitsi-meet.xml88
-rw-r--r--nixos/modules/services/web-apps/keycloak.md141
-rw-r--r--nixos/modules/services/web-apps/keycloak.xml369
-rw-r--r--nixos/modules/services/web-apps/lemmy.nix2
-rw-r--r--nixos/modules/services/web-apps/lemmy.xml2
-rw-r--r--nixos/modules/services/web-apps/matomo-doc.xml107
-rw-r--r--nixos/modules/services/web-apps/matomo.md77
-rw-r--r--nixos/modules/services/web-apps/matomo.nix2
-rw-r--r--nixos/modules/services/web-apps/matomo.xml107
-rw-r--r--nixos/modules/services/web-apps/nextcloud.md237
-rw-r--r--nixos/modules/services/web-apps/nextcloud.xml536
-rw-r--r--nixos/modules/services/web-apps/pict-rs.md1
-rw-r--r--nixos/modules/services/web-apps/pict-rs.nix2
-rw-r--r--nixos/modules/services/web-apps/pict-rs.xml47
-rw-r--r--nixos/modules/services/web-apps/plausible.md35
-rw-r--r--nixos/modules/services/web-apps/plausible.xml78
22 files changed, 1878 insertions, 987 deletions
diff --git a/nixos/modules/services/web-apps/akkoma.xml b/nixos/modules/services/web-apps/akkoma.xml
index 76e6b806f30fe..49cbcc911e1d1 100644
--- a/nixos/modules/services/web-apps/akkoma.xml
+++ b/nixos/modules/services/web-apps/akkoma.xml
@@ -1,3 +1,5 @@
+<!-- Do not edit this file directly, edit its companion .md instead
+     and regenerate this file using nixos/doc/manual/md-to-db.sh -->
 <chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-akkoma">
   <title>Akkoma</title>
   <para>
@@ -371,7 +373,7 @@ services.systemd.akkoma.confinement.enable = true;
         and
         <option>services.systemd.akkoma.serviceConfig.BindReadOnlyPaths</option>
         permit access to outside paths through bind mounts. Refer to
-        <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.exec.html#BindPaths="><link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.exec.html"><citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry></link></link>
+        <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.exec.html#BindPaths="><citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry></link>
         for details.
       </para>
     </section>
diff --git a/nixos/modules/services/web-apps/discourse.md b/nixos/modules/services/web-apps/discourse.md
new file mode 100644
index 0000000000000..35180bea87d90
--- /dev/null
+++ b/nixos/modules/services/web-apps/discourse.md
@@ -0,0 +1,286 @@
+# Discourse {#module-services-discourse}
+
+[Discourse](https://www.discourse.org/) is a
+modern and open source discussion platform.
+
+## Basic usage {#module-services-discourse-basic-usage}
+
+A minimal configuration using Let's Encrypt for TLS certificates looks like this:
+```
+services.discourse = {
+  enable = true;
+  hostname = "discourse.example.com";
+  admin = {
+    email = "admin@example.com";
+    username = "admin";
+    fullName = "Administrator";
+    passwordFile = "/path/to/password_file";
+  };
+  secretKeyBaseFile = "/path/to/secret_key_base_file";
+};
+security.acme.email = "me@example.com";
+security.acme.acceptTerms = true;
+```
+
+Provided a proper DNS setup, you'll be able to connect to the
+instance at `discourse.example.com` and log in
+using the credentials provided in
+`services.discourse.admin`.
+
+## Using a regular TLS certificate {#module-services-discourse-tls}
+
+To set up TLS using a regular certificate and key on file, use
+the [](#opt-services.discourse.sslCertificate)
+and [](#opt-services.discourse.sslCertificateKey)
+options:
+
+```
+services.discourse = {
+  enable = true;
+  hostname = "discourse.example.com";
+  sslCertificate = "/path/to/ssl_certificate";
+  sslCertificateKey = "/path/to/ssl_certificate_key";
+  admin = {
+    email = "admin@example.com";
+    username = "admin";
+    fullName = "Administrator";
+    passwordFile = "/path/to/password_file";
+  };
+  secretKeyBaseFile = "/path/to/secret_key_base_file";
+};
+```
+
+## Database access {#module-services-discourse-database}
+
+Discourse uses PostgreSQL to store most of its
+data. A database will automatically be enabled and a database
+and role created unless [](#opt-services.discourse.database.host) is changed from
+its default of `null` or [](#opt-services.discourse.database.createLocally) is set
+to `false`.
+
+External database access can also be configured by setting
+[](#opt-services.discourse.database.host),
+[](#opt-services.discourse.database.username) and
+[](#opt-services.discourse.database.passwordFile) as
+appropriate. Note that you need to manually create a database
+called `discourse` (or the name you chose in
+[](#opt-services.discourse.database.name)) and
+allow the configured database user full access to it.
+
+## Email {#module-services-discourse-mail}
+
+In addition to the basic setup, you'll want to configure an SMTP
+server Discourse can use to send user
+registration and password reset emails, among others. You can
+also optionally let Discourse receive
+email, which enables people to reply to threads and conversations
+via email.
+
+A basic setup which assumes you want to use your configured
+[hostname](#opt-services.discourse.hostname) as
+email domain can be done like this:
+
+```
+services.discourse = {
+  enable = true;
+  hostname = "discourse.example.com";
+  sslCertificate = "/path/to/ssl_certificate";
+  sslCertificateKey = "/path/to/ssl_certificate_key";
+  admin = {
+    email = "admin@example.com";
+    username = "admin";
+    fullName = "Administrator";
+    passwordFile = "/path/to/password_file";
+  };
+  mail.outgoing = {
+    serverAddress = "smtp.emailprovider.com";
+    port = 587;
+    username = "user@emailprovider.com";
+    passwordFile = "/path/to/smtp_password_file";
+  };
+  mail.incoming.enable = true;
+  secretKeyBaseFile = "/path/to/secret_key_base_file";
+};
+```
+
+This assumes you have set up an MX record for the address you've
+set in [hostname](#opt-services.discourse.hostname) and
+requires proper SPF, DKIM and DMARC configuration to be done for
+the domain you're sending from, in order for email to be reliably delivered.
+
+If you want to use a different domain for your outgoing email
+(for example `example.com` instead of
+`discourse.example.com`) you should set
+[](#opt-services.discourse.mail.notificationEmailAddress) and
+[](#opt-services.discourse.mail.contactEmailAddress) manually.
+
+::: {.note}
+Setup of TLS for incoming email is currently only configured
+automatically when a regular TLS certificate is used, i.e. when
+[](#opt-services.discourse.sslCertificate) and
+[](#opt-services.discourse.sslCertificateKey) are
+set.
+:::
+
+## Additional settings {#module-services-discourse-settings}
+
+Additional site settings and backend settings, for which no
+explicit NixOS options are provided,
+can be set in [](#opt-services.discourse.siteSettings) and
+[](#opt-services.discourse.backendSettings) respectively.
+
+### Site settings {#module-services-discourse-site-settings}
+
+"Site settings" are the settings that can be
+changed through the Discourse
+UI. Their *default* values can be set using
+[](#opt-services.discourse.siteSettings).
+
+Settings are expressed as a Nix attribute set which matches the
+structure of the configuration in
+[config/site_settings.yml](https://github.com/discourse/discourse/blob/master/config/site_settings.yml).
+To find a setting's path, you only need to care about the first
+two levels; i.e. its category (e.g. `login`)
+and name (e.g. `invite_only`).
+
+Settings containing secret data should be set to an attribute
+set containing the attribute `_secret` - a
+string pointing to a file containing the value the option
+should be set to. See the example.
+
+### Backend settings {#module-services-discourse-backend-settings}
+
+Settings are expressed as a Nix attribute set which matches the
+structure of the configuration in
+[config/discourse.conf](https://github.com/discourse/discourse/blob/stable/config/discourse_defaults.conf).
+Empty parameters can be defined by setting them to
+`null`.
+
+### Example {#module-services-discourse-settings-example}
+
+The following example sets the title and description of the
+Discourse instance and enables
+GitHub login in the site settings,
+and changes a few request limits in the backend settings:
+```
+services.discourse = {
+  enable = true;
+  hostname = "discourse.example.com";
+  sslCertificate = "/path/to/ssl_certificate";
+  sslCertificateKey = "/path/to/ssl_certificate_key";
+  admin = {
+    email = "admin@example.com";
+    username = "admin";
+    fullName = "Administrator";
+    passwordFile = "/path/to/password_file";
+  };
+  mail.outgoing = {
+    serverAddress = "smtp.emailprovider.com";
+    port = 587;
+    username = "user@emailprovider.com";
+    passwordFile = "/path/to/smtp_password_file";
+  };
+  mail.incoming.enable = true;
+  siteSettings = {
+    required = {
+      title = "My Cats";
+      site_description = "Discuss My Cats (and be nice plz)";
+    };
+    login = {
+      enable_github_logins = true;
+      github_client_id = "a2f6dfe838cb3206ce20";
+      github_client_secret._secret = /run/keys/discourse_github_client_secret;
+    };
+  };
+  backendSettings = {
+    max_reqs_per_ip_per_minute = 300;
+    max_reqs_per_ip_per_10_seconds = 60;
+    max_asset_reqs_per_ip_per_10_seconds = 250;
+    max_reqs_per_ip_mode = "warn+block";
+  };
+  secretKeyBaseFile = "/path/to/secret_key_base_file";
+};
+```
+
+In the resulting site settings file, the
+`login.github_client_secret` key will be set
+to the contents of the
+{file}`/run/keys/discourse_github_client_secret`
+file.
+
+## Plugins {#module-services-discourse-plugins}
+
+You can install Discourse plugins
+using the [](#opt-services.discourse.plugins)
+option. Pre-packaged plugins are provided in
+`<your_discourse_package_here>.plugins`. If
+you want the full suite of plugins provided through
+`nixpkgs`, you can also set the [](#opt-services.discourse.package) option to
+`pkgs.discourseAllPlugins`.
+
+Plugins can be built with the
+`<your_discourse_package_here>.mkDiscoursePlugin`
+function. Normally, it should suffice to provide a
+`name` and `src` attribute. If
+the plugin has Ruby dependencies, however, they need to be
+packaged in accordance with the [Developing with Ruby](https://nixos.org/manual/nixpkgs/stable/#developing-with-ruby)
+section of the Nixpkgs manual and the
+appropriate gem options set in `bundlerEnvArgs`
+(normally `gemdir` is sufficient). A plugin's
+Ruby dependencies are listed in its
+{file}`plugin.rb` file as function calls to
+`gem`. To construct the corresponding
+{file}`Gemfile` manually, run {command}`bundle init`, then add the `gem` lines to it
+verbatim.
+
+Much of the packaging can be done automatically by the
+{file}`nixpkgs/pkgs/servers/web-apps/discourse/update.py`
+script - just add the plugin to the `plugins`
+list in the `update_plugins` function and run
+the script:
+```bash
+./update.py update-plugins
+```
+
+Some plugins provide [site settings](#module-services-discourse-site-settings).
+Their defaults can be configured using [](#opt-services.discourse.siteSettings), just like
+regular site settings. To find the names of these settings, look
+in the `config/settings.yml` file of the plugin
+repo.
+
+For example, to add the [discourse-spoiler-alert](https://github.com/discourse/discourse-spoiler-alert)
+and [discourse-solved](https://github.com/discourse/discourse-solved)
+plugins, and disable `discourse-spoiler-alert`
+by default:
+
+```
+services.discourse = {
+  enable = true;
+  hostname = "discourse.example.com";
+  sslCertificate = "/path/to/ssl_certificate";
+  sslCertificateKey = "/path/to/ssl_certificate_key";
+  admin = {
+    email = "admin@example.com";
+    username = "admin";
+    fullName = "Administrator";
+    passwordFile = "/path/to/password_file";
+  };
+  mail.outgoing = {
+    serverAddress = "smtp.emailprovider.com";
+    port = 587;
+    username = "user@emailprovider.com";
+    passwordFile = "/path/to/smtp_password_file";
+  };
+  mail.incoming.enable = true;
+  plugins = with config.services.discourse.package.plugins; [
+    discourse-spoiler-alert
+    discourse-solved
+  ];
+  siteSettings = {
+    plugins = {
+      spoiler_enabled = false;
+    };
+  };
+  secretKeyBaseFile = "/path/to/secret_key_base_file";
+};
+```
diff --git a/nixos/modules/services/web-apps/discourse.xml b/nixos/modules/services/web-apps/discourse.xml
index ad9b65abf51e0..a5e8b3656b7de 100644
--- a/nixos/modules/services/web-apps/discourse.xml
+++ b/nixos/modules/services/web-apps/discourse.xml
@@ -1,355 +1,331 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-discourse">
- <title>Discourse</title>
- <para>
-   <link xlink:href="https://www.discourse.org/">Discourse</link> is a
-   modern and open source discussion platform.
- </para>
-
- <section xml:id="module-services-discourse-basic-usage">
-   <title>Basic usage</title>
-   <para>
-     A minimal configuration using Let's Encrypt for TLS certificates looks like this:
-<programlisting>
+<!-- Do not edit this file directly, edit its companion .md instead
+     and regenerate this file using nixos/doc/manual/md-to-db.sh -->
+<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-discourse">
+  <title>Discourse</title>
+  <para>
+    <link xlink:href="https://www.discourse.org/">Discourse</link> is a
+    modern and open source discussion platform.
+  </para>
+  <section xml:id="module-services-discourse-basic-usage">
+    <title>Basic usage</title>
+    <para>
+      A minimal configuration using Let’s Encrypt for TLS certificates
+      looks like this:
+    </para>
+    <programlisting>
 services.discourse = {
-  <link linkend="opt-services.discourse.enable">enable</link> = true;
-  <link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
+  enable = true;
+  hostname = &quot;discourse.example.com&quot;;
   admin = {
-    <link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
-    <link linkend="opt-services.discourse.admin.username">username</link> = "admin";
-    <link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
-    <link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
+    email = &quot;admin@example.com&quot;;
+    username = &quot;admin&quot;;
+    fullName = &quot;Administrator&quot;;
+    passwordFile = &quot;/path/to/password_file&quot;;
   };
-  <link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
+  secretKeyBaseFile = &quot;/path/to/secret_key_base_file&quot;;
 };
-<link linkend="opt-security.acme.defaults.email">security.acme.email</link> = "me@example.com";
-<link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
+security.acme.email = &quot;me@example.com&quot;;
+security.acme.acceptTerms = true;
 </programlisting>
-   </para>
-
-   <para>
-     Provided a proper DNS setup, you'll be able to connect to the
-     instance at <literal>discourse.example.com</literal> and log in
-     using the credentials provided in
-     <literal>services.discourse.admin</literal>.
-   </para>
- </section>
-
- <section xml:id="module-services-discourse-tls">
-   <title>Using a regular TLS certificate</title>
-   <para>
-     To set up TLS using a regular certificate and key on file, use
-     the <xref linkend="opt-services.discourse.sslCertificate" />
-     and <xref linkend="opt-services.discourse.sslCertificateKey" />
-     options:
-
-<programlisting>
+    <para>
+      Provided a proper DNS setup, you’ll be able to connect to the
+      instance at <literal>discourse.example.com</literal> and log in
+      using the credentials provided in
+      <literal>services.discourse.admin</literal>.
+    </para>
+  </section>
+  <section xml:id="module-services-discourse-tls">
+    <title>Using a regular TLS certificate</title>
+    <para>
+      To set up TLS using a regular certificate and key on file, use the
+      <xref linkend="opt-services.discourse.sslCertificate" /> and
+      <xref linkend="opt-services.discourse.sslCertificateKey" />
+      options:
+    </para>
+    <programlisting>
 services.discourse = {
-  <link linkend="opt-services.discourse.enable">enable</link> = true;
-  <link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
-  <link linkend="opt-services.discourse.sslCertificate">sslCertificate</link> = "/path/to/ssl_certificate";
-  <link linkend="opt-services.discourse.sslCertificateKey">sslCertificateKey</link> = "/path/to/ssl_certificate_key";
+  enable = true;
+  hostname = &quot;discourse.example.com&quot;;
+  sslCertificate = &quot;/path/to/ssl_certificate&quot;;
+  sslCertificateKey = &quot;/path/to/ssl_certificate_key&quot;;
   admin = {
-    <link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
-    <link linkend="opt-services.discourse.admin.username">username</link> = "admin";
-    <link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
-    <link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
+    email = &quot;admin@example.com&quot;;
+    username = &quot;admin&quot;;
+    fullName = &quot;Administrator&quot;;
+    passwordFile = &quot;/path/to/password_file&quot;;
   };
-  <link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
+  secretKeyBaseFile = &quot;/path/to/secret_key_base_file&quot;;
 };
 </programlisting>
-
-   </para>
- </section>
-
- <section xml:id="module-services-discourse-database">
-   <title>Database access</title>
-   <para>
-     <productname>Discourse</productname> uses
-     <productname>PostgreSQL</productname> to store most of its
-     data. A database will automatically be enabled and a database
-     and role created unless <xref
-     linkend="opt-services.discourse.database.host" /> is changed from
-     its default of <literal>null</literal> or <xref
-     linkend="opt-services.discourse.database.createLocally" /> is set
-     to <literal>false</literal>.
-   </para>
-
-   <para>
-     External database access can also be configured by setting
-     <xref linkend="opt-services.discourse.database.host" />, <xref
-     linkend="opt-services.discourse.database.username" /> and <xref
-     linkend="opt-services.discourse.database.passwordFile" /> as
-     appropriate. Note that you need to manually create a database
-     called <literal>discourse</literal> (or the name you chose in
-     <xref linkend="opt-services.discourse.database.name" />) and
-     allow the configured database user full access to it.
-   </para>
- </section>
-
- <section xml:id="module-services-discourse-mail">
-   <title>Email</title>
-   <para>
-     In addition to the basic setup, you'll want to configure an SMTP
-     server <productname>Discourse</productname> can use to send user
-     registration and password reset emails, among others. You can
-     also optionally let <productname>Discourse</productname> receive
-     email, which enables people to reply to threads and conversations
-     via email.
-   </para>
-
-   <para>
-     A basic setup which assumes you want to use your configured <link
-     linkend="opt-services.discourse.hostname">hostname</link> as
-     email domain can be done like this:
-
-<programlisting>
+  </section>
+  <section xml:id="module-services-discourse-database">
+    <title>Database access</title>
+    <para>
+      Discourse uses PostgreSQL to store most of its data. A database
+      will automatically be enabled and a database and role created
+      unless <xref linkend="opt-services.discourse.database.host" /> is
+      changed from its default of <literal>null</literal> or
+      <xref linkend="opt-services.discourse.database.createLocally" />
+      is set to <literal>false</literal>.
+    </para>
+    <para>
+      External database access can also be configured by setting
+      <xref linkend="opt-services.discourse.database.host" />,
+      <xref linkend="opt-services.discourse.database.username" /> and
+      <xref linkend="opt-services.discourse.database.passwordFile" /> as
+      appropriate. Note that you need to manually create a database
+      called <literal>discourse</literal> (or the name you chose in
+      <xref linkend="opt-services.discourse.database.name" />) and allow
+      the configured database user full access to it.
+    </para>
+  </section>
+  <section xml:id="module-services-discourse-mail">
+    <title>Email</title>
+    <para>
+      In addition to the basic setup, you’ll want to configure an SMTP
+      server Discourse can use to send user registration and password
+      reset emails, among others. You can also optionally let Discourse
+      receive email, which enables people to reply to threads and
+      conversations via email.
+    </para>
+    <para>
+      A basic setup which assumes you want to use your configured
+      <link linkend="opt-services.discourse.hostname">hostname</link> as
+      email domain can be done like this:
+    </para>
+    <programlisting>
 services.discourse = {
-  <link linkend="opt-services.discourse.enable">enable</link> = true;
-  <link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
-  <link linkend="opt-services.discourse.sslCertificate">sslCertificate</link> = "/path/to/ssl_certificate";
-  <link linkend="opt-services.discourse.sslCertificateKey">sslCertificateKey</link> = "/path/to/ssl_certificate_key";
+  enable = true;
+  hostname = &quot;discourse.example.com&quot;;
+  sslCertificate = &quot;/path/to/ssl_certificate&quot;;
+  sslCertificateKey = &quot;/path/to/ssl_certificate_key&quot;;
   admin = {
-    <link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
-    <link linkend="opt-services.discourse.admin.username">username</link> = "admin";
-    <link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
-    <link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
+    email = &quot;admin@example.com&quot;;
+    username = &quot;admin&quot;;
+    fullName = &quot;Administrator&quot;;
+    passwordFile = &quot;/path/to/password_file&quot;;
   };
   mail.outgoing = {
-    <link linkend="opt-services.discourse.mail.outgoing.serverAddress">serverAddress</link> = "smtp.emailprovider.com";
-    <link linkend="opt-services.discourse.mail.outgoing.port">port</link> = 587;
-    <link linkend="opt-services.discourse.mail.outgoing.username">username</link> = "user@emailprovider.com";
-    <link linkend="opt-services.discourse.mail.outgoing.passwordFile">passwordFile</link> = "/path/to/smtp_password_file";
+    serverAddress = &quot;smtp.emailprovider.com&quot;;
+    port = 587;
+    username = &quot;user@emailprovider.com&quot;;
+    passwordFile = &quot;/path/to/smtp_password_file&quot;;
   };
-  <link linkend="opt-services.discourse.mail.incoming.enable">mail.incoming.enable</link> = true;
-  <link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
+  mail.incoming.enable = true;
+  secretKeyBaseFile = &quot;/path/to/secret_key_base_file&quot;;
 };
 </programlisting>
-
-     This assumes you have set up an MX record for the address you've
-     set in <link linkend="opt-services.discourse.hostname">hostname</link> and
-     requires proper SPF, DKIM and DMARC configuration to be done for
-     the domain you're sending from, in order for email to be reliably delivered.
-   </para>
-
-   <para>
-     If you want to use a different domain for your outgoing email
-     (for example <literal>example.com</literal> instead of
-     <literal>discourse.example.com</literal>) you should set
-     <xref linkend="opt-services.discourse.mail.notificationEmailAddress" /> and
-     <xref linkend="opt-services.discourse.mail.contactEmailAddress" /> manually.
-   </para>
-
-   <note>
-     <para>
-       Setup of TLS for incoming email is currently only configured
-       automatically when a regular TLS certificate is used, i.e. when
-       <xref linkend="opt-services.discourse.sslCertificate" /> and
-       <xref linkend="opt-services.discourse.sslCertificateKey" /> are
-       set.
-     </para>
-   </note>
-
- </section>
-
- <section xml:id="module-services-discourse-settings">
-   <title>Additional settings</title>
-   <para>
-     Additional site settings and backend settings, for which no
-     explicit <productname>NixOS</productname> options are provided,
-     can be set in <xref linkend="opt-services.discourse.siteSettings" /> and
-     <xref linkend="opt-services.discourse.backendSettings" /> respectively.
-   </para>
-
-   <section xml:id="module-services-discourse-site-settings">
-     <title>Site settings</title>
-     <para>
-       <quote>Site settings</quote> are the settings that can be
-       changed through the <productname>Discourse</productname>
-       UI. Their <emphasis>default</emphasis> values can be set using
-       <xref linkend="opt-services.discourse.siteSettings" />.
-     </para>
-
-     <para>
-       Settings are expressed as a Nix attribute set which matches the
-       structure of the configuration in
-       <link xlink:href="https://github.com/discourse/discourse/blob/master/config/site_settings.yml">config/site_settings.yml</link>.
-       To find a setting's path, you only need to care about the first
-       two levels; i.e. its category (e.g. <literal>login</literal>)
-       and name (e.g. <literal>invite_only</literal>).
-     </para>
-
-     <para>
-       Settings containing secret data should be set to an attribute
-       set containing the attribute <literal>_secret</literal> - a
-       string pointing to a file containing the value the option
-       should be set to. See the example.
-     </para>
-   </section>
-
-   <section xml:id="module-services-discourse-backend-settings">
-     <title>Backend settings</title>
-     <para>
-       Settings are expressed as a Nix attribute set which matches the
-       structure of the configuration in
-       <link xlink:href="https://github.com/discourse/discourse/blob/stable/config/discourse_defaults.conf">config/discourse.conf</link>.
-       Empty parameters can be defined by setting them to
-       <literal>null</literal>.
-     </para>
-   </section>
-
-   <section xml:id="module-services-discourse-settings-example">
-     <title>Example</title>
-     <para>
-       The following example sets the title and description of the
-       <productname>Discourse</productname> instance and enables
-       <productname>GitHub</productname> login in the site settings,
-       and changes a few request limits in the backend settings:
-<programlisting>
+    <para>
+      This assumes you have set up an MX record for the address you’ve
+      set in
+      <link linkend="opt-services.discourse.hostname">hostname</link>
+      and requires proper SPF, DKIM and DMARC configuration to be done
+      for the domain you’re sending from, in order for email to be
+      reliably delivered.
+    </para>
+    <para>
+      If you want to use a different domain for your outgoing email (for
+      example <literal>example.com</literal> instead of
+      <literal>discourse.example.com</literal>) you should set
+      <xref linkend="opt-services.discourse.mail.notificationEmailAddress" />
+      and
+      <xref linkend="opt-services.discourse.mail.contactEmailAddress" />
+      manually.
+    </para>
+    <note>
+      <para>
+        Setup of TLS for incoming email is currently only configured
+        automatically when a regular TLS certificate is used, i.e. when
+        <xref linkend="opt-services.discourse.sslCertificate" /> and
+        <xref linkend="opt-services.discourse.sslCertificateKey" /> are
+        set.
+      </para>
+    </note>
+  </section>
+  <section xml:id="module-services-discourse-settings">
+    <title>Additional settings</title>
+    <para>
+      Additional site settings and backend settings, for which no
+      explicit NixOS options are provided, can be set in
+      <xref linkend="opt-services.discourse.siteSettings" /> and
+      <xref linkend="opt-services.discourse.backendSettings" />
+      respectively.
+    </para>
+    <section xml:id="module-services-discourse-site-settings">
+      <title>Site settings</title>
+      <para>
+        <quote>Site settings</quote> are the settings that can be
+        changed through the Discourse UI. Their
+        <emphasis>default</emphasis> values can be set using
+        <xref linkend="opt-services.discourse.siteSettings" />.
+      </para>
+      <para>
+        Settings are expressed as a Nix attribute set which matches the
+        structure of the configuration in
+        <link xlink:href="https://github.com/discourse/discourse/blob/master/config/site_settings.yml">config/site_settings.yml</link>.
+        To find a setting’s path, you only need to care about the first
+        two levels; i.e. its category (e.g. <literal>login</literal>)
+        and name (e.g. <literal>invite_only</literal>).
+      </para>
+      <para>
+        Settings containing secret data should be set to an attribute
+        set containing the attribute <literal>_secret</literal> - a
+        string pointing to a file containing the value the option should
+        be set to. See the example.
+      </para>
+    </section>
+    <section xml:id="module-services-discourse-backend-settings">
+      <title>Backend settings</title>
+      <para>
+        Settings are expressed as a Nix attribute set which matches the
+        structure of the configuration in
+        <link xlink:href="https://github.com/discourse/discourse/blob/stable/config/discourse_defaults.conf">config/discourse.conf</link>.
+        Empty parameters can be defined by setting them to
+        <literal>null</literal>.
+      </para>
+    </section>
+    <section xml:id="module-services-discourse-settings-example">
+      <title>Example</title>
+      <para>
+        The following example sets the title and description of the
+        Discourse instance and enables GitHub login in the site
+        settings, and changes a few request limits in the backend
+        settings:
+      </para>
+      <programlisting>
 services.discourse = {
-  <link linkend="opt-services.discourse.enable">enable</link> = true;
-  <link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
-  <link linkend="opt-services.discourse.sslCertificate">sslCertificate</link> = "/path/to/ssl_certificate";
-  <link linkend="opt-services.discourse.sslCertificateKey">sslCertificateKey</link> = "/path/to/ssl_certificate_key";
+  enable = true;
+  hostname = &quot;discourse.example.com&quot;;
+  sslCertificate = &quot;/path/to/ssl_certificate&quot;;
+  sslCertificateKey = &quot;/path/to/ssl_certificate_key&quot;;
   admin = {
-    <link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
-    <link linkend="opt-services.discourse.admin.username">username</link> = "admin";
-    <link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
-    <link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
+    email = &quot;admin@example.com&quot;;
+    username = &quot;admin&quot;;
+    fullName = &quot;Administrator&quot;;
+    passwordFile = &quot;/path/to/password_file&quot;;
   };
   mail.outgoing = {
-    <link linkend="opt-services.discourse.mail.outgoing.serverAddress">serverAddress</link> = "smtp.emailprovider.com";
-    <link linkend="opt-services.discourse.mail.outgoing.port">port</link> = 587;
-    <link linkend="opt-services.discourse.mail.outgoing.username">username</link> = "user@emailprovider.com";
-    <link linkend="opt-services.discourse.mail.outgoing.passwordFile">passwordFile</link> = "/path/to/smtp_password_file";
+    serverAddress = &quot;smtp.emailprovider.com&quot;;
+    port = 587;
+    username = &quot;user@emailprovider.com&quot;;
+    passwordFile = &quot;/path/to/smtp_password_file&quot;;
   };
-  <link linkend="opt-services.discourse.mail.incoming.enable">mail.incoming.enable</link> = true;
-  <link linkend="opt-services.discourse.siteSettings">siteSettings</link> = {
+  mail.incoming.enable = true;
+  siteSettings = {
     required = {
-      title = "My Cats";
-      site_description = "Discuss My Cats (and be nice plz)";
+      title = &quot;My Cats&quot;;
+      site_description = &quot;Discuss My Cats (and be nice plz)&quot;;
     };
     login = {
       enable_github_logins = true;
-      github_client_id = "a2f6dfe838cb3206ce20";
+      github_client_id = &quot;a2f6dfe838cb3206ce20&quot;;
       github_client_secret._secret = /run/keys/discourse_github_client_secret;
     };
   };
-  <link linkend="opt-services.discourse.backendSettings">backendSettings</link> = {
+  backendSettings = {
     max_reqs_per_ip_per_minute = 300;
     max_reqs_per_ip_per_10_seconds = 60;
     max_asset_reqs_per_ip_per_10_seconds = 250;
-    max_reqs_per_ip_mode = "warn+block";
+    max_reqs_per_ip_mode = &quot;warn+block&quot;;
   };
-  <link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
+  secretKeyBaseFile = &quot;/path/to/secret_key_base_file&quot;;
 };
 </programlisting>
-     </para>
-     <para>
-       In the resulting site settings file, the
-       <literal>login.github_client_secret</literal> key will be set
-       to the contents of the
-       <filename>/run/keys/discourse_github_client_secret</filename>
-       file.
-     </para>
-   </section>
- </section>
+      <para>
+        In the resulting site settings file, the
+        <literal>login.github_client_secret</literal> key will be set to
+        the contents of the
+        <filename>/run/keys/discourse_github_client_secret</filename>
+        file.
+      </para>
+    </section>
+  </section>
   <section xml:id="module-services-discourse-plugins">
     <title>Plugins</title>
     <para>
-      You can install <productname>Discourse</productname> plugins
-      using the <xref linkend="opt-services.discourse.plugins" />
-      option. Pre-packaged plugins are provided in
+      You can install Discourse plugins using the
+      <xref linkend="opt-services.discourse.plugins" /> option.
+      Pre-packaged plugins are provided in
       <literal>&lt;your_discourse_package_here&gt;.plugins</literal>. If
       you want the full suite of plugins provided through
-      <literal>nixpkgs</literal>, you can also set the <xref
-      linkend="opt-services.discourse.package" /> option to
+      <literal>nixpkgs</literal>, you can also set the
+      <xref linkend="opt-services.discourse.package" /> option to
       <literal>pkgs.discourseAllPlugins</literal>.
     </para>
-
     <para>
       Plugins can be built with the
       <literal>&lt;your_discourse_package_here&gt;.mkDiscoursePlugin</literal>
       function. Normally, it should suffice to provide a
       <literal>name</literal> and <literal>src</literal> attribute. If
       the plugin has Ruby dependencies, however, they need to be
-      packaged in accordance with the <link
-      xlink:href="https://nixos.org/manual/nixpkgs/stable/#developing-with-ruby">Developing
-      with Ruby</link> section of the Nixpkgs manual and the
-      appropriate gem options set in <literal>bundlerEnvArgs</literal>
-      (normally <literal>gemdir</literal> is sufficient). A plugin's
-      Ruby dependencies are listed in its
-      <filename>plugin.rb</filename> file as function calls to
-      <literal>gem</literal>. To construct the corresponding
-      <filename>Gemfile</filename> manually, run <command>bundle
-      init</command>, then add the <literal>gem</literal> lines to it
-      verbatim.
+      packaged in accordance with the
+      <link xlink:href="https://nixos.org/manual/nixpkgs/stable/#developing-with-ruby">Developing
+      with Ruby</link> section of the Nixpkgs manual and the appropriate
+      gem options set in <literal>bundlerEnvArgs</literal> (normally
+      <literal>gemdir</literal> is sufficient). A plugin’s Ruby
+      dependencies are listed in its <filename>plugin.rb</filename> file
+      as function calls to <literal>gem</literal>. To construct the
+      corresponding <filename>Gemfile</filename> manually, run
+      <command>bundle init</command>, then add the
+      <literal>gem</literal> lines to it verbatim.
     </para>
-
     <para>
       Much of the packaging can be done automatically by the
       <filename>nixpkgs/pkgs/servers/web-apps/discourse/update.py</filename>
       script - just add the plugin to the <literal>plugins</literal>
-      list in the <function>update_plugins</function> function and run
-      the script:
-      <programlisting language="bash">
+      list in the <literal>update_plugins</literal> function and run the
+      script:
+    </para>
+    <programlisting language="bash">
 ./update.py update-plugins
 </programlisting>
-    </para>
-
     <para>
-      Some plugins provide <link
-      linkend="module-services-discourse-site-settings">site
-      settings</link>. Their defaults can be configured using <xref
-      linkend="opt-services.discourse.siteSettings" />, just like
+      Some plugins provide
+      <link linkend="module-services-discourse-site-settings">site
+      settings</link>. Their defaults can be configured using
+      <xref linkend="opt-services.discourse.siteSettings" />, just like
       regular site settings. To find the names of these settings, look
       in the <literal>config/settings.yml</literal> file of the plugin
       repo.
     </para>
-
     <para>
-      For example, to add the <link
-      xlink:href="https://github.com/discourse/discourse-spoiler-alert">discourse-spoiler-alert</link>
-      and <link
-      xlink:href="https://github.com/discourse/discourse-solved">discourse-solved</link>
-      plugins, and disable <literal>discourse-spoiler-alert</literal>
-      by default:
-
-<programlisting>
+      For example, to add the
+      <link xlink:href="https://github.com/discourse/discourse-spoiler-alert">discourse-spoiler-alert</link>
+      and
+      <link xlink:href="https://github.com/discourse/discourse-solved">discourse-solved</link>
+      plugins, and disable <literal>discourse-spoiler-alert</literal> by
+      default:
+    </para>
+    <programlisting>
 services.discourse = {
-  <link linkend="opt-services.discourse.enable">enable</link> = true;
-  <link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
-  <link linkend="opt-services.discourse.sslCertificate">sslCertificate</link> = "/path/to/ssl_certificate";
-  <link linkend="opt-services.discourse.sslCertificateKey">sslCertificateKey</link> = "/path/to/ssl_certificate_key";
+  enable = true;
+  hostname = &quot;discourse.example.com&quot;;
+  sslCertificate = &quot;/path/to/ssl_certificate&quot;;
+  sslCertificateKey = &quot;/path/to/ssl_certificate_key&quot;;
   admin = {
-    <link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
-    <link linkend="opt-services.discourse.admin.username">username</link> = "admin";
-    <link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
-    <link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
+    email = &quot;admin@example.com&quot;;
+    username = &quot;admin&quot;;
+    fullName = &quot;Administrator&quot;;
+    passwordFile = &quot;/path/to/password_file&quot;;
   };
   mail.outgoing = {
-    <link linkend="opt-services.discourse.mail.outgoing.serverAddress">serverAddress</link> = "smtp.emailprovider.com";
-    <link linkend="opt-services.discourse.mail.outgoing.port">port</link> = 587;
-    <link linkend="opt-services.discourse.mail.outgoing.username">username</link> = "user@emailprovider.com";
-    <link linkend="opt-services.discourse.mail.outgoing.passwordFile">passwordFile</link> = "/path/to/smtp_password_file";
+    serverAddress = &quot;smtp.emailprovider.com&quot;;
+    port = 587;
+    username = &quot;user@emailprovider.com&quot;;
+    passwordFile = &quot;/path/to/smtp_password_file&quot;;
   };
-  <link linkend="opt-services.discourse.mail.incoming.enable">mail.incoming.enable</link> = true;
-  <link linkend="opt-services.discourse.mail.incoming.enable">plugins</link> = with config.services.discourse.package.plugins; [
+  mail.incoming.enable = true;
+  plugins = with config.services.discourse.package.plugins; [
     discourse-spoiler-alert
     discourse-solved
   ];
-  <link linkend="opt-services.discourse.siteSettings">siteSettings</link> = {
+  siteSettings = {
     plugins = {
       spoiler_enabled = false;
     };
   };
-  <link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
+  secretKeyBaseFile = &quot;/path/to/secret_key_base_file&quot;;
 };
 </programlisting>
-
-    </para>
   </section>
 </chapter>
diff --git a/nixos/modules/services/web-apps/grocy.md b/nixos/modules/services/web-apps/grocy.md
new file mode 100644
index 0000000000000..62aad4b103df1
--- /dev/null
+++ b/nixos/modules/services/web-apps/grocy.md
@@ -0,0 +1,66 @@
+# Grocy {#module-services-grocy}
+
+[Grocy](https://grocy.info/) is a web-based self-hosted groceries
+& household management solution for your home.
+
+## Basic usage {#module-services-grocy-basic-usage}
+
+A very basic configuration may look like this:
+```
+{ pkgs, ... }:
+{
+  services.grocy = {
+    enable = true;
+    hostName = "grocy.tld";
+  };
+}
+```
+This configures a simple vhost using [nginx](#opt-services.nginx.enable)
+which listens to `grocy.tld` with fully configured ACME/LE (this can be
+disabled by setting [services.grocy.nginx.enableSSL](#opt-services.grocy.nginx.enableSSL)
+to `false`). After the initial setup the credentials `admin:admin`
+can be used to login.
+
+The application's state is persisted at `/var/lib/grocy/grocy.db` in a
+`sqlite3` database. The migration is applied when requesting the `/`-route
+of the application.
+
+## Settings {#module-services-grocy-settings}
+
+The configuration for `grocy` is located at `/etc/grocy/config.php`.
+By default, the following settings can be defined in the NixOS-configuration:
+```
+{ pkgs, ... }:
+{
+  services.grocy.settings = {
+    # The default currency in the system for invoices etc.
+    # Please note that exchange rates aren't taken into account, this
+    # is just the setting for what's shown in the frontend.
+    currency = "EUR";
+
+    # The display language (and locale configuration) for grocy.
+    culture = "de";
+
+    calendar = {
+      # Whether or not to show the week-numbers
+      # in the calendar.
+      showWeekNumber = true;
+
+      # Index of the first day to be shown in the calendar (0=Sunday, 1=Monday,
+      # 2=Tuesday and so on).
+      firstDayOfWeek = 2;
+    };
+  };
+}
+```
+
+If you want to alter the configuration file on your own, you can do this manually with
+an expression like this:
+```
+{ lib, ... }:
+{
+  environment.etc."grocy/config.php".text = lib.mkAfter ''
+    // Arbitrary PHP code in grocy's configuration file
+  '';
+}
+```
diff --git a/nixos/modules/services/web-apps/grocy.xml b/nixos/modules/services/web-apps/grocy.xml
index fdf6d00f4b123..08de25b4ce2b8 100644
--- a/nixos/modules/services/web-apps/grocy.xml
+++ b/nixos/modules/services/web-apps/grocy.xml
@@ -1,77 +1,84 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-grocy">
-
+<!-- Do not edit this file directly, edit its companion .md instead
+     and regenerate this file using nixos/doc/manual/md-to-db.sh -->
+<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-grocy">
   <title>Grocy</title>
   <para>
-    <link xlink:href="https://grocy.info/">Grocy</link> is a web-based self-hosted groceries
-    &amp; household management solution for your home.
+    <link xlink:href="https://grocy.info/">Grocy</link> is a web-based
+    self-hosted groceries &amp; household management solution for your
+    home.
   </para>
-
   <section xml:id="module-services-grocy-basic-usage">
-   <title>Basic usage</title>
-   <para>
-    A very basic configuration may look like this:
-<programlisting>{ pkgs, ... }:
+    <title>Basic usage</title>
+    <para>
+      A very basic configuration may look like this:
+    </para>
+    <programlisting>
+{ pkgs, ... }:
 {
   services.grocy = {
-    <link linkend="opt-services.grocy.enable">enable</link> = true;
-    <link linkend="opt-services.grocy.hostName">hostName</link> = "grocy.tld";
+    enable = true;
+    hostName = &quot;grocy.tld&quot;;
   };
-}</programlisting>
-    This configures a simple vhost using <link linkend="opt-services.nginx.enable">nginx</link>
-    which listens to <literal>grocy.tld</literal> with fully configured ACME/LE (this can be
-    disabled by setting <link linkend="opt-services.grocy.nginx.enableSSL">services.grocy.nginx.enableSSL</link>
-    to <literal>false</literal>). After the initial setup the credentials <literal>admin:admin</literal>
-    can be used to login.
-   </para>
-   <para>
-    The application's state is persisted at <literal>/var/lib/grocy/grocy.db</literal> in a
-    <package>sqlite3</package> database. The migration is applied when requesting the <literal>/</literal>-route
-    of the application.
-   </para>
+}
+</programlisting>
+    <para>
+      This configures a simple vhost using
+      <link linkend="opt-services.nginx.enable">nginx</link> which
+      listens to <literal>grocy.tld</literal> with fully configured
+      ACME/LE (this can be disabled by setting
+      <link linkend="opt-services.grocy.nginx.enableSSL">services.grocy.nginx.enableSSL</link>
+      to <literal>false</literal>). After the initial setup the
+      credentials <literal>admin:admin</literal> can be used to login.
+    </para>
+    <para>
+      The application’s state is persisted at
+      <literal>/var/lib/grocy/grocy.db</literal> in a
+      <literal>sqlite3</literal> database. The migration is applied when
+      requesting the <literal>/</literal>-route of the application.
+    </para>
   </section>
-
   <section xml:id="module-services-grocy-settings">
-   <title>Settings</title>
-   <para>
-    The configuration for <literal>grocy</literal> is located at <literal>/etc/grocy/config.php</literal>.
-    By default, the following settings can be defined in the NixOS-configuration:
-<programlisting>{ pkgs, ... }:
+    <title>Settings</title>
+    <para>
+      The configuration for <literal>grocy</literal> is located at
+      <literal>/etc/grocy/config.php</literal>. By default, the
+      following settings can be defined in the NixOS-configuration:
+    </para>
+    <programlisting>
+{ pkgs, ... }:
 {
   services.grocy.settings = {
     # The default currency in the system for invoices etc.
     # Please note that exchange rates aren't taken into account, this
     # is just the setting for what's shown in the frontend.
-    <link linkend="opt-services.grocy.settings.currency">currency</link> = "EUR";
+    currency = &quot;EUR&quot;;
 
     # The display language (and locale configuration) for grocy.
-    <link linkend="opt-services.grocy.settings.currency">culture</link> = "de";
+    culture = &quot;de&quot;;
 
     calendar = {
       # Whether or not to show the week-numbers
       # in the calendar.
-      <link linkend="opt-services.grocy.settings.calendar.showWeekNumber">showWeekNumber</link> = true;
+      showWeekNumber = true;
 
       # Index of the first day to be shown in the calendar (0=Sunday, 1=Monday,
       # 2=Tuesday and so on).
-      <link linkend="opt-services.grocy.settings.calendar.firstDayOfWeek">firstDayOfWeek</link> = 2;
+      firstDayOfWeek = 2;
     };
   };
-}</programlisting>
-   </para>
-   <para>
-    If you want to alter the configuration file on your own, you can do this manually with
-    an expression like this:
-<programlisting>{ lib, ... }:
+}
+</programlisting>
+    <para>
+      If you want to alter the configuration file on your own, you can
+      do this manually with an expression like this:
+    </para>
+    <programlisting>
+{ lib, ... }:
 {
-  environment.etc."grocy/config.php".text = lib.mkAfter ''
+  environment.etc.&quot;grocy/config.php&quot;.text = lib.mkAfter ''
     // Arbitrary PHP code in grocy's configuration file
   '';
-}</programlisting>
-   </para>
+}
+</programlisting>
   </section>
-
 </chapter>
diff --git a/nixos/modules/services/web-apps/jitsi-meet.md b/nixos/modules/services/web-apps/jitsi-meet.md
new file mode 100644
index 0000000000000..060ef9752650a
--- /dev/null
+++ b/nixos/modules/services/web-apps/jitsi-meet.md
@@ -0,0 +1,45 @@
+# Jitsi Meet {#module-services-jitsi-meet}
+
+With Jitsi Meet on NixOS you can quickly configure a complete,
+private, self-hosted video conferencing solution.
+
+## Basic usage {#module-services-jitsi-basic-usage}
+
+A minimal configuration using Let's Encrypt for TLS certificates looks like this:
+```
+{
+  services.jitsi-meet = {
+    enable = true;
+    hostName = "jitsi.example.com";
+  };
+  services.jitsi-videobridge.openFirewall = true;
+  networking.firewall.allowedTCPPorts = [ 80 443 ];
+  security.acme.email = "me@example.com";
+  security.acme.acceptTerms = true;
+}
+```
+
+## Configuration {#module-services-jitsi-configuration}
+
+Here is the minimal configuration with additional configurations:
+```
+{
+  services.jitsi-meet = {
+    enable = true;
+    hostName = "jitsi.example.com";
+    config = {
+      enableWelcomePage = false;
+      prejoinPageEnabled = true;
+      defaultLang = "fi";
+    };
+    interfaceConfig = {
+      SHOW_JITSI_WATERMARK = false;
+      SHOW_WATERMARK_FOR_GUESTS = false;
+    };
+  };
+  services.jitsi-videobridge.openFirewall = true;
+  networking.firewall.allowedTCPPorts = [ 80 443 ];
+  security.acme.email = "me@example.com";
+  security.acme.acceptTerms = true;
+}
+```
diff --git a/nixos/modules/services/web-apps/jitsi-meet.xml b/nixos/modules/services/web-apps/jitsi-meet.xml
index ff44c724adf44..4d2d8aa55e19f 100644
--- a/nixos/modules/services/web-apps/jitsi-meet.xml
+++ b/nixos/modules/services/web-apps/jitsi-meet.xml
@@ -1,55 +1,55 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-jitsi-meet">
- <title>Jitsi Meet</title>
- <para>
-   With Jitsi Meet on NixOS you can quickly configure a complete,
-   private, self-hosted video conferencing solution.
- </para>
-
- <section xml:id="module-services-jitsi-basic-usage">
- <title>Basic usage</title>
-   <para>
-     A minimal configuration using Let's Encrypt for TLS certificates looks like this:
-<programlisting>{
+<!-- Do not edit this file directly, edit its companion .md instead
+     and regenerate this file using nixos/doc/manual/md-to-db.sh -->
+<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-jitsi-meet">
+  <title>Jitsi Meet</title>
+  <para>
+    With Jitsi Meet on NixOS you can quickly configure a complete,
+    private, self-hosted video conferencing solution.
+  </para>
+  <section xml:id="module-services-jitsi-basic-usage">
+    <title>Basic usage</title>
+    <para>
+      A minimal configuration using Let’s Encrypt for TLS certificates
+      looks like this:
+    </para>
+    <programlisting>
+{
   services.jitsi-meet = {
-    <link linkend="opt-services.jitsi-meet.enable">enable</link> = true;
-    <link linkend="opt-services.jitsi-meet.enable">hostName</link> = "jitsi.example.com";
+    enable = true;
+    hostName = &quot;jitsi.example.com&quot;;
   };
-  <link linkend="opt-services.jitsi-videobridge.openFirewall">services.jitsi-videobridge.openFirewall</link> = true;
-  <link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
-  <link linkend="opt-security.acme.defaults.email">security.acme.email</link> = "me@example.com";
-  <link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
-}</programlisting>
-   </para>
- </section>
-
- <section xml:id="module-services-jitsi-configuration">
- <title>Configuration</title>
-   <para>
-     Here is the minimal configuration with additional configurations:
-<programlisting>{
+  services.jitsi-videobridge.openFirewall = true;
+  networking.firewall.allowedTCPPorts = [ 80 443 ];
+  security.acme.email = &quot;me@example.com&quot;;
+  security.acme.acceptTerms = true;
+}
+</programlisting>
+  </section>
+  <section xml:id="module-services-jitsi-configuration">
+    <title>Configuration</title>
+    <para>
+      Here is the minimal configuration with additional configurations:
+    </para>
+    <programlisting>
+{
   services.jitsi-meet = {
-    <link linkend="opt-services.jitsi-meet.enable">enable</link> = true;
-    <link linkend="opt-services.jitsi-meet.enable">hostName</link> = "jitsi.example.com";
-    <link linkend="opt-services.jitsi-meet.config">config</link> = {
+    enable = true;
+    hostName = &quot;jitsi.example.com&quot;;
+    config = {
       enableWelcomePage = false;
       prejoinPageEnabled = true;
-      defaultLang = "fi";
+      defaultLang = &quot;fi&quot;;
     };
-    <link linkend="opt-services.jitsi-meet.interfaceConfig">interfaceConfig</link> = {
+    interfaceConfig = {
       SHOW_JITSI_WATERMARK = false;
       SHOW_WATERMARK_FOR_GUESTS = false;
     };
   };
-  <link linkend="opt-services.jitsi-videobridge.openFirewall">services.jitsi-videobridge.openFirewall</link> = true;
-  <link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
-  <link linkend="opt-security.acme.defaults.email">security.acme.email</link> = "me@example.com";
-  <link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
-}</programlisting>
-   </para>
- </section>
-
+  services.jitsi-videobridge.openFirewall = true;
+  networking.firewall.allowedTCPPorts = [ 80 443 ];
+  security.acme.email = &quot;me@example.com&quot;;
+  security.acme.acceptTerms = true;
+}
+</programlisting>
+  </section>
 </chapter>
diff --git a/nixos/modules/services/web-apps/keycloak.md b/nixos/modules/services/web-apps/keycloak.md
new file mode 100644
index 0000000000000..aa8de40d642b1
--- /dev/null
+++ b/nixos/modules/services/web-apps/keycloak.md
@@ -0,0 +1,141 @@
+# Keycloak {#module-services-keycloak}
+
+[Keycloak](https://www.keycloak.org/) is an
+open source identity and access management server with support for
+[OpenID Connect](https://openid.net/connect/),
+[OAUTH 2.0](https://oauth.net/2/) and
+[SAML 2.0](https://en.wikipedia.org/wiki/SAML_2.0).
+
+## Administration {#module-services-keycloak-admin}
+
+An administrative user with the username
+`admin` is automatically created in the
+`master` realm. Its initial password can be
+configured by setting [](#opt-services.keycloak.initialAdminPassword)
+and defaults to `changeme`. The password is
+not stored safely and should be changed immediately in the
+admin panel.
+
+Refer to the [Keycloak Server Administration Guide](
+  https://www.keycloak.org/docs/latest/server_admin/index.html
+) for information on
+how to administer your Keycloak
+instance.
+
+## Database access {#module-services-keycloak-database}
+
+Keycloak can be used with either PostgreSQL, MariaDB or
+MySQL. Which one is used can be
+configured in [](#opt-services.keycloak.database.type). The selected
+database will automatically be enabled and a database and role
+created unless [](#opt-services.keycloak.database.host) is changed
+from its default of `localhost` or
+[](#opt-services.keycloak.database.createLocally) is set to `false`.
+
+External database access can also be configured by setting
+[](#opt-services.keycloak.database.host),
+[](#opt-services.keycloak.database.name),
+[](#opt-services.keycloak.database.username),
+[](#opt-services.keycloak.database.useSSL) and
+[](#opt-services.keycloak.database.caCert) as
+appropriate. Note that you need to manually create the database
+and allow the configured database user full access to it.
+
+[](#opt-services.keycloak.database.passwordFile)
+must be set to the path to a file containing the password used
+to log in to the database. If [](#opt-services.keycloak.database.host)
+and [](#opt-services.keycloak.database.createLocally)
+are kept at their defaults, the database role
+`keycloak` with that password is provisioned
+on the local database instance.
+
+::: {.warning}
+The path should be provided as a string, not a Nix path, since Nix
+paths are copied into the world readable Nix store.
+:::
+
+## Hostname {#module-services-keycloak-hostname}
+
+The hostname is used to build the public URL used as base for
+all frontend requests and must be configured through
+[](#opt-services.keycloak.settings.hostname).
+
+::: {.note}
+If you're migrating an old Wildfly based Keycloak instance
+and want to keep compatibility with your current clients,
+you'll likely want to set [](#opt-services.keycloak.settings.http-relative-path)
+to `/auth`. See the option description
+for more details.
+:::
+
+[](#opt-services.keycloak.settings.hostname-strict-backchannel)
+determines whether Keycloak should force all requests to go
+through the frontend URL. By default,
+Keycloak allows backend requests to
+instead use its local hostname or IP address and may also
+advertise it to clients through its OpenID Connect Discovery
+endpoint.
+
+For more information on hostname configuration, see the [Hostname
+section of the Keycloak Server Installation and Configuration
+Guide](https://www.keycloak.org/server/hostname).
+
+## Setting up TLS/SSL {#module-services-keycloak-tls}
+
+By default, Keycloak won't accept
+unsecured HTTP connections originating from outside its local
+network.
+
+HTTPS support requires a TLS/SSL certificate and a private key,
+both [PEM formatted](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail).
+Their paths should be set through
+[](#opt-services.keycloak.sslCertificate) and
+[](#opt-services.keycloak.sslCertificateKey).
+
+::: {.warning}
+ The paths should be provided as a strings, not a Nix paths,
+since Nix paths are copied into the world readable Nix store.
+:::
+
+## Themes {#module-services-keycloak-themes}
+
+You can package custom themes and make them visible to
+Keycloak through [](#opt-services.keycloak.themes). See the
+[Themes section of the Keycloak Server Development Guide](
+  https://www.keycloak.org/docs/latest/server_development/#_themes
+) and the description of the aforementioned NixOS option for
+more information.
+
+## Configuration file settings {#module-services-keycloak-settings}
+
+Keycloak server configuration parameters can be set in
+[](#opt-services.keycloak.settings). These correspond
+directly to options in
+{file}`conf/keycloak.conf`. Some of the most
+important parameters are documented as suboptions, the rest can
+be found in the [All
+configuration section of the Keycloak Server Installation and
+Configuration Guide](https://www.keycloak.org/server/all-config).
+
+Options containing secret data should be set to an attribute
+set containing the attribute `_secret` - a
+string pointing to a file containing the value the option
+should be set to. See the description of
+[](#opt-services.keycloak.settings) for an example.
+
+## Example configuration {#module-services-keycloak-example-config}
+
+A basic configuration with some custom settings could look like this:
+```
+services.keycloak = {
+  enable = true;
+  settings = {
+    hostname = "keycloak.example.com";
+    hostname-strict-backchannel = true;
+  };
+  initialAdminPassword = "e6Wcm0RrtegMEHl";  # change on first login
+  sslCertificate = "/run/keys/ssl_cert";
+  sslCertificateKey = "/run/keys/ssl_key";
+  database.passwordFile = "/run/keys/db_password";
+};
+```
diff --git a/nixos/modules/services/web-apps/keycloak.xml b/nixos/modules/services/web-apps/keycloak.xml
index 861756e33ac09..148782d30f39e 100644
--- a/nixos/modules/services/web-apps/keycloak.xml
+++ b/nixos/modules/services/web-apps/keycloak.xml
@@ -1,202 +1,177 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-keycloak">
- <title>Keycloak</title>
- <para>
-   <link xlink:href="https://www.keycloak.org/">Keycloak</link> is an
-   open source identity and access management server with support for
-   <link xlink:href="https://openid.net/connect/">OpenID
-   Connect</link>, <link xlink:href="https://oauth.net/2/">OAUTH
-   2.0</link> and <link
-   xlink:href="https://en.wikipedia.org/wiki/SAML_2.0">SAML
-   2.0</link>.
- </para>
-   <section xml:id="module-services-keycloak-admin">
-     <title>Administration</title>
-     <para>
-       An administrative user with the username
-       <literal>admin</literal> is automatically created in the
-       <literal>master</literal> realm. Its initial password can be
-       configured by setting <xref linkend="opt-services.keycloak.initialAdminPassword" />
-       and defaults to <literal>changeme</literal>. The password is
-       not stored safely and should be changed immediately in the
-       admin panel.
-     </para>
-
-     <para>
-       Refer to the <link
-       xlink:href="https://www.keycloak.org/docs/latest/server_admin/index.html">
-       Keycloak Server Administration Guide</link> for information on
-       how to administer your <productname>Keycloak</productname>
-       instance.
-     </para>
-   </section>
-
-   <section xml:id="module-services-keycloak-database">
-     <title>Database access</title>
-     <para>
-       <productname>Keycloak</productname> can be used with either
-       <productname>PostgreSQL</productname>,
-       <productname>MariaDB</productname> or
-       <productname>MySQL</productname>. Which one is used can be
-       configured in <xref
-       linkend="opt-services.keycloak.database.type" />. The selected
-       database will automatically be enabled and a database and role
-       created unless <xref
-       linkend="opt-services.keycloak.database.host" /> is changed
-       from its default of <literal>localhost</literal> or <xref
-       linkend="opt-services.keycloak.database.createLocally" /> is
-       set to <literal>false</literal>.
-     </para>
-
-     <para>
-       External database access can also be configured by setting
-       <xref linkend="opt-services.keycloak.database.host" />, <xref
-       linkend="opt-services.keycloak.database.name" />, <xref
-       linkend="opt-services.keycloak.database.username" />, <xref
-       linkend="opt-services.keycloak.database.useSSL" /> and <xref
-       linkend="opt-services.keycloak.database.caCert" /> as
-       appropriate. Note that you need to manually create the database
-       and allow the configured database user full access to it.
-     </para>
-
-     <para>
-       <xref linkend="opt-services.keycloak.database.passwordFile" />
-       must be set to the path to a file containing the password used
-       to log in to the database. If <xref linkend="opt-services.keycloak.database.host" />
-       and <xref linkend="opt-services.keycloak.database.createLocally" />
-       are kept at their defaults, the database role
-       <literal>keycloak</literal> with that password is provisioned
-       on the local database instance.
-     </para>
-
-     <warning>
-       <para>
-         The path should be provided as a string, not a Nix path, since Nix
-         paths are copied into the world readable Nix store.
-       </para>
-     </warning>
-   </section>
-
-   <section xml:id="module-services-keycloak-hostname">
-     <title>Hostname</title>
-     <para>
-       The hostname is used to build the public URL used as base for
-       all frontend requests and must be configured through <xref
-       linkend="opt-services.keycloak.settings.hostname" />.
-     </para>
-
-     <note>
-       <para>
-         If you're migrating an old Wildfly based Keycloak instance
-         and want to keep compatibility with your current clients,
-         you'll likely want to set <xref
-         linkend="opt-services.keycloak.settings.http-relative-path"
-         /> to <literal>/auth</literal>. See the option description
-         for more details.
-       </para>
-     </note>
-
-     <para>
-       <xref linkend="opt-services.keycloak.settings.hostname-strict-backchannel" />
-       determines whether Keycloak should force all requests to go
-       through the frontend URL. By default,
-       <productname>Keycloak</productname> allows backend requests to
-       instead use its local hostname or IP address and may also
-       advertise it to clients through its OpenID Connect Discovery
-       endpoint.
-     </para>
-
-     <para>
-        For more information on hostname configuration, see the <link
-        xlink:href="https://www.keycloak.org/server/hostname">Hostname
-        section of the Keycloak Server Installation and Configuration
-        Guide</link>.
-     </para>
-   </section>
-
-   <section xml:id="module-services-keycloak-tls">
-     <title>Setting up TLS/SSL</title>
-     <para>
-       By default, <productname>Keycloak</productname> won't accept
-       unsecured HTTP connections originating from outside its local
-       network.
-     </para>
-
-     <para>
-       HTTPS support requires a TLS/SSL certificate and a private key,
-       both <link
-       xlink:href="https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail">PEM
-       formatted</link>. Their paths should be set through <xref
-       linkend="opt-services.keycloak.sslCertificate" /> and <xref
-       linkend="opt-services.keycloak.sslCertificateKey" />.
-     </para>
-
-     <warning>
-       <para>
-         The paths should be provided as a strings, not a Nix paths,
-         since Nix paths are copied into the world readable Nix store.
-       </para>
-     </warning>
-   </section>
-
-   <section xml:id="module-services-keycloak-themes">
-     <title>Themes</title>
-     <para>
-        You can package custom themes and make them visible to
-        Keycloak through <xref linkend="opt-services.keycloak.themes"
-        />. See the <link
-        xlink:href="https://www.keycloak.org/docs/latest/server_development/#_themes">
-        Themes section of the Keycloak Server Development Guide</link>
-        and the description of the aforementioned NixOS option for
-        more information.
-     </para>
-   </section>
-
-   <section xml:id="module-services-keycloak-settings">
-     <title>Configuration file settings</title>
-     <para>
-       Keycloak server configuration parameters can be set in <xref
-       linkend="opt-services.keycloak.settings" />. These correspond
-       directly to options in
-       <filename>conf/keycloak.conf</filename>. Some of the most
-       important parameters are documented as suboptions, the rest can
-       be found in the <link
-       xlink:href="https://www.keycloak.org/server/all-config">All
-       configuration section of the Keycloak Server Installation and
-       Configuration Guide</link>.
-     </para>
-
-     <para>
-       Options containing secret data should be set to an attribute
-       set containing the attribute <literal>_secret</literal> - a
-       string pointing to a file containing the value the option
-       should be set to. See the description of <xref
-       linkend="opt-services.keycloak.settings" /> for an example.
-     </para>
-   </section>
-
-
-   <section xml:id="module-services-keycloak-example-config">
-     <title>Example configuration</title>
-     <para>
-       A basic configuration with some custom settings could look like this:
-<programlisting>
+<!-- Do not edit this file directly, edit its companion .md instead
+     and regenerate this file using nixos/doc/manual/md-to-db.sh -->
+<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-keycloak">
+  <title>Keycloak</title>
+  <para>
+    <link xlink:href="https://www.keycloak.org/">Keycloak</link> is an
+    open source identity and access management server with support for
+    <link xlink:href="https://openid.net/connect/">OpenID
+    Connect</link>, <link xlink:href="https://oauth.net/2/">OAUTH
+    2.0</link> and
+    <link xlink:href="https://en.wikipedia.org/wiki/SAML_2.0">SAML
+    2.0</link>.
+  </para>
+  <section xml:id="module-services-keycloak-admin">
+    <title>Administration</title>
+    <para>
+      An administrative user with the username <literal>admin</literal>
+      is automatically created in the <literal>master</literal> realm.
+      Its initial password can be configured by setting
+      <xref linkend="opt-services.keycloak.initialAdminPassword" /> and
+      defaults to <literal>changeme</literal>. The password is not
+      stored safely and should be changed immediately in the admin
+      panel.
+    </para>
+    <para>
+      Refer to the
+      <link xlink:href="https://www.keycloak.org/docs/latest/server_admin/index.html">Keycloak
+      Server Administration Guide</link> for information on how to
+      administer your Keycloak instance.
+    </para>
+  </section>
+  <section xml:id="module-services-keycloak-database">
+    <title>Database access</title>
+    <para>
+      Keycloak can be used with either PostgreSQL, MariaDB or MySQL.
+      Which one is used can be configured in
+      <xref linkend="opt-services.keycloak.database.type" />. The
+      selected database will automatically be enabled and a database and
+      role created unless
+      <xref linkend="opt-services.keycloak.database.host" /> is changed
+      from its default of <literal>localhost</literal> or
+      <xref linkend="opt-services.keycloak.database.createLocally" /> is
+      set to <literal>false</literal>.
+    </para>
+    <para>
+      External database access can also be configured by setting
+      <xref linkend="opt-services.keycloak.database.host" />,
+      <xref linkend="opt-services.keycloak.database.name" />,
+      <xref linkend="opt-services.keycloak.database.username" />,
+      <xref linkend="opt-services.keycloak.database.useSSL" /> and
+      <xref linkend="opt-services.keycloak.database.caCert" /> as
+      appropriate. Note that you need to manually create the database
+      and allow the configured database user full access to it.
+    </para>
+    <para>
+      <xref linkend="opt-services.keycloak.database.passwordFile" />
+      must be set to the path to a file containing the password used to
+      log in to the database. If
+      <xref linkend="opt-services.keycloak.database.host" /> and
+      <xref linkend="opt-services.keycloak.database.createLocally" />
+      are kept at their defaults, the database role
+      <literal>keycloak</literal> with that password is provisioned on
+      the local database instance.
+    </para>
+    <warning>
+      <para>
+        The path should be provided as a string, not a Nix path, since
+        Nix paths are copied into the world readable Nix store.
+      </para>
+    </warning>
+  </section>
+  <section xml:id="module-services-keycloak-hostname">
+    <title>Hostname</title>
+    <para>
+      The hostname is used to build the public URL used as base for all
+      frontend requests and must be configured through
+      <xref linkend="opt-services.keycloak.settings.hostname" />.
+    </para>
+    <note>
+      <para>
+        If you’re migrating an old Wildfly based Keycloak instance and
+        want to keep compatibility with your current clients, you’ll
+        likely want to set
+        <xref linkend="opt-services.keycloak.settings.http-relative-path" />
+        to <literal>/auth</literal>. See the option description for more
+        details.
+      </para>
+    </note>
+    <para>
+      <xref linkend="opt-services.keycloak.settings.hostname-strict-backchannel" />
+      determines whether Keycloak should force all requests to go
+      through the frontend URL. By default, Keycloak allows backend
+      requests to instead use its local hostname or IP address and may
+      also advertise it to clients through its OpenID Connect Discovery
+      endpoint.
+    </para>
+    <para>
+      For more information on hostname configuration, see the
+      <link xlink:href="https://www.keycloak.org/server/hostname">Hostname
+      section of the Keycloak Server Installation and Configuration
+      Guide</link>.
+    </para>
+  </section>
+  <section xml:id="module-services-keycloak-tls">
+    <title>Setting up TLS/SSL</title>
+    <para>
+      By default, Keycloak won’t accept unsecured HTTP connections
+      originating from outside its local network.
+    </para>
+    <para>
+      HTTPS support requires a TLS/SSL certificate and a private key,
+      both
+      <link xlink:href="https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail">PEM
+      formatted</link>. Their paths should be set through
+      <xref linkend="opt-services.keycloak.sslCertificate" /> and
+      <xref linkend="opt-services.keycloak.sslCertificateKey" />.
+    </para>
+    <warning>
+      <para>
+        The paths should be provided as a strings, not a Nix paths,
+        since Nix paths are copied into the world readable Nix store.
+      </para>
+    </warning>
+  </section>
+  <section xml:id="module-services-keycloak-themes">
+    <title>Themes</title>
+    <para>
+      You can package custom themes and make them visible to Keycloak
+      through <xref linkend="opt-services.keycloak.themes" />. See the
+      <link xlink:href="https://www.keycloak.org/docs/latest/server_development/#_themes">Themes
+      section of the Keycloak Server Development Guide</link> and the
+      description of the aforementioned NixOS option for more
+      information.
+    </para>
+  </section>
+  <section xml:id="module-services-keycloak-settings">
+    <title>Configuration file settings</title>
+    <para>
+      Keycloak server configuration parameters can be set in
+      <xref linkend="opt-services.keycloak.settings" />. These
+      correspond directly to options in
+      <filename>conf/keycloak.conf</filename>. Some of the most
+      important parameters are documented as suboptions, the rest can be
+      found in the
+      <link xlink:href="https://www.keycloak.org/server/all-config">All
+      configuration section of the Keycloak Server Installation and
+      Configuration Guide</link>.
+    </para>
+    <para>
+      Options containing secret data should be set to an attribute set
+      containing the attribute <literal>_secret</literal> - a string
+      pointing to a file containing the value the option should be set
+      to. See the description of
+      <xref linkend="opt-services.keycloak.settings" /> for an example.
+    </para>
+  </section>
+  <section xml:id="module-services-keycloak-example-config">
+    <title>Example configuration</title>
+    <para>
+      A basic configuration with some custom settings could look like
+      this:
+    </para>
+    <programlisting>
 services.keycloak = {
-  <link linkend="opt-services.keycloak.enable">enable</link> = true;
+  enable = true;
   settings = {
-    <link linkend="opt-services.keycloak.settings.hostname">hostname</link> = "keycloak.example.com";
-    <link linkend="opt-services.keycloak.settings.hostname-strict-backchannel">hostname-strict-backchannel</link> = true;
+    hostname = &quot;keycloak.example.com&quot;;
+    hostname-strict-backchannel = true;
   };
-  <link linkend="opt-services.keycloak.initialAdminPassword">initialAdminPassword</link> = "e6Wcm0RrtegMEHl";  # change on first login
-  <link linkend="opt-services.keycloak.sslCertificate">sslCertificate</link> = "/run/keys/ssl_cert";
-  <link linkend="opt-services.keycloak.sslCertificateKey">sslCertificateKey</link> = "/run/keys/ssl_key";
-  <link linkend="opt-services.keycloak.database.passwordFile">database.passwordFile</link> = "/run/keys/db_password";
+  initialAdminPassword = &quot;e6Wcm0RrtegMEHl&quot;;  # change on first login
+  sslCertificate = &quot;/run/keys/ssl_cert&quot;;
+  sslCertificateKey = &quot;/run/keys/ssl_key&quot;;
+  database.passwordFile = &quot;/run/keys/db_password&quot;;
 };
 </programlisting>
-     </para>
-
-   </section>
- </chapter>
+  </section>
+</chapter>
diff --git a/nixos/modules/services/web-apps/lemmy.nix b/nixos/modules/services/web-apps/lemmy.nix
index 267584dd0ca7f..f2eb6e726b903 100644
--- a/nixos/modules/services/web-apps/lemmy.nix
+++ b/nixos/modules/services/web-apps/lemmy.nix
@@ -6,8 +6,6 @@ let
 in
 {
   meta.maintainers = with maintainers; [ happysalada ];
-  # Don't edit the docbook xml directly, edit the md and generate it:
-  # `pandoc lemmy.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > lemmy.xml`
   meta.doc = ./lemmy.xml;
 
   imports = [
diff --git a/nixos/modules/services/web-apps/lemmy.xml b/nixos/modules/services/web-apps/lemmy.xml
index f04316b3c5159..114e11f3488ad 100644
--- a/nixos/modules/services/web-apps/lemmy.xml
+++ b/nixos/modules/services/web-apps/lemmy.xml
@@ -1,3 +1,5 @@
+<!-- Do not edit this file directly, edit its companion .md instead
+     and regenerate this file using nixos/doc/manual/md-to-db.sh -->
 <chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-lemmy">
   <title>Lemmy</title>
   <para>
diff --git a/nixos/modules/services/web-apps/matomo-doc.xml b/nixos/modules/services/web-apps/matomo-doc.xml
deleted file mode 100644
index 69d1170e4523b..0000000000000
--- a/nixos/modules/services/web-apps/matomo-doc.xml
+++ /dev/null
@@ -1,107 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-matomo">
- <title>Matomo</title>
- <para>
-  Matomo is a real-time web analytics application. This module configures
-  php-fpm as backend for Matomo, optionally configuring an nginx vhost as well.
- </para>
- <para>
-  An automatic setup is not suported by Matomo, so you need to configure Matomo
-  itself in the browser-based Matomo setup.
- </para>
- <section xml:id="module-services-matomo-database-setup">
-  <title>Database Setup</title>
-
-  <para>
-   You also need to configure a MariaDB or MySQL database and -user for Matomo
-   yourself, and enter those credentials in your browser. You can use
-   passwordless database authentication via the UNIX_SOCKET authentication
-   plugin with the following SQL commands:
-<programlisting>
-# For MariaDB
-INSTALL PLUGIN unix_socket SONAME 'auth_socket';
-CREATE DATABASE matomo;
-CREATE USER 'matomo'@'localhost' IDENTIFIED WITH unix_socket;
-GRANT ALL PRIVILEGES ON matomo.* TO 'matomo'@'localhost';
-
-# For MySQL
-INSTALL PLUGIN auth_socket SONAME 'auth_socket.so';
-CREATE DATABASE matomo;
-CREATE USER 'matomo'@'localhost' IDENTIFIED WITH auth_socket;
-GRANT ALL PRIVILEGES ON matomo.* TO 'matomo'@'localhost';
-</programlisting>
-   Then fill in <literal>matomo</literal> as database user and database name,
-   and leave the password field blank. This authentication works by allowing
-   only the <literal>matomo</literal> unix user to authenticate as the
-   <literal>matomo</literal> database user (without needing a password), but no
-   other users. For more information on passwordless login, see
-   <link xlink:href="https://mariadb.com/kb/en/mariadb/unix_socket-authentication-plugin/" />.
-  </para>
-
-  <para>
-   Of course, you can use password based authentication as well, e.g. when the
-   database is not on the same host.
-  </para>
- </section>
- <section xml:id="module-services-matomo-archive-processing">
-  <title>Archive Processing</title>
-
-  <para>
-   This module comes with the systemd service
-   <literal>matomo-archive-processing.service</literal> and a timer that
-   automatically triggers archive processing every hour. This means that you
-   can safely
-   <link xlink:href="https://matomo.org/docs/setup-auto-archiving/#disable-browser-triggers-for-matomo-archiving-and-limit-matomo-reports-to-updating-every-hour">
-   disable browser triggers for Matomo archiving </link> at
-   <literal>Administration > System > General Settings</literal>.
-  </para>
-
-  <para>
-   With automatic archive processing, you can now also enable to
-   <link xlink:href="https://matomo.org/docs/privacy/#step-2-delete-old-visitors-logs">
-   delete old visitor logs </link> at <literal>Administration > System >
-   Privacy</literal>, but make sure that you run <literal>systemctl start
-   matomo-archive-processing.service</literal> at least once without errors if
-   you have already collected data before, so that the reports get archived
-   before the source data gets deleted.
-  </para>
- </section>
- <section xml:id="module-services-matomo-backups">
-  <title>Backup</title>
-
-  <para>
-   You only need to take backups of your MySQL database and the
-   <filename>/var/lib/matomo/config/config.ini.php</filename> file. Use a user
-   in the <literal>matomo</literal> group or root to access the file. For more
-   information, see
-   <link xlink:href="https://matomo.org/faq/how-to-install/faq_138/" />.
-  </para>
- </section>
- <section xml:id="module-services-matomo-issues">
-  <title>Issues</title>
-
-  <itemizedlist>
-   <listitem>
-    <para>
-     Matomo will warn you that the JavaScript tracker is not writable. This is
-     because it's located in the read-only nix store. You can safely ignore
-     this, unless you need a plugin that needs JavaScript tracker access.
-    </para>
-   </listitem>
-  </itemizedlist>
- </section>
- <section xml:id="module-services-matomo-other-web-servers">
-  <title>Using other Web Servers than nginx</title>
-
-  <para>
-   You can use other web servers by forwarding calls for
-   <filename>index.php</filename> and <filename>piwik.php</filename> to the
-   <literal><link linkend="opt-services.phpfpm.pools._name_.socket">services.phpfpm.pools.&lt;name&gt;.socket</link></literal> fastcgi unix socket. You can use
-   the nginx configuration in the module code as a reference to what else
-   should be configured.
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/services/web-apps/matomo.md b/nixos/modules/services/web-apps/matomo.md
new file mode 100644
index 0000000000000..f5536a35f7a89
--- /dev/null
+++ b/nixos/modules/services/web-apps/matomo.md
@@ -0,0 +1,77 @@
+# Matomo {#module-services-matomo}
+
+Matomo is a real-time web analytics application. This module configures
+php-fpm as backend for Matomo, optionally configuring an nginx vhost as well.
+
+An automatic setup is not suported by Matomo, so you need to configure Matomo
+itself in the browser-based Matomo setup.
+
+## Database Setup {#module-services-matomo-database-setup}
+
+You also need to configure a MariaDB or MySQL database and -user for Matomo
+yourself, and enter those credentials in your browser. You can use
+passwordless database authentication via the UNIX_SOCKET authentication
+plugin with the following SQL commands:
+```
+# For MariaDB
+INSTALL PLUGIN unix_socket SONAME 'auth_socket';
+CREATE DATABASE matomo;
+CREATE USER 'matomo'@'localhost' IDENTIFIED WITH unix_socket;
+GRANT ALL PRIVILEGES ON matomo.* TO 'matomo'@'localhost';
+
+# For MySQL
+INSTALL PLUGIN auth_socket SONAME 'auth_socket.so';
+CREATE DATABASE matomo;
+CREATE USER 'matomo'@'localhost' IDENTIFIED WITH auth_socket;
+GRANT ALL PRIVILEGES ON matomo.* TO 'matomo'@'localhost';
+```
+Then fill in `matomo` as database user and database name,
+and leave the password field blank. This authentication works by allowing
+only the `matomo` unix user to authenticate as the
+`matomo` database user (without needing a password), but no
+other users. For more information on passwordless login, see
+<https://mariadb.com/kb/en/mariadb/unix_socket-authentication-plugin/>.
+
+Of course, you can use password based authentication as well, e.g. when the
+database is not on the same host.
+
+## Archive Processing {#module-services-matomo-archive-processing}
+
+This module comes with the systemd service
+`matomo-archive-processing.service` and a timer that
+automatically triggers archive processing every hour. This means that you
+can safely
+[disable browser triggers for Matomo archiving](
+https://matomo.org/docs/setup-auto-archiving/#disable-browser-triggers-for-matomo-archiving-and-limit-matomo-reports-to-updating-every-hour
+) at
+`Administration > System > General Settings`.
+
+With automatic archive processing, you can now also enable to
+[delete old visitor logs](https://matomo.org/docs/privacy/#step-2-delete-old-visitors-logs)
+at `Administration > System > Privacy`, but make sure that you run `systemctl start
+matomo-archive-processing.service` at least once without errors if
+you have already collected data before, so that the reports get archived
+before the source data gets deleted.
+
+## Backup {#module-services-matomo-backups}
+
+You only need to take backups of your MySQL database and the
+{file}`/var/lib/matomo/config/config.ini.php` file. Use a user
+in the `matomo` group or root to access the file. For more
+information, see
+<https://matomo.org/faq/how-to-install/faq_138/>.
+
+## Issues {#module-services-matomo-issues}
+
+  - Matomo will warn you that the JavaScript tracker is not writable. This is
+    because it's located in the read-only nix store. You can safely ignore
+    this, unless you need a plugin that needs JavaScript tracker access.
+
+## Using other Web Servers than nginx {#module-services-matomo-other-web-servers}
+
+You can use other web servers by forwarding calls for
+{file}`index.php` and {file}`piwik.php` to the
+[`services.phpfpm.pools.<name>.socket`](#opt-services.phpfpm.pools._name_.socket)
+fastcgi unix socket. You can use
+the nginx configuration in the module code as a reference to what else
+should be configured.
diff --git a/nixos/modules/services/web-apps/matomo.nix b/nixos/modules/services/web-apps/matomo.nix
index 0435d21ce8a2b..9845106599527 100644
--- a/nixos/modules/services/web-apps/matomo.nix
+++ b/nixos/modules/services/web-apps/matomo.nix
@@ -325,7 +325,7 @@ in {
   };
 
   meta = {
-    doc = ./matomo-doc.xml;
+    doc = ./matomo.xml;
     maintainers = with lib.maintainers; [ florianjacob ];
   };
 }
diff --git a/nixos/modules/services/web-apps/matomo.xml b/nixos/modules/services/web-apps/matomo.xml
new file mode 100644
index 0000000000000..30994cc9f1dad
--- /dev/null
+++ b/nixos/modules/services/web-apps/matomo.xml
@@ -0,0 +1,107 @@
+<!-- Do not edit this file directly, edit its companion .md instead
+     and regenerate this file using nixos/doc/manual/md-to-db.sh -->
+<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-matomo">
+  <title>Matomo</title>
+  <para>
+    Matomo is a real-time web analytics application. This module
+    configures php-fpm as backend for Matomo, optionally configuring an
+    nginx vhost as well.
+  </para>
+  <para>
+    An automatic setup is not suported by Matomo, so you need to
+    configure Matomo itself in the browser-based Matomo setup.
+  </para>
+  <section xml:id="module-services-matomo-database-setup">
+    <title>Database Setup</title>
+    <para>
+      You also need to configure a MariaDB or MySQL database and -user
+      for Matomo yourself, and enter those credentials in your browser.
+      You can use passwordless database authentication via the
+      UNIX_SOCKET authentication plugin with the following SQL commands:
+    </para>
+    <programlisting>
+# For MariaDB
+INSTALL PLUGIN unix_socket SONAME 'auth_socket';
+CREATE DATABASE matomo;
+CREATE USER 'matomo'@'localhost' IDENTIFIED WITH unix_socket;
+GRANT ALL PRIVILEGES ON matomo.* TO 'matomo'@'localhost';
+
+# For MySQL
+INSTALL PLUGIN auth_socket SONAME 'auth_socket.so';
+CREATE DATABASE matomo;
+CREATE USER 'matomo'@'localhost' IDENTIFIED WITH auth_socket;
+GRANT ALL PRIVILEGES ON matomo.* TO 'matomo'@'localhost';
+</programlisting>
+    <para>
+      Then fill in <literal>matomo</literal> as database user and
+      database name, and leave the password field blank. This
+      authentication works by allowing only the
+      <literal>matomo</literal> unix user to authenticate as the
+      <literal>matomo</literal> database user (without needing a
+      password), but no other users. For more information on
+      passwordless login, see
+      <link xlink:href="https://mariadb.com/kb/en/mariadb/unix_socket-authentication-plugin/">https://mariadb.com/kb/en/mariadb/unix_socket-authentication-plugin/</link>.
+    </para>
+    <para>
+      Of course, you can use password based authentication as well, e.g.
+      when the database is not on the same host.
+    </para>
+  </section>
+  <section xml:id="module-services-matomo-archive-processing">
+    <title>Archive Processing</title>
+    <para>
+      This module comes with the systemd service
+      <literal>matomo-archive-processing.service</literal> and a timer
+      that automatically triggers archive processing every hour. This
+      means that you can safely
+      <link xlink:href="https://matomo.org/docs/setup-auto-archiving/#disable-browser-triggers-for-matomo-archiving-and-limit-matomo-reports-to-updating-every-hour">disable
+      browser triggers for Matomo archiving</link> at
+      <literal>Administration &gt; System &gt; General Settings</literal>.
+    </para>
+    <para>
+      With automatic archive processing, you can now also enable to
+      <link xlink:href="https://matomo.org/docs/privacy/#step-2-delete-old-visitors-logs">delete
+      old visitor logs</link> at
+      <literal>Administration &gt; System &gt; Privacy</literal>, but
+      make sure that you run
+      <literal>systemctl start matomo-archive-processing.service</literal>
+      at least once without errors if you have already collected data
+      before, so that the reports get archived before the source data
+      gets deleted.
+    </para>
+  </section>
+  <section xml:id="module-services-matomo-backups">
+    <title>Backup</title>
+    <para>
+      You only need to take backups of your MySQL database and the
+      <filename>/var/lib/matomo/config/config.ini.php</filename> file.
+      Use a user in the <literal>matomo</literal> group or root to
+      access the file. For more information, see
+      <link xlink:href="https://matomo.org/faq/how-to-install/faq_138/">https://matomo.org/faq/how-to-install/faq_138/</link>.
+    </para>
+  </section>
+  <section xml:id="module-services-matomo-issues">
+    <title>Issues</title>
+    <itemizedlist spacing="compact">
+      <listitem>
+        <para>
+          Matomo will warn you that the JavaScript tracker is not
+          writable. This is because it’s located in the read-only nix
+          store. You can safely ignore this, unless you need a plugin
+          that needs JavaScript tracker access.
+        </para>
+      </listitem>
+    </itemizedlist>
+  </section>
+  <section xml:id="module-services-matomo-other-web-servers">
+    <title>Using other Web Servers than nginx</title>
+    <para>
+      You can use other web servers by forwarding calls for
+      <filename>index.php</filename> and <filename>piwik.php</filename>
+      to the
+      <link linkend="opt-services.phpfpm.pools._name_.socket"><literal>services.phpfpm.pools.&lt;name&gt;.socket</literal></link>
+      fastcgi unix socket. You can use the nginx configuration in the
+      module code as a reference to what else should be configured.
+    </para>
+  </section>
+</chapter>
diff --git a/nixos/modules/services/web-apps/nextcloud.md b/nixos/modules/services/web-apps/nextcloud.md
new file mode 100644
index 0000000000000..014807f3da23c
--- /dev/null
+++ b/nixos/modules/services/web-apps/nextcloud.md
@@ -0,0 +1,237 @@
+# Nextcloud {#module-services-nextcloud}
+
+[Nextcloud](https://nextcloud.com/) is an open-source,
+self-hostable cloud platform. The server setup can be automated using
+[services.nextcloud](#opt-services.nextcloud.enable). A
+desktop client is packaged at `pkgs.nextcloud-client`.
+
+The current default by NixOS is `nextcloud25` which is also the latest
+major version available.
+
+## Basic usage {#module-services-nextcloud-basic-usage}
+
+Nextcloud is a PHP-based application which requires an HTTP server
+([`services.nextcloud`](#opt-services.nextcloud.enable)
+optionally supports
+[`services.nginx`](#opt-services.nginx.enable))
+and a database (it's recommended to use
+[`services.postgresql`](#opt-services.postgresql.enable)).
+
+A very basic configuration may look like this:
+```
+{ pkgs, ... }:
+{
+  services.nextcloud = {
+    enable = true;
+    hostName = "nextcloud.tld";
+    config = {
+      dbtype = "pgsql";
+      dbuser = "nextcloud";
+      dbhost = "/run/postgresql"; # nextcloud will add /.s.PGSQL.5432 by itself
+      dbname = "nextcloud";
+      adminpassFile = "/path/to/admin-pass-file";
+      adminuser = "root";
+    };
+  };
+
+  services.postgresql = {
+    enable = true;
+    ensureDatabases = [ "nextcloud" ];
+    ensureUsers = [
+     { name = "nextcloud";
+       ensurePermissions."DATABASE nextcloud" = "ALL PRIVILEGES";
+     }
+    ];
+  };
+
+  # ensure that postgres is running *before* running the setup
+  systemd.services."nextcloud-setup" = {
+    requires = ["postgresql.service"];
+    after = ["postgresql.service"];
+  };
+
+  networking.firewall.allowedTCPPorts = [ 80 443 ];
+}
+```
+
+The `hostName` option is used internally to configure an HTTP
+server using [`PHP-FPM`](https://php-fpm.org/)
+and `nginx`. The `config` attribute set is
+used by the imperative installer and all values are written to an additional file
+to ensure that changes can be applied by changing the module's options.
+
+In case the application serves multiple domains (those are checked with
+[`$_SERVER['HTTP_HOST']`](http://php.net/manual/en/reserved.variables.server.php))
+it's needed to add them to
+[`services.nextcloud.config.extraTrustedDomains`](#opt-services.nextcloud.config.extraTrustedDomains).
+
+Auto updates for Nextcloud apps can be enabled using
+[`services.nextcloud.autoUpdateApps`](#opt-services.nextcloud.autoUpdateApps.enable).
+
+## Common problems {#module-services-nextcloud-pitfalls-during-upgrade}
+
+  - **General notes.**
+    Unfortunately Nextcloud appears to be very stateful when it comes to
+    managing its own configuration. The config file lives in the home directory
+    of the `nextcloud` user (by default
+    `/var/lib/nextcloud/config/config.php`) and is also used to
+    track several states of the application (e.g., whether installed or not).
+
+     All configuration parameters are also stored in
+    {file}`/var/lib/nextcloud/config/override.config.php` which is generated by
+    the module and linked from the store to ensure that all values from
+    {file}`config.php` can be modified by the module.
+    However {file}`config.php` manages the application's state and shouldn't be
+    touched manually because of that.
+
+    ::: {.warning}
+    Don't delete {file}`config.php`! This file
+    tracks the application's state and a deletion can cause unwanted
+    side-effects!
+    :::
+
+    ::: {.warning}
+    Don't rerun `nextcloud-occ maintenance:install`!
+    This command tries to install the application
+    and can cause unwanted side-effects!
+    :::
+  - **Multiple version upgrades.**
+    Nextcloud doesn't allow to move more than one major-version forward. E.g., if you're on
+    `v16`, you cannot upgrade to `v18`, you need to upgrade to
+    `v17` first. This is ensured automatically as long as the
+    [stateVersion](#opt-system.stateVersion) is declared properly. In that case
+    the oldest version available (one major behind the one from the previous NixOS
+    release) will be selected by default and the module will generate a warning that reminds
+    the user to upgrade to latest Nextcloud *after* that deploy.
+  - **`Error: Command "upgrade" is not defined.`**
+    This error usually occurs if the initial installation
+    ({command}`nextcloud-occ maintenance:install`) has failed. After that, the application
+    is not installed, but the upgrade is attempted to be executed. Further context can
+    be found in [NixOS/nixpkgs#111175](https://github.com/NixOS/nixpkgs/issues/111175).
+
+    First of all, it makes sense to find out what went wrong by looking at the logs
+    of the installation via {command}`journalctl -u nextcloud-setup` and try to fix
+    the underlying issue.
+
+    - If this occurs on an *existing* setup, this is most likely because
+      the maintenance mode is active. It can be deactivated by running
+      {command}`nextcloud-occ maintenance:mode --off`. It's advisable though to
+      check the logs first on why the maintenance mode was activated.
+    - ::: {.warning}
+      Only perform the following measures on
+      *freshly installed instances!*
+      :::
+
+      A re-run of the installer can be forced by *deleting*
+      {file}`/var/lib/nextcloud/config/config.php`. This is the only time
+      advisable because the fresh install doesn't have any state that can be lost.
+      In case that doesn't help, an entire re-creation can be forced via
+      {command}`rm -rf ~nextcloud/`.
+
+  - **Server-side encryption.**
+    Nextcloud supports [server-side encryption (SSE)](https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/encryption_configuration.html).
+    This is not an end-to-end encryption, but can be used to encrypt files that will be persisted
+    to external storage such as S3. Please note that this won't work anymore when using OpenSSL 3
+    for PHP's openssl extension because this is implemented using the legacy cipher RC4.
+    If [](#opt-system.stateVersion) is *above* `22.05`,
+    this is disabled by default. To turn it on again and for further information please refer to
+    [](#opt-services.nextcloud.enableBrokenCiphersForSSE).
+
+## Using an alternative webserver as reverse-proxy (e.g. `httpd`) {#module-services-nextcloud-httpd}
+
+By default, `nginx` is used as reverse-proxy for `nextcloud`.
+However, it's possible to use e.g. `httpd` by explicitly disabling
+`nginx` using [](#opt-services.nginx.enable) and fixing the
+settings `listen.owner` &amp; `listen.group` in the
+[corresponding `phpfpm` pool](#opt-services.phpfpm.pools).
+
+An exemplary configuration may look like this:
+```
+{ config, lib, pkgs, ... }: {
+  services.nginx.enable = false;
+  services.nextcloud = {
+    enable = true;
+    hostName = "localhost";
+
+    /* further, required options */
+  };
+  services.phpfpm.pools.nextcloud.settings = {
+    "listen.owner" = config.services.httpd.user;
+    "listen.group" = config.services.httpd.group;
+  };
+  services.httpd = {
+    enable = true;
+    adminAddr = "webmaster@localhost";
+    extraModules = [ "proxy_fcgi" ];
+    virtualHosts."localhost" = {
+      documentRoot = config.services.nextcloud.package;
+      extraConfig = ''
+        <Directory "${config.services.nextcloud.package}">
+          <FilesMatch "\.php$">
+            <If "-f %{REQUEST_FILENAME}">
+              SetHandler "proxy:unix:${config.services.phpfpm.pools.nextcloud.socket}|fcgi://localhost/"
+            </If>
+          </FilesMatch>
+          <IfModule mod_rewrite.c>
+            RewriteEngine On
+            RewriteBase /
+            RewriteRule ^index\.php$ - [L]
+            RewriteCond %{REQUEST_FILENAME} !-f
+            RewriteCond %{REQUEST_FILENAME} !-d
+            RewriteRule . /index.php [L]
+          </IfModule>
+          DirectoryIndex index.php
+          Require all granted
+          Options +FollowSymLinks
+        </Directory>
+      '';
+    };
+  };
+}
+```
+
+## Installing Apps and PHP extensions {#installing-apps-php-extensions-nextcloud}
+
+Nextcloud apps are installed statefully through the web interface.
+Some apps may require extra PHP extensions to be installed.
+This can be configured with the [](#opt-services.nextcloud.phpExtraExtensions) setting.
+
+Alternatively, extra apps can also be declared with the [](#opt-services.nextcloud.extraApps) setting.
+When using this setting, apps can no longer be managed statefully because this can lead to Nextcloud updating apps
+that are managed by Nix. If you want automatic updates it is recommended that you use web interface to install apps.
+
+## Maintainer information {#module-services-nextcloud-maintainer-info}
+
+As stated in the previous paragraph, we must provide a clean upgrade-path for Nextcloud
+since it cannot move more than one major version forward on a single upgrade. This chapter
+adds some notes how Nextcloud updates should be rolled out in the future.
+
+While minor and patch-level updates are no problem and can be done directly in the
+package-expression (and should be backported to supported stable branches after that),
+major-releases should be added in a new attribute (e.g. Nextcloud `v19.0.0`
+should be available in `nixpkgs` as `pkgs.nextcloud19`).
+To provide simple upgrade paths it's generally useful to backport those as well to stable
+branches. As long as the package-default isn't altered, this won't break existing setups.
+After that, the versioning-warning in the `nextcloud`-module should be
+updated to make sure that the
+[package](#opt-services.nextcloud.package)-option selects the latest version
+on fresh setups.
+
+If major-releases will be abandoned by upstream, we should check first if those are needed
+in NixOS for a safe upgrade-path before removing those. In that case we should keep those
+packages, but mark them as insecure in an expression like this (in
+`<nixpkgs/pkgs/servers/nextcloud/default.nix>`):
+```
+/* ... */
+{
+  nextcloud17 = generic {
+    version = "17.0.x";
+    sha256 = "0000000000000000000000000000000000000000000000000000";
+    eol = true;
+  };
+}
+```
+
+Ideally we should make sure that it's possible to jump two NixOS versions forward:
+i.e. the warnings and the logic in the module should guard a user to upgrade from a
+Nextcloud on e.g. 19.09 to a Nextcloud on 20.09.
diff --git a/nixos/modules/services/web-apps/nextcloud.xml b/nixos/modules/services/web-apps/nextcloud.xml
index 4207c4008d5b7..a5ac05723ef47 100644
--- a/nixos/modules/services/web-apps/nextcloud.xml
+++ b/nixos/modules/services/web-apps/nextcloud.xml
@@ -1,226 +1,249 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-nextcloud">
- <title>Nextcloud</title>
- <para>
-  <link xlink:href="https://nextcloud.com/">Nextcloud</link> is an open-source,
-  self-hostable cloud platform. The server setup can be automated using
-  <link linkend="opt-services.nextcloud.enable">services.nextcloud</link>. A
-  desktop client is packaged at <literal>pkgs.nextcloud-client</literal>.
- </para>
- <para>
-  The current default by NixOS is <package>nextcloud25</package> which is also the latest
-  major version available.
- </para>
- <section xml:id="module-services-nextcloud-basic-usage">
-  <title>Basic usage</title>
-
+<!-- Do not edit this file directly, edit its companion .md instead
+     and regenerate this file using nixos/doc/manual/md-to-db.sh -->
+<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-nextcloud">
+  <title>Nextcloud</title>
   <para>
-   Nextcloud is a PHP-based application which requires an HTTP server
-   (<literal><link linkend="opt-services.nextcloud.enable">services.nextcloud</link></literal>
-   optionally supports
-   <literal><link linkend="opt-services.nginx.enable">services.nginx</link></literal>)
-   and a database (it's recommended to use
-   <literal><link linkend="opt-services.postgresql.enable">services.postgresql</link></literal>).
+    <link xlink:href="https://nextcloud.com/">Nextcloud</link> is an
+    open-source, self-hostable cloud platform. The server setup can be
+    automated using
+    <link linkend="opt-services.nextcloud.enable">services.nextcloud</link>.
+    A desktop client is packaged at
+    <literal>pkgs.nextcloud-client</literal>.
   </para>
-
   <para>
-   A very basic configuration may look like this:
-<programlisting>{ pkgs, ... }:
+    The current default by NixOS is <literal>nextcloud25</literal> which
+    is also the latest major version available.
+  </para>
+  <section xml:id="module-services-nextcloud-basic-usage">
+    <title>Basic usage</title>
+    <para>
+      Nextcloud is a PHP-based application which requires an HTTP server
+      (<link linkend="opt-services.nextcloud.enable"><literal>services.nextcloud</literal></link>
+      optionally supports
+      <link linkend="opt-services.nginx.enable"><literal>services.nginx</literal></link>)
+      and a database (it’s recommended to use
+      <link linkend="opt-services.postgresql.enable"><literal>services.postgresql</literal></link>).
+    </para>
+    <para>
+      A very basic configuration may look like this:
+    </para>
+    <programlisting>
+{ pkgs, ... }:
 {
   services.nextcloud = {
-    <link linkend="opt-services.nextcloud.enable">enable</link> = true;
-    <link linkend="opt-services.nextcloud.hostName">hostName</link> = "nextcloud.tld";
+    enable = true;
+    hostName = &quot;nextcloud.tld&quot;;
     config = {
-      <link linkend="opt-services.nextcloud.config.dbtype">dbtype</link> = "pgsql";
-      <link linkend="opt-services.nextcloud.config.dbuser">dbuser</link> = "nextcloud";
-      <link linkend="opt-services.nextcloud.config.dbhost">dbhost</link> = "/run/postgresql"; # nextcloud will add /.s.PGSQL.5432 by itself
-      <link linkend="opt-services.nextcloud.config.dbname">dbname</link> = "nextcloud";
-      <link linkend="opt-services.nextcloud.config.adminpassFile">adminpassFile</link> = "/path/to/admin-pass-file";
-      <link linkend="opt-services.nextcloud.config.adminuser">adminuser</link> = "root";
+      dbtype = &quot;pgsql&quot;;
+      dbuser = &quot;nextcloud&quot;;
+      dbhost = &quot;/run/postgresql&quot;; # nextcloud will add /.s.PGSQL.5432 by itself
+      dbname = &quot;nextcloud&quot;;
+      adminpassFile = &quot;/path/to/admin-pass-file&quot;;
+      adminuser = &quot;root&quot;;
     };
   };
 
   services.postgresql = {
-    <link linkend="opt-services.postgresql.enable">enable</link> = true;
-    <link linkend="opt-services.postgresql.ensureDatabases">ensureDatabases</link> = [ "nextcloud" ];
-    <link linkend="opt-services.postgresql.ensureUsers">ensureUsers</link> = [
-     { name = "nextcloud";
-       ensurePermissions."DATABASE nextcloud" = "ALL PRIVILEGES";
+    enable = true;
+    ensureDatabases = [ &quot;nextcloud&quot; ];
+    ensureUsers = [
+     { name = &quot;nextcloud&quot;;
+       ensurePermissions.&quot;DATABASE nextcloud&quot; = &quot;ALL PRIVILEGES&quot;;
      }
     ];
   };
 
   # ensure that postgres is running *before* running the setup
-  systemd.services."nextcloud-setup" = {
-    requires = ["postgresql.service"];
-    after = ["postgresql.service"];
+  systemd.services.&quot;nextcloud-setup&quot; = {
+    requires = [&quot;postgresql.service&quot;];
+    after = [&quot;postgresql.service&quot;];
   };
 
-  <link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
-}</programlisting>
-  </para>
-
-  <para>
-   The <literal>hostName</literal> option is used internally to configure an HTTP
-   server using <literal><link xlink:href="https://php-fpm.org/">PHP-FPM</link></literal>
-   and <literal>nginx</literal>. The <literal>config</literal> attribute set is
-   used by the imperative installer and all values are written to an additional file
-   to ensure that changes can be applied by changing the module's options.
-  </para>
-
-  <para>
-   In case the application serves multiple domains (those are checked with
-   <literal><link xlink:href="http://php.net/manual/en/reserved.variables.server.php">$_SERVER['HTTP_HOST']</link></literal>)
-   it's needed to add them to
-   <literal><link linkend="opt-services.nextcloud.config.extraTrustedDomains">services.nextcloud.config.extraTrustedDomains</link></literal>.
-  </para>
-
-  <para>
-   Auto updates for Nextcloud apps can be enabled using
-   <literal><link linkend="opt-services.nextcloud.autoUpdateApps.enable">services.nextcloud.autoUpdateApps</link></literal>.
-</para>
-
- </section>
-
- <section xml:id="module-services-nextcloud-pitfalls-during-upgrade">
-  <title>Common problems</title>
-  <itemizedlist>
-   <listitem>
-    <formalpara>
-     <title>General notes</title>
-     <para>
-      Unfortunately Nextcloud appears to be very stateful when it comes to
-      managing its own configuration. The config file lives in the home directory
-      of the <literal>nextcloud</literal> user (by default
-      <literal>/var/lib/nextcloud/config/config.php</literal>) and is also used to
-      track several states of the application (e.g., whether installed or not).
-     </para>
-    </formalpara>
+  networking.firewall.allowedTCPPorts = [ 80 443 ];
+}
+</programlisting>
     <para>
-     All configuration parameters are also stored in
-     <filename>/var/lib/nextcloud/config/override.config.php</filename> which is generated by
-     the module and linked from the store to ensure that all values from
-     <filename>config.php</filename> can be modified by the module.
-     However <filename>config.php</filename> manages the application's state and shouldn't be
-     touched manually because of that.
+      The <literal>hostName</literal> option is used internally to
+      configure an HTTP server using
+      <link xlink:href="https://php-fpm.org/"><literal>PHP-FPM</literal></link>
+      and <literal>nginx</literal>. The <literal>config</literal>
+      attribute set is used by the imperative installer and all values
+      are written to an additional file to ensure that changes can be
+      applied by changing the module’s options.
+    </para>
+    <para>
+      In case the application serves multiple domains (those are checked
+      with
+      <link xlink:href="http://php.net/manual/en/reserved.variables.server.php"><literal>$_SERVER['HTTP_HOST']</literal></link>)
+      it’s needed to add them to
+      <link linkend="opt-services.nextcloud.config.extraTrustedDomains"><literal>services.nextcloud.config.extraTrustedDomains</literal></link>.
     </para>
-    <warning>
-     <para>Don't delete <filename>config.php</filename>! This file
-     tracks the application's state and a deletion can cause unwanted
-     side-effects!</para>
-    </warning>
-
-    <warning>
-     <para>Don't rerun <literal>nextcloud-occ
-     maintenance:install</literal>! This command tries to install the application
-     and can cause unwanted side-effects!</para>
-    </warning>
-   </listitem>
-   <listitem>
-    <formalpara>
-     <title>Multiple version upgrades</title>
-     <para>
-      Nextcloud doesn't allow to move more than one major-version forward. E.g., if you're on
-      <literal>v16</literal>, you cannot upgrade to <literal>v18</literal>, you need to upgrade to
-      <literal>v17</literal> first. This is ensured automatically as long as the
-      <link linkend="opt-system.stateVersion">stateVersion</link> is declared properly. In that case
-      the oldest version available (one major behind the one from the previous NixOS
-      release) will be selected by default and the module will generate a warning that reminds
-      the user to upgrade to latest Nextcloud <emphasis>after</emphasis> that deploy.
-     </para>
-    </formalpara>
-   </listitem>
-   <listitem>
-    <formalpara>
-     <title><literal>Error: Command "upgrade" is not defined.</literal></title>
-     <para>
-      This error usually occurs if the initial installation
-      (<command>nextcloud-occ maintenance:install</command>) has failed. After that, the application
-      is not installed, but the upgrade is attempted to be executed. Further context can
-      be found in <link xlink:href="https://github.com/NixOS/nixpkgs/issues/111175">NixOS/nixpkgs#111175</link>.
-     </para>
-    </formalpara>
     <para>
-     First of all, it makes sense to find out what went wrong by looking at the logs
-     of the installation via <command>journalctl -u nextcloud-setup</command> and try to fix
-     the underlying issue.
+      Auto updates for Nextcloud apps can be enabled using
+      <link linkend="opt-services.nextcloud.autoUpdateApps.enable"><literal>services.nextcloud.autoUpdateApps</literal></link>.
     </para>
+  </section>
+  <section xml:id="module-services-nextcloud-pitfalls-during-upgrade">
+    <title>Common problems</title>
     <itemizedlist>
-     <listitem>
-      <para>
-       If this occurs on an <emphasis>existing</emphasis> setup, this is most likely because
-       the maintenance mode is active. It can be deactivated by running
-       <command>nextcloud-occ maintenance:mode --off</command>. It's advisable though to
-       check the logs first on why the maintenance mode was activated.
-      </para>
-     </listitem>
-     <listitem>
-      <warning><para>Only perform the following measures on
-      <emphasis>freshly installed instances!</emphasis></para></warning>
-      <para>
-       A re-run of the installer can be forced by <emphasis>deleting</emphasis>
-       <filename>/var/lib/nextcloud/config/config.php</filename>. This is the only time
-       advisable because the fresh install doesn't have any state that can be lost.
-       In case that doesn't help, an entire re-creation can be forced via
-       <command>rm -rf ~nextcloud/</command>.
-      </para>
-     </listitem>
+      <listitem>
+        <para>
+          <emphasis role="strong">General notes.</emphasis>
+          Unfortunately Nextcloud appears to be very stateful when it
+          comes to managing its own configuration. The config file lives
+          in the home directory of the <literal>nextcloud</literal> user
+          (by default
+          <literal>/var/lib/nextcloud/config/config.php</literal>) and
+          is also used to track several states of the application (e.g.,
+          whether installed or not).
+        </para>
+        <para>
+          All configuration parameters are also stored in
+          <filename>/var/lib/nextcloud/config/override.config.php</filename>
+          which is generated by the module and linked from the store to
+          ensure that all values from <filename>config.php</filename>
+          can be modified by the module. However
+          <filename>config.php</filename> manages the application’s
+          state and shouldn’t be touched manually because of that.
+        </para>
+        <warning>
+          <para>
+            Don’t delete <filename>config.php</filename>! This file
+            tracks the application’s state and a deletion can cause
+            unwanted side-effects!
+          </para>
+        </warning>
+        <warning>
+          <para>
+            Don’t rerun
+            <literal>nextcloud-occ maintenance:install</literal>! This
+            command tries to install the application and can cause
+            unwanted side-effects!
+          </para>
+        </warning>
+      </listitem>
+      <listitem>
+        <para>
+          <emphasis role="strong">Multiple version upgrades.</emphasis>
+          Nextcloud doesn’t allow to move more than one major-version
+          forward. E.g., if you’re on <literal>v16</literal>, you cannot
+          upgrade to <literal>v18</literal>, you need to upgrade to
+          <literal>v17</literal> first. This is ensured automatically as
+          long as the
+          <link linkend="opt-system.stateVersion">stateVersion</link> is
+          declared properly. In that case the oldest version available
+          (one major behind the one from the previous NixOS release)
+          will be selected by default and the module will generate a
+          warning that reminds the user to upgrade to latest Nextcloud
+          <emphasis>after</emphasis> that deploy.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <emphasis role="strong"><literal>Error: Command &quot;upgrade&quot; is not defined.</literal></emphasis>
+          This error usually occurs if the initial installation
+          (<command>nextcloud-occ maintenance:install</command>) has
+          failed. After that, the application is not installed, but the
+          upgrade is attempted to be executed. Further context can be
+          found in
+          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/111175">NixOS/nixpkgs#111175</link>.
+        </para>
+        <para>
+          First of all, it makes sense to find out what went wrong by
+          looking at the logs of the installation via
+          <command>journalctl -u nextcloud-setup</command> and try to
+          fix the underlying issue.
+        </para>
+        <itemizedlist>
+          <listitem>
+            <para>
+              If this occurs on an <emphasis>existing</emphasis> setup,
+              this is most likely because the maintenance mode is
+              active. It can be deactivated by running
+              <command>nextcloud-occ maintenance:mode --off</command>.
+              It’s advisable though to check the logs first on why the
+              maintenance mode was activated.
+            </para>
+          </listitem>
+          <listitem>
+            <warning>
+              <para>
+                Only perform the following measures on <emphasis>freshly
+                installed instances!</emphasis>
+              </para>
+            </warning>
+            <para>
+              A re-run of the installer can be forced by
+              <emphasis>deleting</emphasis>
+              <filename>/var/lib/nextcloud/config/config.php</filename>.
+              This is the only time advisable because the fresh install
+              doesn’t have any state that can be lost. In case that
+              doesn’t help, an entire re-creation can be forced via
+              <command>rm -rf ~nextcloud/</command>.
+            </para>
+          </listitem>
+        </itemizedlist>
+      </listitem>
+      <listitem>
+        <para>
+          <emphasis role="strong">Server-side encryption.</emphasis>
+          Nextcloud supports
+          <link xlink:href="https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/encryption_configuration.html">server-side
+          encryption (SSE)</link>. This is not an end-to-end encryption,
+          but can be used to encrypt files that will be persisted to
+          external storage such as S3. Please note that this won’t work
+          anymore when using OpenSSL 3 for PHP’s openssl extension
+          because this is implemented using the legacy cipher RC4. If
+          <xref linkend="opt-system.stateVersion" /> is
+          <emphasis>above</emphasis> <literal>22.05</literal>, this is
+          disabled by default. To turn it on again and for further
+          information please refer to
+          <xref linkend="opt-services.nextcloud.enableBrokenCiphersForSSE" />.
+        </para>
+      </listitem>
     </itemizedlist>
-   </listitem>
-   <listitem>
-    <formalpara>
-     <title>Server-side encryption</title>
-     <para>
-      Nextcloud supports <link xlink:href="https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/encryption_configuration.html">server-side encryption (SSE)</link>.
-      This is not an end-to-end encryption, but can be used to encrypt files that will be persisted
-      to external storage such as S3. Please note that this won't work anymore when using OpenSSL 3
-      for PHP's openssl extension because this is implemented using the legacy cipher RC4.
-      If <xref linkend="opt-system.stateVersion" /> is <emphasis>above</emphasis> <literal>22.05</literal>,
-      this is disabled by default. To turn it on again and for further information please refer to
-      <xref linkend="opt-services.nextcloud.enableBrokenCiphersForSSE" />.
-     </para>
-    </formalpara>
-   </listitem>
-  </itemizedlist>
- </section>
-
- <section xml:id="module-services-nextcloud-httpd">
-  <title>Using an alternative webserver as reverse-proxy (e.g. <literal>httpd</literal>)</title>
-  <para>
-   By default, <package>nginx</package> is used as reverse-proxy for <package>nextcloud</package>.
-   However, it's possible to use e.g. <package>httpd</package> by explicitly disabling
-   <package>nginx</package> using <xref linkend="opt-services.nginx.enable" /> and fixing the
-   settings <literal>listen.owner</literal> &amp; <literal>listen.group</literal> in the
-   <link linkend="opt-services.phpfpm.pools">corresponding <literal>phpfpm</literal> pool</link>.
-  </para>
-  <para>
-   An exemplary configuration may look like this:
-<programlisting>{ config, lib, pkgs, ... }: {
-  <link linkend="opt-services.nginx.enable">services.nginx.enable</link> = false;
+  </section>
+  <section xml:id="module-services-nextcloud-httpd">
+    <title>Using an alternative webserver as reverse-proxy (e.g.
+    <literal>httpd</literal>)</title>
+    <para>
+      By default, <literal>nginx</literal> is used as reverse-proxy for
+      <literal>nextcloud</literal>. However, it’s possible to use e.g.
+      <literal>httpd</literal> by explicitly disabling
+      <literal>nginx</literal> using
+      <xref linkend="opt-services.nginx.enable" /> and fixing the
+      settings <literal>listen.owner</literal> &amp;
+      <literal>listen.group</literal> in the
+      <link linkend="opt-services.phpfpm.pools">corresponding
+      <literal>phpfpm</literal> pool</link>.
+    </para>
+    <para>
+      An exemplary configuration may look like this:
+    </para>
+    <programlisting>
+{ config, lib, pkgs, ... }: {
+  services.nginx.enable = false;
   services.nextcloud = {
-    <link linkend="opt-services.nextcloud.enable">enable</link> = true;
-    <link linkend="opt-services.nextcloud.hostName">hostName</link> = "localhost";
+    enable = true;
+    hostName = &quot;localhost&quot;;
 
     /* further, required options */
   };
-  <link linkend="opt-services.phpfpm.pools._name_.settings">services.phpfpm.pools.nextcloud.settings</link> = {
-    "listen.owner" = config.services.httpd.user;
-    "listen.group" = config.services.httpd.group;
+  services.phpfpm.pools.nextcloud.settings = {
+    &quot;listen.owner&quot; = config.services.httpd.user;
+    &quot;listen.group&quot; = config.services.httpd.group;
   };
   services.httpd = {
-    <link linkend="opt-services.httpd.enable">enable</link> = true;
-    <link linkend="opt-services.httpd.adminAddr">adminAddr</link> = "webmaster@localhost";
-    <link linkend="opt-services.httpd.extraModules">extraModules</link> = [ "proxy_fcgi" ];
-    virtualHosts."localhost" = {
-      <link linkend="opt-services.httpd.virtualHosts._name_.documentRoot">documentRoot</link> = config.services.nextcloud.package;
-      <link linkend="opt-services.httpd.virtualHosts._name_.extraConfig">extraConfig</link> = ''
-        &lt;Directory "${config.services.nextcloud.package}"&gt;
-          &lt;FilesMatch "\.php$"&gt;
-            &lt;If "-f %{REQUEST_FILENAME}"&gt;
-              SetHandler "proxy:unix:${config.services.phpfpm.pools.nextcloud.socket}|fcgi://localhost/"
+    enable = true;
+    adminAddr = &quot;webmaster@localhost&quot;;
+    extraModules = [ &quot;proxy_fcgi&quot; ];
+    virtualHosts.&quot;localhost&quot; = {
+      documentRoot = config.services.nextcloud.package;
+      extraConfig = ''
+        &lt;Directory &quot;${config.services.nextcloud.package}&quot;&gt;
+          &lt;FilesMatch &quot;\.php$&quot;&gt;
+            &lt;If &quot;-f %{REQUEST_FILENAME}&quot;&gt;
+              SetHandler &quot;proxy:unix:${config.services.phpfpm.pools.nextcloud.socket}|fcgi://localhost/&quot;
             &lt;/If&gt;
           &lt;/FilesMatch&gt;
           &lt;IfModule mod_rewrite.c&gt;
@@ -238,68 +261,73 @@
       '';
     };
   };
-}</programlisting>
-  </para>
- </section>
-
- <section xml:id="installing-apps-php-extensions-nextcloud">
-  <title>Installing Apps and PHP extensions</title>
-
-  <para>
-   Nextcloud apps are installed statefully through the web interface.
-
-   Some apps may require extra PHP extensions to be installed.
-   This can be configured with the <xref linkend="opt-services.nextcloud.phpExtraExtensions" /> setting.
-  </para>
-
-  <para>
-   Alternatively, extra apps can also be declared with the <xref linkend="opt-services.nextcloud.extraApps" /> setting.
-   When using this setting, apps can no longer be managed statefully because this can lead to Nextcloud updating apps
-   that are managed by Nix. If you want automatic updates it is recommended that you use web interface to install apps.
-  </para>
- </section>
-
- <section xml:id="module-services-nextcloud-maintainer-info">
-  <title>Maintainer information</title>
-
-  <para>
-   As stated in the previous paragraph, we must provide a clean upgrade-path for Nextcloud
-   since it cannot move more than one major version forward on a single upgrade. This chapter
-   adds some notes how Nextcloud updates should be rolled out in the future.
-  </para>
-
-  <para>
-   While minor and patch-level updates are no problem and can be done directly in the
-   package-expression (and should be backported to supported stable branches after that),
-   major-releases should be added in a new attribute (e.g. Nextcloud <literal>v19.0.0</literal>
-   should be available in <literal>nixpkgs</literal> as <literal>pkgs.nextcloud19</literal>).
-   To provide simple upgrade paths it's generally useful to backport those as well to stable
-   branches. As long as the package-default isn't altered, this won't break existing setups.
-   After that, the versioning-warning in the <literal>nextcloud</literal>-module should be
-   updated to make sure that the
-   <link linkend="opt-services.nextcloud.package">package</link>-option selects the latest version
-   on fresh setups.
-  </para>
-
-  <para>
-   If major-releases will be abandoned by upstream, we should check first if those are needed
-   in NixOS for a safe upgrade-path before removing those. In that case we should keep those
-   packages, but mark them as insecure in an expression like this (in
-   <literal>&lt;nixpkgs/pkgs/servers/nextcloud/default.nix&gt;</literal>):
-<programlisting>/* ... */
+}
+</programlisting>
+  </section>
+  <section xml:id="installing-apps-php-extensions-nextcloud">
+    <title>Installing Apps and PHP extensions</title>
+    <para>
+      Nextcloud apps are installed statefully through the web interface.
+      Some apps may require extra PHP extensions to be installed. This
+      can be configured with the
+      <xref linkend="opt-services.nextcloud.phpExtraExtensions" />
+      setting.
+    </para>
+    <para>
+      Alternatively, extra apps can also be declared with the
+      <xref linkend="opt-services.nextcloud.extraApps" /> setting. When
+      using this setting, apps can no longer be managed statefully
+      because this can lead to Nextcloud updating apps that are managed
+      by Nix. If you want automatic updates it is recommended that you
+      use web interface to install apps.
+    </para>
+  </section>
+  <section xml:id="module-services-nextcloud-maintainer-info">
+    <title>Maintainer information</title>
+    <para>
+      As stated in the previous paragraph, we must provide a clean
+      upgrade-path for Nextcloud since it cannot move more than one
+      major version forward on a single upgrade. This chapter adds some
+      notes how Nextcloud updates should be rolled out in the future.
+    </para>
+    <para>
+      While minor and patch-level updates are no problem and can be done
+      directly in the package-expression (and should be backported to
+      supported stable branches after that), major-releases should be
+      added in a new attribute (e.g. Nextcloud
+      <literal>v19.0.0</literal> should be available in
+      <literal>nixpkgs</literal> as
+      <literal>pkgs.nextcloud19</literal>). To provide simple upgrade
+      paths it’s generally useful to backport those as well to stable
+      branches. As long as the package-default isn’t altered, this won’t
+      break existing setups. After that, the versioning-warning in the
+      <literal>nextcloud</literal>-module should be updated to make sure
+      that the
+      <link linkend="opt-services.nextcloud.package">package</link>-option
+      selects the latest version on fresh setups.
+    </para>
+    <para>
+      If major-releases will be abandoned by upstream, we should check
+      first if those are needed in NixOS for a safe upgrade-path before
+      removing those. In that case we should keep those packages, but
+      mark them as insecure in an expression like this (in
+      <literal>&lt;nixpkgs/pkgs/servers/nextcloud/default.nix&gt;</literal>):
+    </para>
+    <programlisting>
+/* ... */
 {
   nextcloud17 = generic {
-    version = "17.0.x";
-    sha256 = "0000000000000000000000000000000000000000000000000000";
+    version = &quot;17.0.x&quot;;
+    sha256 = &quot;0000000000000000000000000000000000000000000000000000&quot;;
     eol = true;
   };
-}</programlisting>
-  </para>
-
-  <para>
-   Ideally we should make sure that it's possible to jump two NixOS versions forward:
-   i.e. the warnings and the logic in the module should guard a user to upgrade from a
-   Nextcloud on e.g. 19.09 to a Nextcloud on 20.09.
-  </para>
- </section>
+}
+</programlisting>
+    <para>
+      Ideally we should make sure that it’s possible to jump two NixOS
+      versions forward: i.e. the warnings and the logic in the module
+      should guard a user to upgrade from a Nextcloud on e.g. 19.09 to a
+      Nextcloud on 20.09.
+    </para>
+  </section>
 </chapter>
diff --git a/nixos/modules/services/web-apps/pict-rs.md b/nixos/modules/services/web-apps/pict-rs.md
index 4b622049909d2..2fa6bb3aebced 100644
--- a/nixos/modules/services/web-apps/pict-rs.md
+++ b/nixos/modules/services/web-apps/pict-rs.md
@@ -15,6 +15,7 @@ this will start the http server on port 8080 by default.
 ## Usage {#module-services-pict-rs-usage}
 
 pict-rs offers the following endpoints:
+
 - `POST /image` for uploading an image. Uploaded content must be valid multipart/form-data with an
     image array located within the `images[]` key
 
diff --git a/nixos/modules/services/web-apps/pict-rs.nix b/nixos/modules/services/web-apps/pict-rs.nix
index ee9ff9b484f6f..ad07507ca37db 100644
--- a/nixos/modules/services/web-apps/pict-rs.nix
+++ b/nixos/modules/services/web-apps/pict-rs.nix
@@ -5,8 +5,6 @@ let
 in
 {
   meta.maintainers = with maintainers; [ happysalada ];
-  # Don't edit the docbook xml directly, edit the md and generate it:
-  # `pandoc pict-rs.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > pict-rs.xml`
   meta.doc = ./pict-rs.xml;
 
   options.services.pict-rs = {
diff --git a/nixos/modules/services/web-apps/pict-rs.xml b/nixos/modules/services/web-apps/pict-rs.xml
index bf129f5cc2ac2..3f5900c55f151 100644
--- a/nixos/modules/services/web-apps/pict-rs.xml
+++ b/nixos/modules/services/web-apps/pict-rs.xml
@@ -1,3 +1,5 @@
+<!-- Do not edit this file directly, edit its companion .md instead
+     and regenerate this file using nixos/doc/manual/md-to-db.sh -->
 <chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-pict-rs">
   <title>Pict-rs</title>
   <para>
@@ -8,7 +10,7 @@
     <para>
       the minimum to start pict-rs is
     </para>
-    <programlisting language="bash">
+    <programlisting language="nix">
 services.pict-rs.enable = true;
 </programlisting>
     <para>
@@ -18,14 +20,20 @@ services.pict-rs.enable = true;
   <section xml:id="module-services-pict-rs-usage">
     <title>Usage</title>
     <para>
-      pict-rs offers the following endpoints: -
-      <literal>POST /image</literal> for uploading an image. Uploaded
-      content must be valid multipart/form-data with an image array
-      located within the <literal>images[]</literal> key
+      pict-rs offers the following endpoints:
     </para>
-    <programlisting>
-This endpoint returns the following JSON structure on success with a 201 Created status
-```json
+    <itemizedlist>
+      <listitem>
+        <para>
+          <literal>POST /image</literal> for uploading an image.
+          Uploaded content must be valid multipart/form-data with an
+          image array located within the <literal>images[]</literal> key
+        </para>
+        <para>
+          This endpoint returns the following JSON structure on success
+          with a 201 Created status
+        </para>
+        <programlisting language="json">
 {
     &quot;files&quot;: [
         {
@@ -43,9 +51,8 @@ This endpoint returns the following JSON structure on success with a 201 Created
     ],
     &quot;msg&quot;: &quot;ok&quot;
 }
-```
 </programlisting>
-    <itemizedlist>
+      </listitem>
       <listitem>
         <para>
           <literal>GET /image/download?url=...</literal> Download an
@@ -66,8 +73,20 @@ This endpoint returns the following JSON structure on success with a 201 Created
           <literal>GET /image/details/original/{file}</literal> for
           getting the details of a full-resolution image. The returned
           JSON is structured like so:
-          <literal>json     {         &quot;width&quot;: 800,         &quot;height&quot;: 537,         &quot;content_type&quot;: &quot;image/webp&quot;,         &quot;created_at&quot;: [             2020,             345,             67376,             394363487         ]     }</literal>
         </para>
+        <programlisting language="json">
+{
+    &quot;width&quot;: 800,
+    &quot;height&quot;: 537,
+    &quot;content_type&quot;: &quot;image/webp&quot;,
+    &quot;created_at&quot;: [
+        2020,
+        345,
+        67376,
+        394363487
+    ]
+}
+</programlisting>
       </listitem>
       <listitem>
         <para>
@@ -124,7 +143,11 @@ This endpoint returns the following JSON structure on success with a 201 Created
         </para>
         <para>
           An example of usage could be
-          <literal>GET /image/process.jpg?src=asdf.png&amp;thumbnail=256&amp;blur=3.0</literal>
+        </para>
+        <programlisting>
+GET /image/process.jpg?src=asdf.png&amp;thumbnail=256&amp;blur=3.0
+</programlisting>
+        <para>
           which would create a 256x256px JPEG thumbnail and blur it
         </para>
       </listitem>
diff --git a/nixos/modules/services/web-apps/plausible.md b/nixos/modules/services/web-apps/plausible.md
new file mode 100644
index 0000000000000..1328ce69441a0
--- /dev/null
+++ b/nixos/modules/services/web-apps/plausible.md
@@ -0,0 +1,35 @@
+# Plausible {#module-services-plausible}
+
+[Plausible](https://plausible.io/) is a privacy-friendly alternative to
+Google analytics.
+
+## Basic Usage {#module-services-plausible-basic-usage}
+
+At first, a secret key is needed to be generated. This can be done with e.g.
+```ShellSession
+$ openssl rand -base64 64
+```
+
+After that, `plausible` can be deployed like this:
+```
+{
+  services.plausible = {
+    enable = true;
+    adminUser = {
+      # activate is used to skip the email verification of the admin-user that's
+      # automatically created by plausible. This is only supported if
+      # postgresql is configured by the module. This is done by default, but
+      # can be turned off with services.plausible.database.postgres.setup.
+      activate = true;
+      email = "admin@localhost";
+      passwordFile = "/run/secrets/plausible-admin-pwd";
+    };
+    server = {
+      baseUrl = "http://analytics.example.org";
+      # secretKeybaseFile is a path to the file which contains the secret generated
+      # with openssl as described above.
+      secretKeybaseFile = "/run/secrets/plausible-secret-key-base";
+    };
+  };
+}
+```
diff --git a/nixos/modules/services/web-apps/plausible.xml b/nixos/modules/services/web-apps/plausible.xml
index 92a571b9fbdb5..39ff004ffd95f 100644
--- a/nixos/modules/services/web-apps/plausible.xml
+++ b/nixos/modules/services/web-apps/plausible.xml
@@ -1,51 +1,45 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-plausible">
- <title>Plausible</title>
- <para>
-  <link xlink:href="https://plausible.io/">Plausible</link> is a privacy-friendly alternative to
-  Google analytics.
- </para>
- <section xml:id="module-services-plausible-basic-usage">
-  <title>Basic Usage</title>
+<!-- Do not edit this file directly, edit its companion .md instead
+     and regenerate this file using nixos/doc/manual/md-to-db.sh -->
+<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-plausible">
+  <title>Plausible</title>
   <para>
-   At first, a secret key is needed to be generated. This can be done with e.g.
-   <screen><prompt>$ </prompt>openssl rand -base64 64</screen>
+    <link xlink:href="https://plausible.io/">Plausible</link> is a
+    privacy-friendly alternative to Google analytics.
   </para>
-  <para>
-   After that, <package>plausible</package> can be deployed like this:
-<programlisting>{
+  <section xml:id="module-services-plausible-basic-usage">
+    <title>Basic Usage</title>
+    <para>
+      At first, a secret key is needed to be generated. This can be done
+      with e.g.
+    </para>
+    <programlisting>
+$ openssl rand -base64 64
+</programlisting>
+    <para>
+      After that, <literal>plausible</literal> can be deployed like
+      this:
+    </para>
+    <programlisting>
+{
   services.plausible = {
-    <link linkend="opt-services.plausible.enable">enable</link> = true;
+    enable = true;
     adminUser = {
-      <link linkend="opt-services.plausible.adminUser.activate">activate</link> = true; <co xml:id='ex-plausible-cfg-activate' />
-      <link linkend="opt-services.plausible.adminUser.email">email</link> = "admin@localhost";
-      <link linkend="opt-services.plausible.adminUser.passwordFile">passwordFile</link> = "/run/secrets/plausible-admin-pwd";
+      # activate is used to skip the email verification of the admin-user that's
+      # automatically created by plausible. This is only supported if
+      # postgresql is configured by the module. This is done by default, but
+      # can be turned off with services.plausible.database.postgres.setup.
+      activate = true;
+      email = &quot;admin@localhost&quot;;
+      passwordFile = &quot;/run/secrets/plausible-admin-pwd&quot;;
     };
     server = {
-      <link linkend="opt-services.plausible.server.baseUrl">baseUrl</link> = "http://analytics.example.org";
-      <link linkend="opt-services.plausible.server.secretKeybaseFile">secretKeybaseFile</link> = "/run/secrets/plausible-secret-key-base"; <co xml:id='ex-plausible-cfg-secretbase' />
+      baseUrl = &quot;http://analytics.example.org&quot;;
+      # secretKeybaseFile is a path to the file which contains the secret generated
+      # with openssl as described above.
+      secretKeybaseFile = &quot;/run/secrets/plausible-secret-key-base&quot;;
     };
   };
-}</programlisting>
-   <calloutlist>
-    <callout arearefs='ex-plausible-cfg-activate'>
-     <para>
-      <varname>activate</varname> is used to skip the email verification of the admin-user that's
-      automatically created by <package>plausible</package>. This is only supported if
-      <package>postgresql</package> is configured by the module. This is done by default, but
-      can be turned off with <xref linkend="opt-services.plausible.database.postgres.setup" />.
-     </para>
-    </callout>
-    <callout arearefs='ex-plausible-cfg-secretbase'>
-     <para>
-      <varname>secretKeybaseFile</varname> is a path to the file which contains the secret generated
-      with <package>openssl</package> as described above.
-     </para>
-    </callout>
-   </calloutlist>
-  </para>
- </section>
+}
+</programlisting>
+  </section>
 </chapter>