SSD Nodes Learn
ガイド Matt Connor著者 Matt Connor ・更新日 2026-07-24

VPSでNextcloudをDockerで構築する方法

Docker ComposeとPostgres、Redis、Nginxを用いてVPS上にNextcloudを構築する手順を解説します。SQLiteを避け、Let's EncryptによるTLS設定と、データ消失を防ぐための確実なバックアップ・アップグレード手法を具体的に紹介します。

実装内容の概要

このガイドでは、Docker Composeを使用してVPS上でNextcloudを構築します。Let's Encrypt TLSをフロントに配置し、確実にリストア可能なバックアップを設定します。構成は4つのコンテナと1つのプロキシです。ループバックで待機する公式の nextcloud イメージ、ファイルメタデータを保持するPostgres、ファイルロックを保持するRedis、cronループのみを実行する2つ目のNextcloudイメージ、そしてすべてのTLS終端を担うホスト上のnginxで構成されます。インストールには20分かかりますが、重要なのはそこではありません。1時間以内に行う2つの決定が、1年後にファイルが残っているかどうかを左右します。SQLiteではなく実用的なデータベースを使用すること、そしてdataディレクトリ、データベース、config.php を一貫したセットとしてキャプチャするバックアップを作成することです。

この手順は、Ubuntu 24.04 LTSまたはDebian 13、Docker自身のレポジトリからインストールされたCompose v2プラグインを含むDocker Engine、およびVPSに cloud.example.com を指すDNS A レコード(IPv6を使用する場合は AAAA も含む)が設定済みであることを前提としています。すべての作業には、自身で管理するサーバーが必要です。他社のSaaS上でTLS終端とデータベースダンプを同時に行うことはできません。

サイジング:メモリ消費の主な要因

Nextcloudのメモリ使用量は、主に以下の3つの要素によって決まります。これらは「Nextcloud」そのもののメモリ消費ではありません。

PHP workers. -apache イメージは、PHPインタープリタを保持するワーカープロセスを使用して、各同時リクエストを処理します。各ワーカーは、PHPがリクエストを強制終了するまでの上限として PHP_MEMORY_LIMIT までメモリを消費する可能性があります。最悪のケースにおける常駐メモリ量は、おおよそ 同時リクエスト数 × メモリ制限 です。デスクトップ同期クライアントは、ユーザーごとに複数の並列接続を開きます。上限を決めるのは、ユーザー数ではなく同時実行数です。

データベース. Postgresは接続ごとにバックエンドをフォークし、共有バッファを常駐させます。ワーキングセットのサイズは、バイト数ではなく ファイル数 に比例して増加します。oc_filecache は、ユーザーごとにファイル1つにつき1行を保持します。10万個の小さなファイルは、100個の大きなファイルよりもデータベースへの負荷が高くなります。

プレビュー生成. サムネイルの生成には、元の画像をフル解像度でメモリ上にデコードする必要があります。ビデオプレビューは ffmpeg を呼び出します。occ preview:generate-all を実行すると、このスパイク(急激なメモリ消費)が連続して発生します。これは、小規模なVPSがOOM killerによって強制終了される最も一般的な原因です。

Redisの消費量は比較的低いです。後から追加する機能(Collabora、全文検索、アンチウイルススキャナーなど)は、それぞれ独自のメモリフットプリントを持つ別の常駐サービスとなります。これらは、機能を有効にする前にサイジング計画に含める必要があります。

RAMが不足している場合の対策は以下の通りです:PHP_MEMORY_LIMIT を下げる、preview_max_x / preview_max_y / preview_max_filesize_image に上限を設ける、enabledPreviewProviders を実際に閲覧する形式のみに絞る、そしてデータディレクトリがファイルサイズの数倍に肥大化しないよう trashbin_retention_obligationversions_retention_obligation を設定する。また、swap file を追加してください。Swapは低速ですが、アップグレード中にOOM killが発生する事態はそれよりも深刻です。

SQLite が動作しない理由

