Article

0円で構築する社内情報共有システム

高田晃太郎
0円で構築する社内情報共有システム

統計によると、ナレッジワーカーは勤務時間の約19%を情報の検索と収集に費やしています¹。これは週5日勤務で1日あたり約90分に相当し、国内アンケートでも「社内情報の検索に1日1時間以上を費やす」傾向が報告されています⁸。さらに業務アプリの増加は分断を加速させ、公開レポートでは大規模組織で管理対象アプリが平均329種に及ぶとされています²。検索コストと分断を同時に解くには、散在する資料を安全に集約し、高速に横断検索できる一貫した基盤が要になります。

そこで本稿では、CTOやエンジニアリングマネージャーが即日着手できる設計として、クラウドの常時無料枠や手元の既存サーバーを活用し、追加コスト0円から始められる社内情報共有システムの構成例を提示します。汎用の無料枠(Always Free/Free Tier)を前提に、SSO(Single Sign-On:一度の認証で複数サービスを利用可能)で統合認証しつつ、Wiki・開発ドキュメント・全文検索・ファイル保管をひとつの反復可能なスタックにまとめます。目安として100ユーザー・同時接続20・1vCPU/2GB RAMの範囲で快適に動作することを狙った構成を示し、実装コード、運用KPI、チューニングの具体策を開示します。なお、各クラウドの無料枠は提供条件が変動し得る点、利用量や外部通信に応じて従量課金が発生する場合がある点は事前に確認してください。

0円の前提とアーキテクチャの全体像

追加費用を抑えるコアは、既存の社内サーバーかクラウドの常時無料枠を使い、オープンソースのコンポーネントを統合することにあります。例えばAWSのFree TierやOCIのAlways Freeでは、1 vCPU/約1GiB級のVM(例:t系マイクロ)や、Armベースで最大4 OCPU/24GBメモリといった構成が代表的に提供されています³⁴。これに収まるようコンポーネントを選び、CPUとメモリを食いがちな要素を避けるのがポイントです。機能は四層に分けると整理しやすく、認証・公開・検索・保管をそれぞれ独立に考えます。

認証はKeycloakによるOIDC/SAML(どちらもシングルサインオンのための業界標準プロトコル)で統合します。公開面は閲覧と編集の体験がよいWiki.jsを中心に据え、設計仕様やADR(Architecture Decision Record:設計判断の記録)などの開発文書はDocusaurusの静的サイトで扱うと、差分管理とレビューがしやすくなります。検索は軽量で速いMeilisearchを使い、Wikiと静的コンテンツの両方からインデックスを生成します。ファイル保管はMinIOのS3互換ストレージを用い、画像や大きめの添付を集約します。これらをNginx(リバースプロキシ)とoauth2-proxyの組み合わせで一つのFQDN配下にまとめ、Let’s EncryptでTLS(通信の暗号化)を自動化します⁶。

コンポーネント選定の理由と具体的数値

メモリ消費の目安を最初に押さえておくと設計が安定します。Nginxは常駐で十数MBから数十MBに収まり、Meilisearchは小〜中規模のインデックスで約512MBを見込むと安全です。Wiki.jsはアクティブ編集が少なければ300〜500MB程度に収まることが多く、Docusaurusはビルド時のみメモリを消費しますが公開は静的ファイルなので軽量です。KeycloakはJavaベースのため余裕をもって1GB前後を用意すると良く⁵、全体として2GBメモリが現実的な下限になります。CPUについてはNginxの静的配信は低負荷で、同時接続20・平均レスポンス200msという前提であれば1 vCPUでも詰まりにくい構成が狙えます。

スループットの設計は利用行動から逆算します。100ユーザーで月次PVが3万、平日日中の集中帯で全体の40%が発生すると仮定すると、ピーク1時間あたり約600PV、1分あたり10PVの平均になります。検索はアクセスの2割程度としてピーク分間2件、インデックス更新は非同期にバッチ処理。こうした前提であれば、低スペックの無料インスタンスでも一定の応答性を維持できます。

セキュリティとSSO設計の勘所

外部公開しない前提でも、認証の一元化は必須です。KeycloakでOIDCクライアントを作成し、oauth2-proxyを介してNginxの背後にある各アプリケーションにIDトークンを中継します。グループやロールはKeycloak側で付与し、Wikiの編集権限や検索APIの書き込み権限に連携させます。監査を容易にするため、すべてのHTTPリクエストにユーザーのサブジェクトIDとメールをヘッダで付けると良いでしょう。TLSはLet’s Encryptの自動更新で要件を満たせますが⁶、社内からのみアクセスするならゼロトラストアクセスの無料枠を併用してWAN露出を最小化する選択肢もあります⁷。多要素認証(MFA)の有効化もセットで検討してください。

