Skip to content

Installation

Mikhail Deynekin edited this page Nov 13, 2026 · 2 revisions

Installation

Requirements

Requirement Why Check
PHP ≥ 8.5 src/bootstrap.php answers HTTP 500 below the floor php -v
mbstring, json, dom, libxml the bootstrap refuses to run without them php -m
zip backups and the self-updater php -m | grep zip
curl update check, release download php -m | grep curl
vendor/autoload.php the bootstrap exits when it is missing test -f vendor/autoload.php
storage/ writable by the PHP user cache, index, logs, backups, rate-limit state test -w storage
content root readable documents live in content/ unless BROWSE_DIR says otherwise test -r content

PHP 8.3 and 8.4 are not supported: 3.4.0 dropped them. Any 8.5.x patch is accepted, so a host on the 8.5 line is never bricked by a patch-level floor; 8.5.12 is the version the project is verified on.

Layout A — document root at public/ (recommended)

server {
    server_name docs.example.com;
    root /var/www/docs/public;
    index index.php;

    location / { try_files $uri $uri/ /index.php?$args; }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/run/php/php8.5-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

.md.ini stays above the document root, so it cannot be downloaded.

Layout B — document root at the project root (shared hosting)

Works out of the box: the root index.php, md.php and updater.php forward to the same engine with asset URLs computed for the /public prefix. The shipped .htaccess must be honoured — verify that GET /.md.ini returns 403/404, otherwise switch to layout A immediately.

Layout C — built-in server (development)

php -S localhost:8080 -t public public/router.php

Sub-directory installs

Mounting the app under a prefix (https://host/docs/ or a symlink inside another site's docroot) needs no configuration: the front controller derives its web prefix from SCRIPT_NAME, so assets, links and the service-worker scope stay inside the prefix.

On a docroot-at-public install the updater is reached through the shipped public/updater.php bridge, so /updater.php answers in both layouts.

Permissions

chown -R www-data:www-data storage
chmod -R ug+rwX storage
chmod 640 .md.ini

storage/logs/setup.log (the one-time setup code) is created 0600 and should stay that way.

The PWA manifest needs no server configuration

?api=manifest returns the manifest with Content-Type: application/manifest+json and the layout links to it, so nothing depends on the host knowing .webmanifest.

Do not "fix" the static public/manifest.webmanifest with a types { } block: that block replaces the whole inherited MIME table (Debian's mime.types is itself wrapped in one), and every .css/.js/.svg on the host becomes application/octet-stream — the site then renders unstyled. Measured on nginx 1.24. If you need the static file typed, map only the extension:

location ~ \.webmanifest$ {
    default_type application/manifest+json;
    try_files $uri =404;
}

Verified installations

Host Layout PHP Checked
Ubuntu 26.04, nginx, PHP-FPM per-site pool A, mounted in a sub-directory next to another application 8.5.12 viewer, library, tags, search operators, missing document → 404, ?api=manifest answering application/manifest+json, sw.js served, tables fitting the reading column (734px in a 736px column, 0 clipped cells), machine API status / conflicts / install dry_run (949 files verified against the release archive), public/updater.php bridge

Upgrading

Copy the new tree over the old one and keep content/, storage/ and .md.ini. Or let the application do it: see Updates.

Clone this wiki locally