Nextcloud は SQLite をサポートしており、公式イメージでもそのまま使用可能です。しかし、使用しないでください。SQLite はデータベース全体にロックをかけて書き込みをシリアル化します。つまり、ファイル全体に対して一度に一つの書き込みしかできません。Nextcloud は、ファイルロック、activity 行、cache エントリ、job 状態など、絶えず書き込みを行います。また、ディレクトリツリーを同期するデスクトップクライアントは、多くの並列リクエストを送信します。このパターンでは SQLSTATE[HY000]: General error: 5 database is locked や HTTP 500 が発生します。この問題は、インスタンスが実用的な規模になり始めた時に発生します。

後から occ db:convert-type を使って変換することは可能ですが、稼働中のデータセットに対して行う、長時間のやり直しができない移行作業となります。最初から Postgres または MariaDB を使用してください。

Compose file

この内容を /srv/nextcloud/compose.yaml に記述してください。secrets は、同じディレクトリにある mode 600.env ファイルに配置します。

services:
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    volumes:
      - db:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: nextcloud
      POSTGRES_USER: nextcloud
      POSTGRES_PASSWORD: ${DB_PASSWORD}

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --requirepass ${REDIS_PASSWORD}

  app:
    image: nextcloud:31-apache
    restart: unless-stopped
    depends_on: [db, redis]
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - html:/var/www/html
      - /srv/nextcloud/data:/var/www/html/data
    environment:
      POSTGRES_HOST: db
      POSTGRES_DB: nextcloud
      POSTGRES_USER: nextcloud
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      REDIS_HOST: redis
      REDIS_HOST_PASSWORD: ${REDIS_PASSWORD}
      NEXTCLOUD_ADMIN_USER: admin
      NEXTCLOUD_ADMIN_PASSWORD: ${ADMIN_PASSWORD}
      NEXTCLOUD_TRUSTED_DOMAINS: cloud.example.com
      TRUSTED_PROXIES: 172.16.0.0/12
      OVERWRITEPROTOCOL: https
      OVERWRITECLIURL: https://cloud.example.com
      APACHE_DISABLE_REWRITE_IP: "1"
      PHP_MEMORY_LIMIT: 512M
      PHP_UPLOAD_LIMIT: 10G

  cron:
    image: nextcloud:31-apache
    restart: unless-stopped
    entrypoint: /cron.sh
    depends_on: [db, redis]
    volumes:
      - html:/var/www/html
      - /srv/nextcloud/data:/var/www/html/data

volumes:
  db:
  html:

31 をそのままコピーする前に、主要な tag を固定し、Docker Hub で現在の tag を確認してください。latest を使用すると、将来の docker compose pull でメジャーバージョンが更新される可能性があります。Nextcloud はその動作をサポートしていません。

data directory には、あえて named volume ではなく bind mount を使用します。バックアップツールから直接指定できるパスの方が、構成の簡潔さよりも重要だからです。イメージの www-data UID と、Nextcloud が必要とする権限でディレクトリを作成してください。

sudo mkdir -p /srv/nextcloud/data
sudo chown -R 33:33 /srv/nextcloud/data
sudo chmod 0770 /srv/nextcloud/data

port publish に注意してください: 127.0.0.1:8080:80。Docker は DNAT ルールを書き込むことでポートを公開します。このルールは ufw の INPUT chain よりも先に評価されます。そのため、8080:80 のままでは、ufw の設定に関わらず、暗号化されていない Nextcloud が公開インターネットに露出してしまいます。loopback にバインドすることで、公開インターフェースへの露出を防げます。その場合、ファイアウォールは proxy の許可設定だけで済みます。また、SSH をインターネット全体に公開したくない場合は、自己ホストの WireGuard VPN で VPS に接続する ことにより、公開ルールから port 22 を完全に削除できます。

sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

docker compose up -d で起動し、docker compose logs -f app でログを確認してください。初回起動時にアプリケーションツリー全体が volume にコピーされ、インストーラーが実行されます。この処理が完了するまで、コンテナは応答しません。