実装ガイド:0円スタックの具体構築

ここからは実装を段階的に進めます。最小構成の仮想マシンにDockerとDocker Composeを入れ、すべてをコンテナで統合します。環境変数は.envに分離し、ボリュームは/var/lib配下に永続化します。最初に全コンポーネントを起動し、次にNginxとoauth2-proxyで認証をかけ、最後に検索インデクサーとCIを繋ぎます。以下のサンプルは検証用の値を含むため、本番では必ず強固なパスワードや鍵に置き換えてください。

# docker-compose.yml
version: "3.8"
services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_USER: wikijs
      POSTGRES_PASSWORD: strongpassword
      POSTGRES_DB: wiki
    volumes:
      - pgdata:/var/lib/postgresql/data
    restart: unless-stopped

  keycloak:
    image: quay.io/keycloak/keycloak:24.0
    command: ["start", "--hostname-strict=false"]
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: adminpass
    ports:
      - "8081:8080"
    restart: unless-stopped

  wikijs:
    image: requarks/wiki:2
    environment:
      DB_TYPE: postgres
      DB_HOST: postgres
      DB_PORT: 5432
      DB_USER: wikijs
      DB_PASS: strongpassword
      DB_NAME: wiki
    depends_on:
      - postgres
    restart: unless-stopped

  meilisearch:
    image: getmeili/meilisearch:v1.7
    environment:
      MEILI_MASTER_KEY: master_key_here
    ports:
      - "7700:7700"
    restart: unless-stopped

  minio:
    image: quay.io/minio/minio:RELEASE.2024-05-10T01-41-38Z
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: minio
      MINIO_ROOT_PASSWORD: minio_pass
    volumes:
      - minio:/data
    ports:
      - "9000:9000"
      - "9001:9001"
    restart: unless-stopped

  oauth2-proxy:
    image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0
    environment:
      OAUTH2_PROXY_PROVIDER: keycloak-oidc
      OAUTH2_PROXY_COOKIE_SECRET: 32bytebase64secretbase=
      OAUTH2_PROXY_CLIENT_ID: wikiclient
      OAUTH2_PROXY_CLIENT_SECRET: client_secret_here
      OAUTH2_PROXY_OIDC_ISSUER_URL: http://keycloak:8080/realms/internal
      OAUTH2_PROXY_EMAIL_DOMAINS: "*"
      OAUTH2_PROXY_HTTP_ADDRESS: 0.0.0.0:4180
      OAUTH2_PROXY_REVERSE_PROXY: "true"
      OAUTH2_PROXY_PASS_ACCESS_TOKEN: "true"
    ports:
      - "4180:4180"
    depends_on:
      - keycloak
    restart: unless-stopped

  nginx:
    image: nginx:1.25
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - wikijs
      - oauth2-proxy
    restart: unless-stopped

volumes:
  pgdata:
  minio:

Nginxはすべてのアプリに対してoauth2-proxy経由でアクセスさせ、未認証ならKeycloakにリダイレクトさせます。ヘッダにユーザー情報を付与し、バックエンドに渡します。

# nginx.conf
worker_processes auto;
events { worker_connections 1024; }
http {
  server {
    listen 80;
    server_name knowledge.example.internal;
    location / {
      return 301 https://$host$request_uri;
    }
  }
  server {
    listen 443 ssl http2;
    server_name knowledge.example.internal;
    ssl_certificate     /etc/letsencrypt/live/knowledge.example.internal/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/knowledge.example.internal/privkey.pem;

    # auth via oauth2-proxy
    location /oauth2/ {
      proxy_pass       http://oauth2-proxy:4180;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_set_header X-Forwarded-Host $host;
      proxy_set_header X-Forwarded-Uri $request_uri;
    }

    location /wiki/ {
      auth_request /oauth2/auth;
      error_page 401 = /oauth2/start?rd=$request_uri;
      proxy_pass http://wikijs:3000/;
      proxy_set_header X-User-Email $upstream_http_x_auth_request_email;
      proxy_set_header X-User $upstream_http_x_auth_request_preferred_username;
    }

    location /docs/ {
      auth_request /oauth2/auth;
      error_page 401 = /oauth2/start?rd=$request_uri;
      root /usr/share/nginx/html; # Docusaurus のビルド出力を配置
      try_files $uri $uri/ /docs/index.html;
    }

    location = /oauth2/auth {
      internal;
      proxy_pass http://oauth2-proxy:4180/oauth2/auth;
      proxy_set_header X-Forwarded-Uri $request_uri;
      proxy_set_header X-Real-IP $remote_addr;
    }
  }
}

