Use case · Documentation wiki

Wiki.js self hosted on a dedicated server, PostgreSQL behind it

A self-hosted Wiki.js is one Node.js container and one PostgreSQL container: the requirements page says a single core runs it, two cores let the background workers keep up, and Linux needs at least 1 GB of RAM. This page runs the stable 2.x branch with Docker Compose, keeps port 3000 on 127.0.0.1 until the admin account exists, then puts a proxy, a firewall, a nightly dump and an update routine around it.

See the machines

What Wiki.js 2.x asks of the machine

ResourceWiki.js requirements page
CPURuns on a single core; 2 or more recommended so the background workers get used
RAMAt least 1 GB on Linux; the process sits near 70 MB, with short bursts while rendering or indexing
StorageAt least 1 GB recommended; text wikis stay at a few megabytes, uploads change that
DatabasePostgreSQL 9.5 or later, latest preferred; the PostgreSQL Docker image already carries the pg_trgm extension its search module needs
Node.js22 or 24, already inside the Docker image
DomainA dedicated domain or sub-domain; Wiki.js cannot live in a subfolder

Nothing in that table needs more than the smallest machine we rent. A whole server adds isolation: the database and uploads sit on disks no other customer touches, and you decide when it reboots.

Which version: 2.x, not the 3.0 beta

The 2.x branch now lives in the requarks/wiki-v2 repository, described there as the maintenance version; v2.5.316 was tagged on 2 October 2026. The main requarks/wiki repository carries 3.0, and its README calls it a beta of the upcoming release, not for production use. Run 2.x.

Pick PostgreSQL even though 2.x also accepts MySQL, MariaDB, MS SQL and SQLite. The requirements page says those four will not be supported in the next major version and that SQLite is not recommended for production, and the high-availability mode works only on PostgreSQL. Still choosing a wiki at all? The same exercise for BookStack and for Outline is on their own pages.

Choosing the machine

Any line on the dedicated server range clears the requirements. A Moonshot cartridge is one small server per slot, built for one service per box, and a documentation wiki is exactly that. A single-socket rack server suits a wiki sharing the machine with the rest of a team's tooling, for instance a Forgejo Git host for the pages to sync into.

  • Console. Every racked single-, dual- and quad-socket server has an out-of-band console (HPE iLO, Dell iDRAC or a Supermicro BMC); among the cartridges only the m510 and m710x have one, the rest have power control only. A console is what gets you back in after a firewall mistake.
  • Architecture. Stay on an x86 line. The X-Gene ARM cartridge boots Ubuntu 16.04 and 18.04 and nothing else, both older than the Ubuntu 24.04 this page starts from.
  • Network. Every line has an unmetered port and one IPv4; IPv6 is added at checkout at no charge. The firewall below covers both address families.
  • Delivery. A racked machine is handed over in about 30 minutes once payment is approved; a built-to-order line takes 4 to 24 hours.

Firewall, secret and Compose file

Start from Ubuntu 24.04 LTS, one of the releases the Wiki.js Ubuntu guide covers. Add Docker's apt repository and install Docker Engine with the Compose plugin; the engine should be release 28 or later. Then enable ufw with nothing but SSH allowed, before any container exists, and put the database password in a file readable by root alone. ufw makes IPv6 copies only of rules added while IPV6=yes, hence the sed line at the top.

sudo sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
sudo ufw allow OpenSSH
sudo ufw --force enable
sudo mkdir -p /opt/wiki
cd /opt/wiki
sudo install -m 600 /dev/null db.env
PW=$(openssl rand -hex 32)
printf "POSTGRES_PASSWORD='%s'\n" "$PW" | sudo tee -a db.env > /dev/null
printf "DB_PASS='%s'\n" "$PW" | sudo tee -a db.env > /dev/null
unset PW

Save the following as /opt/wiki/compose.yaml. It starts from the Compose example in the Wiki.js Docker guide and changes four things:

  • Password. The guide's example writes it inline; here it comes from db.env, mode 600. The Ubuntu guide's DB_PASS_FILE mount does not suit a root-only file, because the wiki container runs as an unprivileged user (the Docker guide names it wiki) that cannot read it. Anyone allowed to run docker can still see the value with docker inspect, so keep Docker access to root holders.
  • PostgreSQL 17, pinned as in the Ubuntu guide rather than left to float.
  • Logging. The example's logging block turns the database container's logs off; it is dropped so database errors stay visible in docker compose logs db.
  • Port. 3000 is published on 127.0.0.1 only.