TLSとリバースプロキシ

ディストリビューションから nginx と certbot をインストールします。適切な server_name を設定した標準的な port-80 の server block を作成し、certbot で書き換えを行います。HTTP-01 チャレンジの仕組み、更新タイマー、および失敗パターンについては、issuing Let's Encrypt certificates with certbot and nginx on Ubuntu 24.04 で詳しく説明しています。

sudo apt install nginx certbot python3-certbot-nginx
sudo certbot --nginx -d cloud.example.com

Certbot は ssl_certificate 行と :80:443 のリダイレクトを追加し、90日ごとに証明書を更新する systemd timer をインストールします。systemctl list-timers | grep certbot で存在を確認してください。更新タイマーが有効になっていない場合、90日後に証明書が失効します。

プロキシブロックの内容:

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name cloud.example.com;

    # certbot manages ssl_certificate / ssl_certificate_key here

    add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;

    client_max_body_size 10G;
    client_body_timeout 300s;

    location = /.well-known/carddav { return 301 /remote.php/dav; }
    location = /.well-known/caldav  { return 301 /remote.php/dav; }

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host  $host;
        proxy_request_buffering off;
        proxy_buffering off;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}

nginx 1.25 以降では http2 on; を追加してください。Ubuntu 24.04 に搭載されている古いビルドでは、対応する設定は listen 443 ssl http2; です。nginx -t を実行すると、使用中のビルドがどちらを受け入れるか確認できます。

client_max_body_size と長い read timeout の設定により、大容量のアップロードが途中で失敗するのを防ぎます。proxy_request_buffering off を使用すると、ファイルをプロキシのディスクに一度書き込むのではなく、ストリームとして転送できます。

単一のアプリを運用する場合、ホスト上の nginx を使うのが最も簡単です。Nextcloud を他のコンテナと共有する VPS で運用する場合は、running Traefik as a Docker Compose reverse proxy for multiple apps を検討してください。これを使用すると、ルーティングと証明書の発行をコンテナの label に集約できます。その際、client_max_body_size やタイムアウトに関する考慮事項は、middleware や transport の設定として再び必要になります。

trusted_proxies と overwriteprotocol

これは、多くのセルフホスト型 Nextcloud インスタンスで発生する問題です。症状は原因と無関係に見えることがあります。

X-Forwarded-Proto: https は、リクエストが trusted_proxies に記載されたアドレスから送信された場合にのみ適用されます。適用されない場合、Nextcloud はリクエストをプレーンな HTTP と判断し、http:// URL を生成します。プロキシがそれらを HTTPS にリダイレクトし、ブラウザがそれに従うため、Nextcloud は再び http:// を生成します。これがリダイレクトループの原因です。OVERWRITEPROTOCOL: https を設定すると、スキームが固定されます。

TRUSTED_PROXIES における落とし穴は、Nextcloud が認識するアドレスが 127.0.0.1 ではないことです。nginx はホスト上で動作し、公開されたポートに接続します。そのため、コンテナからは Docker ブリッジのゲートウェイ(172.x の一部)が見えます。実際のサブネットを確認してください:

docker network inspect nextcloud_default \
  -f '{{range .IPAM.Config}}{{.Subnet}}{{end}}'

その CIDR(またはそれをカバーする 172.16.0.0/12)を TRUSTED_PROXIES に入力してください。範囲を広すぎると、クライアントが X-Forwarded-For を偽装できてしまいます。設定が間違っていると、すべてのログインがゲートウェイのアドレスから行われたとみなされます。その結果、ブルートフォース保護によってインスタンス全体がブロックされ、管理画面には "The reverse proxy header configuration is incorrect, or you are accessing Nextcloud from a trusted proxy." と表示されます。

cron コンテナには、ホスト名を推測するための受信リクエストがないため、OVERWRITECLIURL が重要になります。これがないと、バックグラウンドジョブが localhost へのリンクを生成し、メール通知に使用できない URL が含まれることになります。