oauth2-proxyの設定はKeycloakのクライアント情報と一致させます。メールドメインでの制限、クッキーの保護、アクセストークンの透過を明示します。

# oauth2-proxy.cfg の例(環境変数でも可)
provider = "keycloak-oidc"
email_domains = ["example.co.jp"]
upstreams = ["static://200"]
pass_access_token = true
cookie_secure = true
cookie_httponly = true
http_address = ":4180"
redirect_url = "https://knowledge.example.internal/oauth2/callback"
login_url = "https://knowledge.example.internal/oauth2/start"

全文検索のために、Wiki.jsのAPIとDocusaurusのビルド成果物からテキストを収集し、Meilisearchに投入します。Node.jsの小さなジョブで十分です。エラーは指数バックオフで再試行し、投入サイズを1,000件単位に分割します。

// indexer.js
import fetch from "node-fetch";
import { MeiliSearch } from "meilisearch";

const meili = new MeiliSearch({ host: process.env.MEILI_HOST, apiKey: process.env.MEILI_KEY });

async function fetchWikiPages() {
  const res = await fetch(`${process.env.WIKI_URL}/api/pages`, {
    headers: { Authorization: `Bearer ${process.env.WIKI_TOKEN}` }
  });
  if (!res.ok) throw new Error(`Wiki API ${res.status}`);
  const data = await res.json();
  return data.items.map(p => ({ id: `wiki_${p.id}`, title: p.title, content: p.contentPlain }));
}

async function fetchDocs() {
  const res = await fetch(`${process.env.DOCS_INDEX}`);
  if (!res.ok) throw new Error(`Docs index ${res.status}`);
  const list = await res.json();
  return Promise.all(list.files.map(async f => {
    const r = await fetch(`${process.env.DOCS_BASE}/${f}`);
    return { id: `docs_${f}`, title: f.replace(/\.mdx?$/, ""), content: await r.text() };
  }));
}

async function chunkedIndex(index, docs, size = 1000) {
  for (let i = 0; i < docs.length; i += size) {
    const slice = docs.slice(i, i + size);
    await index.addDocuments(slice);
  }
}

async function main() {
  const index = await meili.getIndex("knowledge").catch(async () => meili.createIndex("knowledge", { primaryKey: "id" }));
  let tries = 0;
  while (tries < 5) {
    try {
      const [wiki, docs] = await Promise.all([fetchWikiPages(), fetchDocs()]);
      await chunkedIndex(index, [...wiki, ...docs]);
      console.log(`Indexed: ${wiki.length + docs.length}`);
      return;
    } catch (e) {
      tries += 1;
      const wait = Math.min(32000, 1000 * 2 ** tries);
      console.error(`Index error: ${e}. retry in ${wait}ms`);
      await new Promise(r => setTimeout(r, wait));
    }
  }
  process.exitCode = 1;
}

main().catch(err => { console.error(err); process.exit(1); });

開発文書はGitで管理し、CIで静的サイトをビルドしてNginxの公開ディレクトリへ同期します。無料のリポジトリと自前サーバーの組み合わせで追加費用をかけずに運用できます。以下はDocusaurusをNode.js 20でビルドし、SSHで同期する例です。

# .github/workflows/deploy-docs.yml
name: Build & Deploy Docs
on:
  push:
    branches: ["main"]
    paths: ["docs/**", "docusaurus.config.js", "package.json"]
jobs:
  build-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run build
      - name: Deploy via rsync
        env:
          SSH_HOST: ${{ secrets.SSH_HOST }}
          SSH_USER: ${{ secrets.SSH_USER }}
          SSH_KEY:  ${{ secrets.SSH_KEY }}
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_KEY" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          rsync -az --delete -e "ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa" build/ ${SSH_USER}@${SSH_HOST}:/usr/share/nginx/html/docs/

初期セットアップを短時間で終えるには、OS設定とファイアウォールを小さく揃えるのが早道です。Docker、Certbot、UFWをまとめて導入し、HTTPとHTTPSだけを開けます。

