本文へ移動
購読
GUIDE#saas更新 2026.09.25

Wiki.jsで社内Wikiを立ち上げる:Docker導入・権限設計・バックアップの実践

Wiki.js 2.5をPostgreSQL 16とDocker Composeで構築し、グループとページルールの権限設計、日本語検索の実測、pg_dumpとGitのバックアップまで検証した手順です。

level
中級
time
約60分
stack
Docker Compose / Wiki.js 2.5.315(requarks/wiki:2) / PostgreSQL 16(postgres:16-alpine) / nginx
author
@ryoma_t
// 前提条件
Docker と docker compose が使えるLinuxサーバー / TLSを終端するリバースプロキシ(nginxなど)とDNS
Wiki.js 2.5で作成した社内手順書ページ「パスワードリセット手順」。左に手順書フォルダーのページ一覧、中央に目次・タグ・最終更新者、右に本文と変更履歴が表示されている
画像:nagoya.dev(実際の画面・実行結果をもとに作成)

Wiki.js(ウィキジェイエス)とは、Node.jsで動くオープンソースの社内Wikiエンジンで、Markdownで書いたページをグループごとの権限で公開できるソフトウェアです。NotionやConfluenceのようなSaaSではなく、自社のサーバーやコンテナに置いて運用します。ページ本文だけでなく添付ファイル・ユーザー・設定までがデータベースに入る構造で、閲覧と編集の権限は「グループ」に「ページルール(パスの前方一致など)」を組み合わせて決めます。

ここでは執筆時点でDockerの requarks/wiki:2 タグが指す Wiki.js 2.5.315 をPostgreSQL 16と組み合わせてDocker Composeで起動し、初回セットアップ、全社員・情シス・管理者の3層の権限設計、2種類のデータベース検索エンジンで日本語がどこまで見つかるかの実測、pg_dumpとGitストレージによるバックアップ、リバースプロキシ配下での注意点までを、検証環境で確かめた順に示します。読み終えると「社員はログインして手順書を読める、編集できるのは情シスだけ、ネットワーク設計は管理者しか開けない」社内Wikiが手元にできます。エラーの文言とコマンドの出力は、検証環境で実際に出たものです。

この記事でわかること

  • Wiki.jsを選ぶ条件と、Microsoft 365付属ツール・Notion・Confluenceとの使い分け
  • PostgreSQL 16と組み合わせたcompose.yamlの各行の意味と、初回ウィザードで決める4項目
  • 「全社員は手順書を閲覧のみ、情シスは編集、networkは管理者だけ」を実現するグループとページルール、その判定の仕組み
  • Database - BasicとDatabase - PostgreSQLで日本語の検索語が何件見つかったかの実測
  • pg_dumpの取得と復元テスト、Gitストレージの設定、リバースプロキシと更新の手順、検証中に出た実際のエラー

Wiki.jsとは:社内Wikiの選択肢の中での位置づけ

Wiki.jsはAGPL v3で公開されているソフトウェアで、本体はNode.js、データはPostgreSQL・MySQL・MariaDB・SQLite・SQL Serverのいずれかに保存します(2系の話で、公式Dockerイメージは DB_TYPE などの環境変数で接続先を渡す作りです)。ページはMarkdownかビジュアルエディターで書き、履歴・コメント・添付ファイル・検索を標準で備え、認証はローカルのほかOIDCやSAMLなど複数のモジュールから選べます(起動ログには Loaded 21 authentication strategies と出ます)。ライセンス費用はかからず、かかるのはサーバーと運用する人の時間です。

「社内Wiki」の候補として比べる相手は、Microsoft 365やGoogle Workspaceに付属するツール(SharePoint・OneNote・Googleサイト)、Notion、Confluence Cloudあたりです。違いは費用の考え方、データの置き場所、権限の切り方に出ます。

観点M365/Workspace付属ツールNotion・Confluence CloudWiki.js(セルフホスト)
データの置き場所ベンダーのクラウドベンダーのクラウド自社サーバーまたは自社管理のクラウド
費用の考え方既存契約に含まれることが多いユーザー数に比例ソフトは無償、サーバーと運用の人手が費用
権限の単位サイト・ライブラリ・ノートブックワークスペース・スペース・ページグループ×パスの前方一致・完全一致・正規表現・タグ
履歴と監査バージョン履歴ページ履歴DBのページ履歴+Gitのコミット履歴(作者付き)
運用の負担ほぼなしほぼなしOS・DB・アプリの更新、バックアップ、監視を自社で
日本語の全文検索ありあり執筆時点で制約が大きい(後述の実測)

判断の目安ははっきりしています。Microsoft 365やGoogle Workspaceを契約済みで、手順書を社外のクラウドに置くことに制約がなければ、付属ツールで始めるほうが早く、運用の手間もありません。Wiki.jsが候補になるのは、ネットワーク構成や機器の管理情報のように社外に出せない文書を扱う、閲覧者は数百人で編集者は数人という構成でユーザー数課金が重い、Markdownで書いてGitに履歴を残したい、Dockerの面倒を見られる担当者が少なくとも1人いる、という条件が重なるときです。逆に、編集者の中心が非エンジニアで同時編集やコメントでの合意形成が必須なら、SaaSのほうが定着します。ヘルプデスクの問い合わせを減らす目的なら、ツール選びより先にナレッジ運用の設計を決めておくと、Wikiの構造がぶれません。