バックグラウンドジョブ: cron(AJAXではなく)

Nextcloudのデフォルトのジョブ実行方式はAJAXです。これは、ユーザーがページを読み込んだ際にジョブが実行される仕組みです。午前4時などの時間帯にブラウジングするユーザーはいないため、ゴミ箱の期限切れ処理、バージョン管理のクリーンアップ、プレビューの生成、Federatedの再試行などが停止します。その結果、データディレクトリのサイズが際限なく増大します。上記の cron サービスは、同じボリュームに対して公式の /cron.sh ループを実行します。Nextcloudにこれを使用するよう設定してください。

docker compose exec -u www-data app php occ background:cron

すべての occ コマンドは、次の形式に従います: docker compose exec -u www-data app php occ <command>。エイリアスとして登録することをお勧めします。

バックアップ:3つの要素が揃わなければ意味がない

Filesystemのみのバックアップでは、壊れたインスタンスへの復元は不可能です。Data directoryにはデータ本体が含まれますが、Postgresはファイルキャッシュ、共有設定、ユーザー、アプリの状態を保持しています。また、config.phpはデータベースの認証情報、instance ID、およびpassword saltを保持しています。データベースなしでファイルを復元しても、Nextcloudはそれらを認識できません。config.phpなしでデータベースを復元しても、データベースを開くことはできません。新しいdata directoryに対して古いデータベースを復元すると、移動したファイルを指す共有設定が発生します。

停止状態(quiesced)のインスタンスから、以下の3つすべてをバックアップしてください:

#!/usr/bin/env bash
set -euo pipefail
cd /srv/nextcloud
DEST="/var/backups/nextcloud/$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$DEST"

occ() { docker compose exec -T -u www-data app php occ "$@"; }

occ maintenance:mode --on
trap 'occ maintenance:mode --off' EXIT

docker compose exec -T db \
  pg_dump -U nextcloud --clean --if-exists nextcloud | gzip > "$DEST/db.sql.gz"

docker compose exec -T app \
  tar -C /var/www/html -cf - config custom_apps themes > "$DEST/app.tar"

rsync -a --delete /srv/nextcloud/data/ /var/backups/nextcloud/data/

Maintenance modeを使用することで、dumpとファイルコピーの内容が一致します。これをスキップすると、rsyncがまだ到達していないファイルを、データベースが参照している状態のままキャプチャしてしまいます。このスクリプトは、タイムスタンプ付きのdatabase dumpsを保持しますが、data directoryのミラーは1つだけです。rsync --deleteは実行のたびに上書きするため、最新のdumpのみがファイルコピーと一致します。

次に、バックアップをサーバーから移動させてください。バックアップ対象と同じVPSに保存されたものは、バックアップではなく単なるコピーです。一般的には、object storageまたは別のホストに対してresticを使用します。これのdeduplicationは、毎晩のtarballよりもdata directoryの処理に優れています。リポジトリの初期化から nightly timer、復元手順までの全設定は、off-box VPS backups with resticに記載されています。

復元は、単に手順を逆にするだけでは不十分です。新しく起動したスタックはインストーラーを実行し、新しいconfig.php(新しいinstance IDとpassword salt)を作成します。その新しいIDに対してdumpをインポートすると、セッションやshare tokensが破損します。まず、以下の順序で古いIDを復元してください:

docker compose up -d && docker compose stop app cron    # create the volumes, then halt the app
sudo rsync -a --delete /var/backups/nextcloud/data/ /srv/nextcloud/data/
docker compose run --rm -T --entrypoint "" app \
  tar -C /var/www/html -xf - < app.tar                  # the original config.php returns
gunzip -c db.sql.gz | docker compose exec -T db psql -U nextcloud -d nextcloud
docker compose start app cron
docker compose exec -T -u www-data app php occ maintenance:mode --off
docker compose exec -T -u www-data app php occ files:scan --all

files:scanは、ファイルキャッシュをディスク上の実際の状態と一致させます。実際に必要になる前に、予備のVPSで一度練習しておいてください。