# /opt/wiki/compose.yaml
services:
  db:
    image: postgres:17
    env_file: db.env
    environment:
      POSTGRES_DB: wiki
      POSTGRES_USER: wikijs
    restart: unless-stopped
    volumes:
      - db-data:/var/lib/postgresql/data

  wiki:
    image: ghcr.io/requarks/wiki:2
    depends_on:
      - db
    init: true
    env_file: db.env
    environment:
      DB_TYPE: postgres
      DB_HOST: db
      DB_PORT: 5432
      DB_USER: wikijs
      DB_NAME: wiki
    restart: unless-stopped
    ports:
      - "127.0.0.1:3000:3000"

volumes:
  db-data:

Why 127.0.0.1: a mapping such as 3000:3000 with no address is published on every host address, 0.0.0.0 and [::]. On IPv4 that traffic skips ufw's rules. IPv6 traffic bypasses ufw too if Docker's own IPv6 networking is enabled; if not, the [::] socket belongs to docker-proxy and ufw still applies. A loopback binding sidesteps all of it, and on Docker Engine 28 and later it is reachable from the machine itself only.

Bring the stack up from /opt/wiki with sudo docker compose up -d. The :2 tag follows the newest 2.x release, which the Docker guide recommends over latest.

The setup wizard, over an SSH tunnel

On first start, Wiki.js serves a setup wizard to whoever loads the site, and that visitor creates the root administrator. Finish it before anything public points at port 3000.

  1. On your workstation, forward a local port through SSH as your usual user: ssh -N -L 3000:127.0.0.1:3000 you@wiki-host.
  2. Browse to http://localhost:3000. The wizard asks for the administrator email, a password and the Site URL; give it the public address the wiki will have, e.g. https://wiki.example.com. An optional telemetry switch sits on the same form.
  3. Still on the tunnel, sign in and lock the wiki before it is public: under Administration, Groups, set the Guests group's page rule to Deny (or strip its global permissions), and under Administration, Authentication, Local, confirm self-registration is off.
  4. Only then go public. Point a DNS record for that name at the server, install Caddy from Caddy's apt repository, and give /etc/caddy/Caddyfile one site block for wiki.example.com holding the single directive reverse_proxy 127.0.0.1:3000. Caddy requests the certificate itself.
  5. Open the web ports: sudo ufw allow 80,443/tcp. Caddy runs on the host as an ordinary process, so without that rule ufw blocks it on IPv4 and IPv6 alike. Because IPV6=yes went in before any rule, this one is written for both families.

Change the Site URL later under Administration, General; the troubleshooting page traces wrong links in outgoing emails to an unset one.

Who can read, who can register

The step above exists because of two defaults in the setup code (server/setup.js in requarks/wiki-v2): the Local login is created with self-registration off, and the Guests group gets read access to every path.

  • Registration. Create accounts yourself under Users. If you turn self-registration on, the auth docs let you restrict it to email domains and pick the group new users land in.
  • Guests. The groups guide gives two equivalent ways to make a wiki private: switch the Guests group's default page rule from Allow to Deny, or remove its global permissions. Re-check it after any change to groups.
  • Second factor. From 2.5, Administration, Security has an option to enforce 2FA for every user.

Storage targets and Git sync

Every page lives in the database. Storage modules write a second copy to Amazon S3, Azure Blob Storage, DigitalOcean Spaces, Git, the local file system or SFTP; Git alone syncs both ways, so edits made in a clone come back.

  • Give Wiki.js a repository of its own; a subfolder or submodule of another repository is not supported.
  • Sync runs every 5 minutes by default, not on each save; pages written before Git was switched on need the Add Untracked Changes action.