構成を決める:PostgreSQL 16とcompose.yaml

検証は requarks/wiki:2(2.5.315)と postgres:16-alpine の2コンテナ構成です。イメージの大きさは docker images で1.03GBと420MB、起動直後のメモリはWiki.js本体が約89MiB、PostgreSQLが約28MiBでした。ページを4つ作り、PostgreSQL検索とGitストレージを有効にした状態で本体が約153MiB、DBが約50MiB(docker stats)なので、小さなチームなら2GBメモリの仮想マシンで足ります。メモリより先に気にするのはディスクで、イメージだけで1.5GB近く使うため、更新のたびに古いイメージを消す運用が要ります。

作業ディレクトリを作り、DBパスワードを .env に生成してからcompose.yamlを置きます。

terminal
mkdir -p ~/wiki && cd ~/wiki
printf 'DB_PASS=%s\n' "$(openssl rand -hex 16)" > .env
chmod 600 .env
compose.yaml
services:
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: wiki
      POSTGRES_USER: wikijs
      POSTGRES_PASSWORD: ${DB_PASS}
    volumes:
      - db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U wikijs -d wiki"]
      interval: 5s
      timeout: 5s
      retries: 20

  wiki:
    image: requarks/wiki:2
    restart: unless-stopped
    ports:
      - "127.0.0.1:3000:3000"
    environment:
      DB_TYPE: postgres
      DB_HOST: db
      DB_PORT: 5432
      DB_USER: wikijs
      DB_PASS: ${DB_PASS}
      DB_NAME: wiki
    volumes:
      - wiki-data:/wiki/data
    depends_on:
      db:
        condition: service_healthy

volumes:
  db:
  wiki-data:

各行の意味と、あえて使わなかった設定は次のとおりです。

  • healthcheck と condition: service_healthy:PostgreSQLが接続を受け付けてからWiki.jsを起動します。これがないと、つまずきポイントで示す ECONNREFUSED のリトライが初回起動で出ます。
  • ports: "127.0.0.1:3000:3000":ホストのループバックにだけ公開し、外からはリバースプロキシ経由でしか届かないようにします。"3000:3000" と書くとすべてのインターフェースに開き、ファイアウォールの状態によっては社内ネットワークから認証なしのセットアップ画面に到達できてしまいます。
  • DB_PASS: ${DB_PASS}:値は .env から読み込みます。compose.yamlに直接書くと、Gitに入れた瞬間にパスワードが履歴に残ります。docker inspect で環境変数は見えるので、それも避けたい場合はイメージが対応している DB_PASS_FILE(ファイルパスを渡す方式、Docker secrets向け)に切り替えます。
  • wiki-data:/wiki/data:描画キャッシュ、アップロードの一時ファイル、Gitストレージのローカルリポジトリが入る領域です。ページや添付ファイルの本体は入りません(バックアップの節で確かめます)。
  • 使わなかったもの:SSL_ACTIVE と LETSENCRYPT_DOMAIN(本体にTLSを終端させる機能)は既存のnginxで終端するので無効のまま、HA_ACTIVE も単一ノードでは不要です。
terminal
docker compose up -d
docker compose logs -f wiki

初回起動のログには、= Wiki.js 2.5.315 = のバナー、Database Connection Successful [ OK ]、続いて DB Configuration is empty or incomplete. Switching to Setup mode... と Browse to http://YOUR-SERVER-IP:3000/ to complete setup! が出ます。イメージ内の /wiki/config.yml は $(DB_HOST) のような置き換え記法で書かれていて、環境変数がそのまま接続先になります。

初回セットアップ:管理者アカウントとSite URL

ウィザードは社内ネットワークに公開する前に済ませます。手元のPCで ssh -L 3000:127.0.0.1:3000 <サーバー> としてポートを転送し、ブラウザーで http://127.0.0.1:3000 を開くとウィザードが表示されます。入力するのは管理者のメールアドレス、パスワード(確認入力あり)、Site URL、テレメトリー送信の可否の4点だけです。Site URLには利用者がブラウザーに打つURL(例:https://wiki.example.co.jp)を入れます。この値はメール通知のリンク、検索エンジン向けのメタタグ、OIDCなど外部認証のコールバックURLを組み立てるときに使われ、あとから管理画面の全般設定で変えられます。テレメトリーは切っておきます。

「Install」を押すと、画面は内部で POST /finalize を呼び、本体は設定をDBに保存してから自分で再起動します。検証ではこのAPIに同じ4項目をJSONで送り、約20秒後にログイン画面が返りました。再起動後のログには HTTP Server: [ RUNNING ] に続いて認証・検索・ストレージのモジュールを読み込む行が並びました。同時に Syncing locales with Graph endpoint... と Fetching latest updates from Graph endpoint... も出ます。テレメトリーを切っても、言語ファイルの同期と更新情報の取得のために開発元のエンドポイント(graph.requarks.io)へ通信する動作は残ります。