Upgrades: 一度に一つのメジャーバージョンのみ

Nextcloudは、一度に一つのメジャーバージョンのみアップグレード可能です。29から31へ直接アップグレードしようとすると、正常に終了せず、Exception: Updates between multiple major versions and downgrades are unsupported.が発生してメンテナンスモードになります。

Dockerでのアップグレード手順は以下の通りです。まずバックアップを取得します。次に、appcronの両方のサービスにおいて、tagを31から32へ書き換えます。その後、docker compose pull && docker compose up -dを行い、続いてdocker compose logs -f appを実行します。imageのentrypointが既存のデータに対して新しいコードを検出し、自動的にocc upgradeを実行します。この処理を中断しないでください。ログの出力が止まったら、docker compose exec -u www-data app php occ statusを実行し、versionstringとアプリが再度有効になっていることを確認してください。

トラブルを防ぐための2つのルールがあります。まず一つのメジャーバージョンを上げ、動作を確認してから、次のバージョンを上げてください。また、cronを一致させずにappサービスのtagを編集しないでください。一つのdatabaseに対して2つの異なるNextcloudバージョンが実行されると、データが破損する原因になります。

実際に発生するエラー

"Your data directory is readable by other users. Please change the permissions to 0770." バインドマウントされたディレクトリに、group または world の read ビットが設定されています。 sudo chmod 0770 /srv/nextcloud/data および sudo chown -R 33:33 /srv/nextcloud/data

"Your data directory is invalid. Ensure there is a file called .ocdata in the root." バインドマウントの先が、Nextcloud で初期化されていない場所を指しています。パスの入力ミス、または動作中のインスタンスに対して空のディレクトリが置換された可能性があります。ホスト側のパスが volume の行と一致しているか確認してください。

"Access through untrusted domain." リクエスト内の hostname が trusted_domains に含まれていません。 NEXTCLOUD_TRUSTED_DOMAINS は初回インストール時にのみ適用されます。その後は、occ config:system:set trusted_domains 1 --value=cloud.example.com で設定してください。

502 Bad Gateway (/var/log/nginx/error.log 内で connect() failed (111: Connection refused) while connecting to upstream が発生)。 nginx が 127.0.0.1:8080 で通信に失敗しています。コンテナがまだ初期化中であるか (docker compose logs app を確認)、コンテナが終了しているか (docker compose ps)、または publish の行が proxy_pass ポートと一致していない可能性があります。 ss -ltnp | grep 8080 で確認してください。

リダイレクトループ、または管理画面での "insecure" 警告。 OVERWRITEPROTOCOL: https が不足しているか、TRUSTED_PROXIES に Docker gateway のサブネットが含まれていません。上記の proxy セクションを参照してください。

LockedException: "files/..." is locked. REDIS_HOST を設定すると、イメージは Redis を locking backend として構成するため、stale locks は稀になります。設定しない場合、ロックはデータベースの oc_file_locks テーブルに保存されます。書き込み中にリクエストが中断されると、行が残ります。手動でロック行を削除する前に、occ config:system:get memcache.locking が Redis クラスを返すか確認し、実際に Redis が使用されているか確認してください。

"The PHP memory limit is below the recommended value of 512MB." PHP_MEMORY_LIMIT の値を増やし、コンテナを再作成してください。これにより、最悪の場合のメモリ上限がどう変わるかに注意してください。

スケールアップ時の問題点

最初の壁は、データディレクトリがボリュームの容量を超えることです。VPSでボリュームを拡張するには、リサイズとファイルシステムの拡張が必要です。容量が100%に達してから行うのは困難なため、ディスク使用率のアラートは早めに設定してください。

2番目の壁は oc_filecache です。行数が増えると、ファイル一覧の表示や同期スキャンが遅くなります。解決策はデータベースの最適化です。Postgresを高速なストレージに配置し、十分な共有メモリを割り当ててください。また、不要なデータやバージョンが蓄積しないよう、保持設定(retention settings)を使用して削除を行ってください。