Getting the SSH key into the container is our own arrangement, not the docs': they ask only for a path Wiki.js can read and, for permission trouble with mounted keys, offer running the container as root, which the Docker guide itself flags as not a secure way to run containers. From /opt/wiki:

  1. Create the key with no passphrase, since the Git storage guide says protected keys will not work: sudo ssh-keygen -t rsa -b 4096 -N '' -f /opt/wiki/git-key.
  2. Add git-key.pub to the repository as a deploy key with write access.
  3. Hand the private key to the container's user: sudo chown "$(sudo docker compose exec -T wiki id -u)" /opt/wiki/git-key && sudo chmod 600 /opt/wiki/git-key. Keep the -T: without it the TTY appends a carriage return to the uid.
  4. Under the wiki service in compose.yaml, add volumes: with the entry ./git-key:/etc/wiki/git-key:ro, then recreate it with sudo docker compose up -d.
  5. In Administration, Storage, Git, choose ssh authentication and enter /etc/wiki/git-key as the SSH Private Key Path.

Backups, restores and updates

A database dump is the whole backup. Pages live in the database, and so do uploads: the asset model in the 2.x source writes each file into an assetData table and keeps a cache on disk that it refills from there. That is why the wiki container needs no data volume. If you enable the Local File System target, mount its path from the host so those copies outlive the container. Create the backup directory first, then a nightly cron entry that dumps through Compose, with no TTY:

sudo mkdir -p -m 700 /var/backups/wiki
printf '%s\n' '30 3 * * * root umask 077; cd /opt/wiki && docker compose exec -T db pg_dump -U wikijs -F c wiki > /var/backups/wiki/wiki-$(date +\%F).dump' | sudo tee /etc/cron.d/wiki-backup

The dump holds every page and the password hashes, hence 700 and 600. Copy dumps, compose.yaml and db.env to another machine yourself.

A restore follows the transfer guide (--no-TTY below is the long form of -T), but check the file first: the drop step deletes the live database, and a missing or empty dump would leave you with nothing. The check lists only the archive's contents, so it catches those, not every kind of damage. From /opt/wiki, with D set to the dump, for example D=/var/backups/wiki/wiki-2026-10-23.dump:

  1. sudo test -s "$D" && sudo cat "$D" | sudo docker compose exec --no-TTY db pg_restore --list > /dev/null && echo dump-ok. No dump-ok, no restore: the file is empty, missing or unreadable.
  2. sudo docker compose stop wiki
  3. sudo docker compose exec --no-TTY db dropdb -U wikijs wiki, then the same with createdb in place of dropdb
  4. sudo cat "$D" | sudo docker compose exec --no-TTY db pg_restore -U wikijs -d wiki, then sudo docker compose start wiki

To update, take a fresh dump, then run sudo docker compose pull wiki and sudo docker compose up --force-recreate -d, the two commands the upgrade page gives. Back up first: the same page warns there is no going back once the schema has been upgraded.

The licence: AGPL-3.0

Both the 2.x and 3.0 repositories carry the GNU Affero General Public License v3.0. Our general reading, which is not legal advice: running Wiki.js unmodified for your team costs nothing and asks nothing of you. Distribute copies to anyone else and they receive it under the same licence, with its source. Modify it and let anyone, your own staff included, use the modified version over a network, and the licence requires that those users can get its source too. Pages your team writes are your content, not part of the program.

Questions

Can Wiki.js sit under a path like example.com/wiki?

No. The requirements page says Wiki.js needs a dedicated domain or sub-domain and cannot be mapped to a subfolder, and the troubleshooting page lists subdirectory proxying as a known cause of errors. Use a name such as wiki.example.com.

Should I use the built-in Let's Encrypt instead of Caddy?

Wiki.js 2.1 and later can request its own certificate through SSL_ACTIVE and LETSENCRYPT_DOMAIN, but the container must then publish its HTTP port and HTTPS port 3443 to the internet itself, and the HTTP port has to stay open for renewals. A host proxy keeps the container on 127.0.0.1 and leaves certificates to one process you can reuse for other sites.

What about the Update Companion from the Ubuntu guide?

It is an extra container that runs upgrades from the Administration area by mounting the Docker socket, which controls every container on the host. This page leaves it out and updates with two Compose commands; that is our choice, not a Wiki.js requirement.

Lost the admin password: how is it reset?

The troubleshooting page resets it in the database: generate a bcrypt hash with 12 rounds and write it into the password column of the users table for your email. With this Compose setup, run psql inside the db container as user wikijs against database wiki.

Pick the machine for the wiki

Every dedicated line, its memory, disks, port and cities, with prices read live from the catalogue.

See the machines

Related on Server Room

Other use cases