UIを日本語にするのは管理画面の「言語」です。ここには順番があり、日本語を先にダウンロードしてから、サイトの言語に日本語を選んで適用します。検証でAPIから順番を逆にして適用だけ先に実行したところ、Cannot read properties of undefined (reading 'isRTL') というエラーになり、設定値だけが ja に切り替わって文字列が入っていない中途半端な状態になりました。言語ファイルはこの時点で開発元から取得するので、社内から外に出られないサーバーではプロキシの許可先に加えておきます。なお執筆時点の2.5.315では、閲覧側とナビゲーションは日本語になりますが、管理画面のタブ(SETTINGS・PERMISSIONS・PAGE RULESなど)や説明文には英語のまま残る箇所があります。

セットアップ画面は認証なしで誰でも管理者を作れる画面です。compose.yamlの ports をループバックに限定し、ウィザードを終えるまではリバースプロキシの公開設定を入れない順番にしてください。検証環境でも、ウィザード完了まで127.0.0.1以外からは到達できない構成にしています。

社内Wikiの権限設計:グループとページルール

Wiki.jsの権限判定は2段階です。まずグループの「PERMISSIONS」で持っている全体権限(read:pages、write:pages、manage:pages など)に、要求された操作が含まれているか。含まれていれば次に「PAGE RULES」で、対象ページのパスに対して許可(ALLOW)か拒否(DENY)かを決めます。全体権限にない操作はページルールで許可しても通らず、逆に全体権限があってもページルールが空なら何も開けません。PAGE RULESタブにも、Permissionsタブで全体権限を有効にしないとページルールは効かない、という趣旨の注意書きが英語で出ます。Administratorsグループ(manage:system を持つ)はこの判定を素通りします。

ルールが複数当たったときの優先順位は、ソースコードと管理画面の説明で一致していました。パスが長い(より具体的な)ルールが勝ち、同じ長さなら「前方一致 < 後方一致 < 正規表現 < タグ一致 < 完全一致」の順に強く、同じ長さ・同じ種類ならDENYがALLOWに勝ちます。つまり「全体を許可して network 配下だけ拒否」は、拒否ルールのパスのほうが長いので必ず拒否が勝ちます。ロケールは「Any Locale」のままにします。日本語だけのWikiで絞る意味はなく、絞るとロケール指定を間違えたページが誰にも見えなくなります。

グループ全体権限(PERMISSIONS)ページルール(PAGE RULES)想定メンバー
全社員read:pages、read:assets、read:comments許可:完全一致 home/許可:前方一致 手順書全員
情シス全社員の権限+write:pages、manage:pages、delete:pages、write:assets、manage:assets、write:comments、manage:comments、read:source、read:history許可:前方一致(パス空=すべて)/拒否:前方一致 network情報システム部
Administrators(管理者)manage:system不要(判定を素通り)情シスの責任者1〜2人
Guests(未ログイン)なしなし—

作り方は管理画面の「グループ」から「新規グループ」で名前を付け、PERMISSIONSタブで権限にチェック、PAGE RULESタブの「ADD RULE」で行を足します。パスは先頭の / なしで入力します(入力欄の左に固定表示されます)。ユーザーは「ユーザー」の「新規ユーザー」でメールアドレス・氏名・パスワードを登録し、ローカル認証(local)でグループを割り当てます。Entra IDやKeycloakのOIDCに切り替えるのはWikiの構造が固まってからで十分で、認証モジュールの設定にはログインしたユーザーを自動で入れるグループの項目があるので、そこに「全社員」を指定します。もう1つ、ユーザーのタイムゾーンは初期値が America/New_York で、検証で作ったユーザーは全員この値でした。そのままだとページの「最終更新」が日本時間より13時間(米国の夏時間が終わると14時間)遅れて表示されるので、ユーザー作成後に管理画面のユーザー編集(またはプロフィール)で Asia/Tokyo に変えます。

一番の落とし穴はGuestsです。初期状態のGuestsグループは read:pages・read:assets・read:comments を持ち、ページルールも「前方一致、パス空=すべて許可」が入っています。検証環境で認証情報なしに /手順書/パスワードリセット を取得すると、HTTP 200で本文がそのまま返りました。社内向けWikiでは、ページを1つ書く前にGuestsの権限とルールをすべて外します。外したあとは未ログインで / を開くと 302 で /login へ、ページを直接指定すると 403、添付ファイルも 403 になりました。

Guestsを閉じるまで、社内ネットワークに到達できる人は誰でもログインなしで全ページを読めます。内線番号程度なら困らなくても、networkの設計ページを先に書いてしまうと取り返しがつきません。順番は「Guestsを閉じる → グループを作る → ページを書く」です。

設計どおりに効いているか、検証環境で山田(全社員)・鈴木(情シス)の2人と未ログインの3通りで各URLのHTTPステータスを確かめた結果です。編集画面は /e/ja/<パス> で、新規作成は存在しないパスの編集画面を開いて確認しています。

URL山田(全社員)鈴木(情シス)未ログイン
/(home)200200302(/loginへ)
/手順書/パスワードリセット200200403
/network/vlan設計403403403
/e/ja/手順書/パスワードリセット(編集)403200—
/e/ja/手順書/新しいページ(新規作成)403200—
/e/ja/network/vlan設計(編集)403403—