3番目は、プレビュー生成によるリソース競合です。小規模なサーバーでは、プレビュープロバイダーを限定し、業務時間中に occ preview:generate-all を実行しないでください。

それ以上の規模になると、追加機能には専用のサーバーが必要です。Collaboraや全文検索は、独自のメモリプロファイルを持つ常駐サービスです。これらをファイルの実体があるサーバーと同じ場所に配置すると、メリットなく障害ドメインが拡大します。ボリュームの構成が適切でなくなったら、ファイルストレージをS3互換のプライマリストレージに移行してください。ただし、これによりバックアップは複雑になります。データベースにはメタデータが保持されるため、バケットと同期してダンプする必要があります。

インスタンスが実際のユーザーに提供される段階になったら、Uptime Kuma を導入してください。同期クライアントが異常を検知する前に、ダウンタイムを把握できます。プライベートクラウドは 独自のメールサーバー と相性が良いです。手動でのサービス連携を避けたい場合は、Cloudron, CasaOS, Coolify を比較して検討してください。

FAQ

Postgresの代わりにSQLiteでNextcloudを実行できますか?

実行可能です。公式イメージでも可能です。ただし、デスクトップ同期クライアントが並列リクエストを送信すると、SQLSTATE[HY000]: General error: 5 database is lockedが発生し、HTTP 500エラーになります。SQLiteはデータベース全体に書き込みロックをかけるためです。Nextcloudはファイルロック、アクティビティ行、ジョブの状態など、常に書き込みを行います。PostgresまたはMariaDBの使用を推奨します。occ db:convert-typeは存在しますが、稼働中のデータに対して行うには、すべてをやり直すような大規模な移行作業が必要です。

NextcloudのVPSには実際にどの程度のRAMが必要ですか?

ユーザー数ではなく、同時実行数に基づいてサイズを決定してください。最悪のケースにおける常駐メモリは、おおよそ「同時リクエスト数 × PHP_MEMORY_LIMIT」に、Postgresのshared buffersと接続ごとのバックエンド、およびプレビュー生成時のスパイク分を加算したものです。2 GBのサーバーであれば、プレビュー機能を制限し、swapを追加すれば小規模な家庭用インスタンスとして動作します。Collaboraや全文検索を追加する場合は、それら別の常駐サービス分のメモリも考慮する必要があります。

nginxリバースプロキシ経由で大きなアップロードが失敗するのはなぜですか?

通常、プロキシ側の2つの設定が原因です。client_max_body_sizeがデフォルトの1 MBのままではリクエストが切り捨てられます。また、proxy_read_timeoutproxy_send_timeoutの値が短いと、転送の途中で切断されます。これら両方の値を十分に大きく設定してください。また、proxy_request_buffering offをspoolではなくstreamに設定し、アプリコンテナ側のPHP_UPLOAD_LIMITもそれに合わせて引き上げてください。

Nextcloudがリダイレクトループに陥る、またはリバースプロキシに関する警告が出るのはなぜですか?

コンテナは127.0.0.1にあるnginxを見ていません。172.x付近にあるDocker bridge gatewayを見ています。TRUSTED_PROXIESにそのアドレスが設定されていない場合、X-Forwarded-Proto: httpsヘッダーが無視されます。その結果、Nextcloudがhttp://のURLを生成し、プロキシがそれを送り返すことでループが発生します。TRUSTED_PROXIESを実際のブリッジサブネットに設定し、OVERWRITEPROTOCOL: httpsを固定してください。

Nextcloudを29から31へ直接アップグレードできますか?

できません。Nextcloudは一度に1つのメジャーバージョンのみアップグレードできます。バージョンをスキップするとUpdates between multiple major versions and downgrades are unsupported.で停止し、インスタンスがメンテナンスモードになります。バックアップを取得し、appcronの両方のサービス、およびdocker compose pull && docker compose up -dのタグを1つずつ上げ、occ statusで確認した後、この手順を繰り返してください。