#!/usr/bin/env bash
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y ca-certificates curl gnupg ufw
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo $VERSION_CODENAME) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin certbot
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80,443/tcp
ufw --force enable
systemctl enable docker --now

バックアップは0円でも設計次第で堅牢にできます。PostgreSQLのダンプとMinIOのバケットをローカル別ディスクに日次退避し、世代管理を30日に設定します。Pythonでエラー処理と整合性チェックを入れておくと復旧試験が容易です。

# backup.py
import os, subprocess, datetime, sys

BACKUP_DIR = "/var/backups/knowledge"
PG_URL = os.environ.get("PG_URL", "postgresql://wikijs:strongpassword@localhost/wiki")
BUCKET = os.environ.get("MINIO_BUCKET", "attachments")

os.makedirs(BACKUP_DIR, exist_ok=True)
ts = datetime.datetime.utcnow().strftime("%Y%m%d-%H%M%S")
pg_path = os.path.join(BACKUP_DIR, f"pg-{ts}.dump")
minio_path = os.path.join(BACKUP_DIR, f"minio-{ts}.tar")

try:
    subprocess.run(["pg_dump", PG_URL, "-Fc", "-f", pg_path], check=True)
    subprocess.run(["mc", "alias", "set", "local", "http://127.0.0.1:9000", "minio", "minio_pass"], check=True)
    subprocess.run(["mc", "cp", "--recursive", f"local/{BUCKET}", "/tmp/minio-backup"], check=True)
    subprocess.run(["tar", "cf", minio_path, "/tmp/minio-backup"], check=True)
except subprocess.CalledProcessError as e:
    print(f"backup failed: {e}", file=sys.stderr)
    sys.exit(1)

# retention 30 days
subprocess.run(["find", BACKUP_DIR, "-type", "f", "-mtime", "+30", "-delete"], check=True)
print("backup ok")

TLS、ドメイン、ゼロトラストの無料運用

DNSは無料のプロバイダを用意し、Aレコードをサーバーに向けます。Let’s EncryptのHTTP-01で証明書を取得し、更新はCronで自動化します⁶。社外からの安全なアクセスが必要でもコストをかけたくない場合は、無料枠が用意されたゼロトラストアクセス製品のトンネル機能を活用し、公開ポートを閉じたまま社内SSOで保護された経路だけを開けると、攻撃面が最小化されます⁷。SSOと合わせて多要素認証を有効にし、回復コードの配布と定期リビューを運用に組み込みます。

運用KPIとコスト、定量効果の見立て

0円構築の真価は、実装できたかではなく、業務時間をどれだけ返せたかで測るべきです。最初の四半期は三つのKPIに絞ると評価が容易です。検索に要する平均時間、重複ドキュメントの比率、アクティブユーザー率の三点です。検索時間については、導入前後のベースラインを取ったうえで30%短縮を初期目標に置くのが妥当です。仮に100名のホワイトカラーが週あたり90分を検索に費やしているとすると、一人あたり27分の削減、組織全体で週45時間、月180時間の回収という試算になります。人件費を時給5,000円と仮置きすれば、月90万円相当の間接コスト削減に匹敵します。重複率はファイル名やハッシュで計測し、10%未満までの削減を狙います。アクティブ率は週次で60%以上を保つと習慣化が進みます。

インフラコストは無料枠を維持しつつ、可用性の体感値を高めることができます。無料枠のリソースであっても、稼働率の目安を99.5%程度に置けば、業務時間に与える影響は限定的です。ダウンタイムの許容は月3時間強という水準ですが、夜間のメンテナンスに集中させるだけでも実害は小さくなります。インシデントの一次対応は当番制で平準化し、平均復旧時間30分以内を意識します。

ベンチマークの目安とチューニングの具体策

静的ドキュメントの配信はボトルネックになりにくいため、Nginxでの圧縮とETag、キャッシュ制御を有効化すれば、体感速度の改善が期待できます。Meilisearchの検索はシャードを増やす必要はなく、インデックスの属性選択を絞るだけで応答時間を数十ミリ秒に抑えられるケースが多いです。Wiki.jsは画像の埋め込みが大量になると負荷が増えるため、MinIOに画像を上げてキャッシュ相当の仕組みを活用するとメモリ増加を抑えられます。JavaのKeycloakはJVMオプションでヒープを512MB〜1024MBに縛ると安定しやすく、ガベージコレクションのポーズ時間も短くなります。CPU使用率が平均で10%を超えてくるようなら、インデクサーの実行頻度を15分に伸ばしてピーク帯の競合を避けます。