サイドバーの「参照」で出るツリーも同じ判定を通ります。ツリーを返すAPI(GraphQLの pages.tree)で鈴木としてルート階層を取得すると「手順書」フォルダーと home だけが返り、管理者では network フォルダーも返りました。拒否ルールは本文だけでなくナビゲーションにも効くので、存在を知らない人がツリーからたどり着くことはありません。

Markdownエディターの運用ルール

権限設計がパスの前方一致に依存するので、フォルダー構造を先に決めて後から動かさないことが運用の前提です。検証では 手順書/パスワードリセット のような日本語パスも、ページ表示・ページルール・Gitへの書き出しまで問題なく通りました。ただしURLはパーセントエンコードされ(/%E6%89%8B%E9%A0%86...)、チャットに貼ると長くて読みにくく、Gitのファイル名も git ls-tree ではエスケープ表示になります。Mattermostなどからリンクを多用するなら、第1階層は howto・network・it-internal のような英字にし、タイトルを日本語にする折衷が扱いやすく、ページルールの前方一致も英字で書けます。

次に「説明(description)」欄を空にしないルールです。後述のとおりDatabase - Basic検索はタイトル・説明・パスしか見ないので、本文に「稟議」と書いても説明に書かなければ見つかりません。手順書の説明欄には「対象者・対象システム・主な検索語」を1行で書く、と決めておきます。タグは「手順書」「アカウント」「ネットワーク」のように語彙を固定し、ページルールの「タグ一致」やタグ一覧からの絞り込みに使います。

エディターはページ作成時に選び、情シスが書く手順書はMarkdownに統一します。Gitに書き出したときに差分が読めるからです。書き出されたファイルには次のフロントマターが付き、タイトル・説明・タグ・更新日時がそのまま残ります(検証環境で書き出された実物です)。

手順書/パスワードリセット.md(先頭部分)
---
title: パスワードリセット手順
description: Microsoft 365 アカウントのパスワードを自分で、または情シス経由でリセットする手順
published: true
date: 2026-09-24T06:38:14.292Z
tags: 手順書, アカウント
editor: markdown
dateCreated: 2026-09-24T06:31:52.741Z
---

手順書の型も決めておきます。検証で使ったのは「対象」「手順」「うまくいかないとき(症状と確認すること)」「変更履歴(稟議番号)」の4見出しです。変更履歴に稟議番号を書く運用にすると、Gitのコミットとページ履歴の両方に残るので、監査で「いつ誰がなぜ変えたか」を追うときに探す場所が1つで済みます。

手順書/vpn接続(Markdownの型・例)
# VPN接続手順(在宅勤務用)

対象: 貸与PCを使う全社員

## 手順

1. 貸与PCを社外ネットワークにつなぎます。
2. VPNクライアントでプロファイル `office-vpn` を選びます。
3. 社員番号とパスワードでサインインし、認証アプリの通知を承認します。

## うまくいかないとき

| 症状 | 確認すること |
| --- | --- |
| 「サーバーに到達できません」 | 自宅ルーターの再起動、テザリングでの再試行 |
| 認証に失敗する | パスワードの期限切れ([パスワードリセット手順](/手順書/パスワードリセット)) |

## 変更履歴

- 2026-09: プロファイル名を office-vpn に統一(稟議 No. 例-0042)

検索エンジンの選択:日本語で何が見つかるか

管理画面の「検索エンジン」には、追加のサーバーなしで使える「Database - Basic」と「Database - PostgreSQL」のほか、Elasticsearch、Algolia、AWS CloudSearch、Azure Searchなどが並びます。2つのデータベース方式の仕組みをソースコードで確かめると、Basicはタイトル・説明・パスに対する部分一致(PostgreSQLでは ILIKE '%語%')で、本文は見ません。PostgreSQL方式はタイトル・説明・本文を to_tsvector で分かち書きして索引を作り、辞書は simple・english・german などから選びます。選択肢に日本語はなく、PostgreSQL標準のパーサーは空白や記号でしか語を切りません。

これが日本語でどう効くかは、索引の中身を見ると分かります。検証環境のPostgreSQLで手順書のタイトルを分かち書きさせた結果です。

psql
wiki=# select to_tsvector('simple', 'パスワードリセット手順 Microsoft 365 アカウントの VPN 接続');
 '365':3 'microsoft':2 'vpn':5 'アカウントの':4 'パスワードリセット手順':1 '接続':6

「パスワードリセット手順」がひとかたまりの語になっているので、「パスワード」で検索しても索引には当たりません。検証環境の4ページ(home、手順書2ページ、network1ページ)で同じ検索語を両方のエンジンで試した結果が次の表です。管理者で実行し、PostgreSQL方式は辞書を simple にしてインデックスを再構築してから計測しました。

検索語語がある場所Database - BasicDatabase - PostgreSQL(simple)
パスワードタイトル・パス1件1件(パスの部分一致で救済)
在宅タイトル「在宅勤務用」1件0件(「在宅勤務用」が1語になる)
手順タイトル・説明・パス3件2件(説明だけに含むhomeは外れる)
VPN/vpnタイトル・パス1件1件
稟議本文のみ0件0件(「の追加は稟議番号を本ページの履歴コメントに残す」が1語になる)
Microsoft説明と本文1件(説明に含むページのみ)2件(本文が対象になる)
パスワードリセット手順タイトル、他ページのリンク文字列1件3件(かたまりごと一致)

結論は「執筆時点、どちらの方式でも日本語の全文検索は成立しない」です。Basicは日本語の部分一致が効く代わりに本文を見ず、PostgreSQL方式は本文を見る代わりに、日本語は空白や記号で区切られた「かたまり」単位でしか一致しません。英単語・型番・コマンド名が本文に多い情シスWikiならPostgreSQL方式で拾える範囲が広がりますが、日本語の手順書が中心なら Database - Basic を選び、説明欄に検索語を書く運用のほうが利用者の体感は良くなります。形態素解析を含む日本語検索が必須なら、Elasticsearch/OpenSearchに日本語アナライザーを入れて外部エンジンとしてつなぐ構成になります。執筆時点で未検証で、Elasticsearch本体を動かす分のメモリとディスクを別に確保する必要があります。

細かい挙動として、検索結果の件数(totalHits)は権限で除外したページも数えます。全社員の山田で「vlan」を検索すると件数は1、結果は0件でした。ページの存在だけは推測できるので、管理者専用ページのタイトルやパスに秘匿情報を入れない運用が要ります。

バックアップと復元:pg_dumpとGitストレージ

バックアップ設計の前に、何がどこに保存されるかを確かめました。検証で64×32ピクセルのPNG(135バイト)をアップロードすると、PostgreSQLの assetData テーブルに bytea として135バイトの行が入り、/wiki/data 側には描画キャッシュ(cache/*.bin)と、アップロード中に使う一時ファイル(uploads/)しか増えませんでした。ページ本文・履歴・ユーザー・グループ・設定も同じDBです。つまり pg_dump 1本で内容が丸ごと取れます。/wiki/data はキャッシュと一時ファイルなので失っても本文は消えず、Gitストレージのローカルリポジトリも、起動時の初期化でリモートから取り直す作りです(ソース上の動作で、ボリュームを消しての確認はしていません)。

データ保存場所バックアップ手段
ページ本文・履歴・タグPostgreSQL(pages、pageHistory など)pg_dump
添付ファイルPostgreSQL(assets、assetData)pg_dump
ユーザー・グループ・設定PostgreSQL(users、groups、settings など)pg_dump
描画キャッシュ・一時ファイルボリューム wiki-data不要(再生成される)
Gitのローカルリポジトリボリューム wiki-data の repo/不要(リモートに同じ内容がある)
DBパスワード.env秘密情報として別に保管
backup.sh
#!/bin/sh
# compose.yaml と同じディレクトリに置き、毎日 02:00 に cron で実行する例。14日分を残す
set -eu
cd "$(dirname "$0")"
mkdir -p backup
docker compose exec -T db pg_dump -U wikijs -d wiki -Fc > "backup/wiki-$(date +%F).dump"
find backup -name 'wiki-*.dump' -mtime +14 -delete

検証環境の4ページ・添付1件のダンプは約110KB(DB全体で9.4MB)でした。-Fc(カスタム形式)にしておくと pg_restore でテーブル単位の復元や所有者の読み替えができます。復元できることを確かめないバックアップは意味がないので、同じPostgreSQLの中に別名のDBを作って戻し、件数を見ました。

terminal
docker compose exec -T db createdb -U wikijs wiki_restore
docker compose exec -T db pg_restore -U wikijs -d wiki_restore --no-owner < backup/wiki-2026-09-24.dump
docker compose exec -T db psql -U wikijs -d wiki_restore -Atc 'select count(*) from pages; select filename, length(data) from assets join "assetData" on assets.id = "assetData".id;'
# 4
# vpn-client.png|135
docker compose exec -T db dropdb -U wikijs wiki_restore

本番のDBを丸ごと戻すときは、Wiki.jsを止めてから上書きします。検証環境で下の3行を実行したところ、pg_restore は終了コード0で終わり、起動後にページ4件・添付1件がそのまま読め、発行済みのログイン(JWT)も使えました。JWTの署名に使う鍵ペアも settings テーブルの certs としてDBに入っているためです。裏返すと、ダンプを手に入れた人はこの秘密鍵で任意のユーザーのトークンを作れるので、バックアップファイルは情シスしか読めない場所に置き、社外に出すなら暗号化します。PostgreSQL検索を使っているなら、起動後に管理画面の検索エンジンで「インデックスの再構築」を実行しておきます。

terminal
docker compose stop wiki
docker compose exec -T db pg_restore -U wikijs -d wiki --clean --if-exists < backup/wiki-2026-09-24.dump
docker compose start wiki

ボリュームまで含めて別ホストへ移すなら、docker volume ls で名前(ディレクトリ名が接頭辞)を確かめたうえで、一時コンテナで固めます。記事の構成なら docker run --rm -v wiki_wiki-data:/data:ro -v "$PWD/backup":/backup alpine:3 tar czf /backup/wiki-data.tgz -C /data . で、検証環境(ボリューム名は wikijs-self-hosted-wiki_wiki-data)では31KBでした。

Gitストレージモジュールは、ページと添付ファイルをMarkdown+ファイルとしてGitリポジトリに書き出す(あるいは取り込む)仕組みで、pg_dumpの代わりではなく「人が読める形の履歴」を足す位置づけです。管理画面の「ストレージ」でGitを選び、次の値を入れます。

  • 認証方式:SSH(デプロイキー。パスフレーズなしの秘密鍵をパスか内容で渡す)か、Basic(HTTPSのURLにユーザー名とパスワード/PAT)
  • リポジトリURI:[email protected]:it/wiki-backup.git のような形式。ブランチは既定で master
  • 同期方向:sync(双方向)、push(Wiki→Gitのみ)、pull(Git→Wikiのみ)。バックアップ目的なら push
  • 同期間隔:ISO 8601の期間表記で、既定は PT5M(5分)
  • 既定の作者名とメールアドレス:変更者を特定できないコミットに使う値。ローカルリポジトリの置き場は既定の ./data/repo(ボリューム内)のまま

検証では、Wiki.jsコンテナ内のボリュームに作った空のベアリポジトリ(git init --bare /wiki/data/backup.git)を相手に push モードで動作を確かめました。有効化した直後のステータスは Invalid branch! Make sure it exists on the remote first. のエラーでした。空のリポジトリには master ブランチがなく、初回の取得で見つからないためです。対処は同じ画面のアクションを2つ順に実行することです。「Add Untracked Changes」でDBの全ページと添付ファイルをローカルリポジトリに書き出して docs: add all untracked content というコミットを作り(この操作はコミットまでで、プッシュはしません)、続けて「Force Sync」でプッシュするとリモートに master ができます。

ここで終わりにすると、定期バックアップが動かないまま残ります。ソースを読むと、有効化時の初期化(init)に失敗したストレージには5分ごとの同期ジョブが登録されず、アクションを実行しても初期化はやり直されません。実際、アクションのあとにページを更新してもステータスはエラーのままでした。コンテナを再起動すると (STORAGE/GIT) Checking out branch master... のログとともにステータスが operational に変わり、以後はログに Syncing with storage target git: [ COMPLETED ] が5分ごとに並びました(検証環境の5時間で60回)。ソース上はストレージ設定を保存し直したときにも初期化が走るので、再起動できない時間帯はそちらでも足ります。

鈴木でページを更新して同期すると、コミットの作者はWikiのユーザー情報から 鈴木 花子 <[email protected]> になり、メッセージは docs: update 手順書/パスワードリセット でした。ベアリポジトリ側で確認したログです。

terminal(wikiコンテナ内)
$ git --git-dir=/wiki/data/backup.git log --format='%h %an <%ae> %s' master
ca157e1 鈴木 花子 <[email protected]> docs: update 手順書/パスワードリセット
8fbd55d Wiki.js <[email protected]> docs: add all untracked content
$ git --git-dir=/wiki/data/backup.git ls-tree -r --name-only master
home.md
"network/vlan\350\250\255\350\250\210.md"
vpn-client.png
"\346\211\213\351\240\206\346\233\270/vpn\346\216\245\347\266\232.md"
"\346\211\213\351\240\206\346\233\270/\343\203\221\343\202\271\343\203\257\343\203\274\343\203\211\343\203\252\343\202\273\343\203\203\343\203\210.md"

注意が2つあります。push モードは強制プッシュ(--force)でリモートをWikiの内容で上書きするので、Gitの側で直接編集してはいけません(Gitからも直したいなら sync にします)。もう1つは、リポジトリには network/ 配下の管理者専用ページも平文で入ることです。Wikiでどれだけ権限を絞っても、プッシュ先のリポジトリを全社員が読めれば意味がありません。

Gitリポジトリには、ページルールで隠したページも含めて全文がMarkdownで入ります。プッシュ先は情シス専用のプライベートリポジトリにし、デプロイキーはそのリポジトリだけに書き込める鍵にしてください。ローカルパスを相手にしたのは検証だけの構成で、本番ではSSHかHTTPSのリモートを使います。

リバースプロキシ配下の設定と更新手順

社員に使わせるときは、nginxなどでTLSを終端して3000番に渡します。設定の型はnginxのリバースプロキシ構築ガイドのとおりで、Wiki.js向けに意識するのは client_max_body_size と X-Forwarded-For の2点です。

/etc/nginx/conf.d/wiki.conf(抜粋)
server {
    listen 443 ssl;
    server_name wiki.example.co.jp;
    # ssl_certificate / ssl_certificate_key は省略
    client_max_body_size 20m;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        # クライアントが付けてきた X-Forwarded-For は捨て、接続元のIPで上書きする
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

X-Forwarded-For を渡すだけでは足りず、管理画面の「セキュリティ」にあるプロキシヘッダー(X-Forwarded-*)を信頼する設定を有効にする必要があります。理由はログインのレート制限です。ログインAPIは「同じIPから60秒に5回まで」に制限されていて、6回目は Too many requests, please try again in 58 seconds. を返します。数えるのは失敗だけではなく、正しいパスワードで6回続けてログインしても6回目は拒否されました。この「IP」は、プロキシヘッダーを信頼しない限りリバースプロキシのアドレスになるので、社員全員が1つのIPとして数えられます。始業直後の1分間に6人がログインすれば、6人目は正しいパスワードでも入れません。

信頼する設定にすると、今度はnginxが渡す X-Forwarded-For の中身が効いてきます。この設定はExpressの trust proxy を有効にするだけなので、ヘッダーに並んだアドレスの左端がクライアントとみなされます。nginxでよく使う $proxy_add_x_forwarded_for は、クライアントが送ってきた値の後ろに接続元を足すだけです。利用者が自分で X-Forwarded-For を付ければ左端を好きな値にでき、失敗を何回重ねても毎回別人として数えられます。上の設定で $remote_addr による上書きにしているのはこのためです。検証環境でnginxを前に置き、1台の端末から X-Forwarded-For を変えながら誤ったパスワードでログインした結果です。

プロキシヘッダーの信頼nginxのX-Forwarded-For結果(6回の失敗)
しない(初期値)$proxy_add_x_forwarded_for6回目が「Too many requests」。名乗るアドレスに関係なく、全員がnginxのIPとして数えられる
する$proxy_add_x_forwarded_for6回とも通常の「Invalid email / username or password.」。偽装したアドレスごとに数えられ、制限をすり抜ける
する$remote_addr(上書き)6回目が「Too many requests」。偽装した値は捨てられ、端末の実際のアドレスで数えられる

リバースプロキシ配下で「朝だけ何人かログインできない」と言われたら、まずこれを疑います。信頼する設定への切り替えは再起動なしで即時に効きました。反対に、リバースプロキシを置かずに3000番を直接公開している構成でこの設定を有効にしてはいけません。クライアントが名乗った X-Forwarded-For がそのまま使われ、表の2行目と同じ状態になります。nginxの前にさらにロードバランサーがある構成では、nginxのrealipモジュールで $remote_addr を実際の端末のアドレスにしてから渡します(この構成は未検証です)。なお X-Forwarded-Proto は、ソースを追う限り本体のTLS機能を有効にしたときのリダイレクト判定にしか使われません。HTTPSで公開したときに直すべきは全般設定のSite URLで、http:// のままだとメールや外部認証のリンクが http:// で組み立てられます。アップロードは本体側の上限(初期値5MB、管理画面のセキュリティで変更)と client_max_body_size の小さいほうで止まります。

更新は、pg_dumpを取ってから docker compose pull と docker compose up -d です。DBの構造変更は起動時に自動で流れ、検証環境の migrations テーブルには16件(最新は 2.5.128.js)が記録されていました。変更管理の対象にするならイメージタグを requarks/wiki:2.5.315 のように固定し、変更申請のタイミングで明示的に上げます(執筆時点でこのタグはDocker Hubに存在します)。:2 のままなら2系の範囲で最新に追従しますが、次のメジャーバージョンには切り替わらないので、3系への移行は公式の移行手順を確認してから別の計画として進めます。

terminal
docker compose exec -T db pg_dump -U wikijs -d wiki -Fc > backup/before-upgrade.dump
docker compose pull
docker compose up -d
docker compose logs wiki | grep -E 'Wiki.js 2|HTTP Server: '
docker image prune -f   # 古いイメージを消す(他のスタックと共有していないサーバーで)

つまずきポイント

起動直後に Database Connection Error: ECONNREFUSED が続く

healthcheckなしの depends_on で起動した初回、Wiki.jsのログに Database Connection Error: ECONNREFUSED 172.18.0.2:5432 と Will retry in 3 seconds... [Attempt 1 of 10] が出ました。PostgreSQLの初期化が終わる前に接続しに行っただけで、検証では2回目で Database Connection Successful [ OK ] になっています。ただしソース上、リトライは3秒間隔で10回までで、それでもつながらなければ接続処理はエラーで打ち切られます(検証ではそこまで遅い状況は再現していません)。記事のcompose.yaml(healthcheckと condition: service_healthy)に切り替えて起動し直したときは、この行は出ませんでした。.env の DB_PASS を後から変えても既存のDBボリュームのパスワードは変わらない(PostgreSQLの初期化時にしか反映されない)点にも注意します。

言語を日本語にしたら Cannot read properties of undefined (reading 'isRTL')

言語ファイルをダウンロードする前にサイトの言語を日本語へ適用すると出ます。管理画面では順番どおりに操作すれば踏みませんが、APIやスクリプトで自動化するときは downloadLocale → updateLocale の順を守ります。出てしまった場合も、ダウンロードしてから適用し直せば直り、検証ではその後 en と ja の両方が installed になりました。外部への通信が遮断されている環境では、ダウンロード自体が失敗します。

リバースプロキシ配下で Too many requests が全員に出る

プロキシヘッダーを信頼しない状態では全社員が同じIPとして数えられ、60秒間に5回ログインがあると(成功も数えます)、6回目以降は誰でも拒否されます。管理画面の「セキュリティ」でヘッダーを信頼する設定に変え、nginx側では X-Forwarded-For を $remote_addr で上書きして渡します。切り替えは再起動なしで効きました。$proxy_add_x_forwarded_for のままにすると、今度はヘッダーを偽装した利用者が制限をすり抜けられます。

Gitストレージが Invalid branch! Make sure it exists on the remote first.

空のリモートリポジトリを指定したときの初回エラーです。「Add Untracked Changes」でローカルにコミットを作り、「Force Sync」でプッシュするとリモートにブランチができます。そのあと必ずコンテナを再起動するか、ストレージ設定を保存し直してください。初期化に失敗した状態のままでは5分ごとの同期ジョブが登録されず、ステータスもエラーのまま変わりません。再起動後にログへ Syncing with storage target git: [ COMPLETED ] が出れば定期同期が動いています。ブランチ名の既定は master なので、main で作ったリポジトリを使うときは設定のブランチ欄も合わせます。

動作確認

起動から権限まで、コマンドで確かめる手順です。コンテナ名の接頭辞はディレクトリ名で、記事の手順どおりなら wiki-db-1・wiki-wiki-1、ポートは3000番です。下の出力は検証環境のもので、プロジェクト名を wikijs-self-hosted-wiki、ホスト側ポートを13000番にしていたため、その部分だけ表示が異なります。

terminal
$ docker compose ps --format 'table {{.Name}}\t{{.Status}}\t{{.Ports}}'
NAME                             STATUS                 PORTS
wikijs-self-hosted-wiki-db-1     Up 5 hours (healthy)   5432/tcp
wikijs-self-hosted-wiki-wiki-1   Up 3 minutes           3443/tcp, 127.0.0.1:13000->3000/tcp

$ docker compose logs wiki | grep -E 'Wiki.js 2|Database Connection|HTTP Server: '
wiki-1  | 2026-09-24T11:44:02.503Z [MASTER] info: = Wiki.js 2.5.315 =====================
wiki-1  | 2026-09-24T11:44:03.402Z [MASTER] info: Database Connection Successful [ OK ]
wiki-1  | 2026-09-24T11:44:07.514Z [MASTER] info: HTTP Server: [ RUNNING ]

$ curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://127.0.0.1:3000/
302 http://127.0.0.1:13000/login

$ docker compose exec -T db psql -U wikijs -d wiki -Atc 'select path, title from pages order by id'
home|情シスWiki トップ
手順書/パスワードリセット|パスワードリセット手順
手順書/vpn接続|VPN接続手順(在宅勤務用)
network/vlan設計|VLAN設計(社内ネットワーク)

3つ目の 302 はGuestsを閉じた状態の期待値で、200 が返るなら未ログインで読める状態です。権限はブラウザーで山田・鈴木の2アカウントでログインし、/network/vlan設計 が「Unauthorized」になること、鈴木でだけ右下に鉛筆(編集)のボタンが出ることを見ます。バックアップは前節の pg_restore の件数確認まで含めて、初回に一度は通しておきます。

よくある質問

社内Wikiとは何ですか?

社内の手順書・設定情報・ルールを、決まった構造とURLで蓄積し、権限のある人が検索して読める仕組みです。チャットやメールとの違いは「最新版が1か所にあり、履歴が残る」こと、ファイルサーバーとの違いは「ブラウザーで読め、ページ同士をリンクで参照できる」ことです。この記事の構成では、ページ履歴とGitのコミットの両方に変更が残ります。

社内Wikiを無料で作るには?

ソフトウェアの費用だけなら、Wiki.jsのようなオープンソースを自社サーバーやコンテナで動かせば0円です。ただしサーバー(この記事の構成で2GBメモリの仮想マシン1台)、TLS証明書とリバースプロキシ、バックアップ先、更新と監視の人手は要ります。Microsoft 365やGoogle Workspaceを契約済みなら付属ツールに追加費用も運用もかからないので、先にそれで足りるかを確かめます。

社内Wikiが失敗する原因は?

技術より運用で失敗します。書く場所(フォルダー構造)を決めずに始めて重複ページが増える、検索で見つからずチャットで聞くほうが早くなる、更新責任者がいなくて古い手順が残る、が典型です。Wiki.jsなら、パス設計とページルールを最初に固定し、説明欄に検索語を書く運用を徹底し、Gitの履歴で更新の有無を見える化することで、それぞれに手を打てます。

NotionやConfluenceではなくWiki.jsを選ぶ理由は?

データを社外に置けない、閲覧者が多く編集者が少なくてユーザー数課金が合わない、Markdownで書いてGitに履歴を残したい、という条件に当てはまるときです。逆に、非エンジニアの編集者が多く、同時編集やコメントでの合意形成が中心ならSaaSのほうが定着します。日本語の全文検索は執筆時点のWiki.js単体では弱いので、それが決め手なら候補から外れます。

まとめ

Wiki.js 2.5.315をPostgreSQL 16と組み合わせたcompose.yamlで起動し、ウィザードで管理者とSite URLを決め、Guestsを閉じてから全社員・情シス・管理者のグループとパス前方一致のページルールを作れば、社内Wikiの骨格ができます。検索は日本語の全文検索が効かない前提でDatabase - Basicと説明欄の運用を選び、バックアップはpg_dumpを毎日、Gitストレージは人が読める履歴として足します。リバースプロキシ配下では、nginxで X-Forwarded-For を $remote_addr で上書きしたうえで、プロキシヘッダーを信頼する設定にします。次の一歩は、手順書のフォルダー構造と説明欄のルールを1枚にまとめて情シス内で合意し、その合意文をWikiの最初のページとして home に書くことです。

次に読む

$ subscribe --weekly
毎週月曜、企業ITとホームラボの要点を5分で。