リスクと拡張:無料を卒業する判断基準

0円という制約は設計力を高めますが、使われるほど限界が見えてきます。ユーザーが300を超え、同時接続が50を安定的に上回り、検索インデックスが数百万ドキュメント規模になった時点が、有償のマネージド移行やスケールアウトを検討する合図です。データの可用性要件が高くなると、二重化とオフサイトバックアップが必須となり、ここからはさすがに無償では賄えません。そのときのために、最初からコンポーネント間を疎結合にしておくことが重要です。SSOはそのまま活かし、Wikiは商用SaaSまたは別のOSSに差し替え、検索は外部の検索サービスやクラスタリング対応のエンジンに移すなど、部品単位で置き換え可能なアーキテクチャにしておけば、停止時間を最小に抑えて段階的に拡張できます。

また、ガバナンスの観点では、公開範囲の誤設定、退職者のアクセス残置、機密資料の誤登録が主要リスクです。四半期ごとの権限棚卸し、機密区分のラベル運用、監査ログの保全をルーチン化し、年1回はディザスタリカバリの机上演習と実機復旧テストを行うと、実運用の事故率が大きく下がります。無料で始めても、プロセスの成熟度は無料では手に入りません。ここは投資よりも習慣の領域で、チームの合意と小さな継続が効きます。

内部ナレッジの定着を加速する小さな工夫

技術的な基盤が整っても、使われなければ価値は生まれません。週次のデモで「今週の検索トップ3」と「更新された重要ドキュメント」を5分で共有し、PRのテンプレートにドキュメントの更新チェックを入れておくと、利用率が着実に上がります。検索キーワードの分析から不足しているトピックを特定し、埋めるタスクをスプリント計画に入れる運用が定着すると、検索時間の短縮が数字で見えるようになります。

まとめ:今日始めて、来週には数字で示す

情報共有は壮大な改革である必要はありません。0円から始められる構成でも、SSOで守られたWiki、Git管理のドキュメント、軽快な全文検索、統一されたファイル保管が揃えば、検索時間の30%短縮という具体的な成果に現実味が出ます。最初の目標は高機能ではなく、運用の確実さと習慣化です。今日VMを一台用意し、ここで示したComposeを起動し、明日には最初のページをチームで共同編集してみてください。来週の定例で検索時間のベースラインを共有できたなら、すでに改善は始まっています。

もし今のチームで始めることに迷いがあるなら、関係者三名でパイロットを切って、二週間だけ本気で運用してみるのはどうでしょう。実データが後押ししてくれます。追加コスト0円から動き出せるのが、この設計の実用的な利点です。

参考文献

  1. KMWorld. “According to Interact Source, time—the…” https://www.kmworld.com/Articles/ReadArticle.aspx?ArticleID=135756&pageNum=2#:~:text=According%20to%20Interact%20Source%2C%20time%E2%80%94the,to%20do%20their%20jobs%20effectively
  2. CMSWire. “Knowledge Really Is Power: Search and the Intelligent Workplace — The average modern enterprise has data spread across 329 business applications.” https://www.cmswire.com/digital-workplace/knowledge-really-is-power-search-and-the-intelligent-workplace/#:~:text=The%20average%20modern%20enterprise%20has,data%C2%A0spread%20across%20329%20business%20applications
  3. AWS. “Free Tier FAQs.” https://aws.amazon.com/free/free-tier-faqs/
  4. Oracle Cloud Infrastructure. “Always Free Resources (up to 4 OCPU and 24 GB of memory).” https://docs.oracle.com/iaas/Content/FreeTier/freetier_topic-Always_Free_Resources.htm#:~:text=,and%2024%20GB%20of%20memory
  5. Keycloak. “Memory and CPU Sizing.” https://www.keycloak.org/high-availability/concepts-memory-and-cpu-sizing
  6. Let’s Encrypt. Official site. https://letsencrypt.org/
  7. Cloudflare. “Starting Cloudflare Zero Trust at $0.” https://getcloudflare.com/starting-cloudflare-zero-trust-at-dollar0
  8. PR TIMES(楽天インサイト調査). 「1日のうち社内情報を調べている時間は平均1時間以上…」 https://prtimes.jp/main/html/rd/p/000000190.000027275.html