Nextcloud на своём сервере: nginx, PHP-FPM 8.3, Collabora и coturn
Задача: перевезти рабочий Nextcloud на новый сервер, попутно заменив Apache на nginx и приведя PHP в чувство. Тридцать пять пользователей, офисные документы онлайн, звонки в Talk.
Всё это делается по документации. Проблема в том, что документация описывает установку с нуля на чистую систему, а реальность — это перенос живого инстанса, где половина настроек унаследована от предшественника, а вторая половина потерялась при копировании.
Поэтому статья не столько про установку, сколько про грабли. Их набралось на полтора десятка, и почти каждая давала симптом, по которому причину не угадать.
Домен в примерах — cloud.example.com, публичный адрес сервера — 203.0.113.10, внутренний — 10.130.0.34.
Порядок работ
Чтобы не отлаживать всё разом, разумная последовательность такая:
- Базовая система: nginx, PHP-FPM, кэш, SSL. На этом этапе Nextcloud должен просто открываться.
- Восстановление того, что не переехало: скрытые файлы, права, настройки
config.php. - Пользователи.
- Collabora.
notify_pushи coturn — то, что нужно клиентам и звонкам, но не мешает работе, если отложить.
Я так и шёл, и это оказалось правильным решением: каждый следующий этап ломался по своим причинам, и разделение позволяло не путать их между собой.
Первое, что теряется при переезде: .htaccess
После копирования файлов Nextcloud встретил меня ошибками в разделе «Проверка безопасности». Причина простая: .htaccess и .user.ini — файлы скрытые, и при копировании некоторыми способами синхронизации они не поехали.
Может показаться, что при переходе на nginx .htaccess не нужен вовсе. Нужен: Nextcloud проверяет его наличие и содержимое сам, независимо от того, какой веб-сервер стоит, а .user.ini задаёт лимиты PHP.
Восстанавливаются из официального архива своей версии:
apt install -y bzip2
cd /tmp
wget https://download.nextcloud.com/server/releases/nextcloud-34.0.1.tar.bz2
tar xjf nextcloud-34.0.1.tar.bz2 nextcloud/.htaccess nextcloud/.user.ini
cp nextcloud/.htaccess nextcloud/.user.ini /var/www/html/
chown www-data:www-data /var/www/html/.htaccess /var/www/html/.user.ini
Версия архива должна совпадать с версией инстанса — файлы между релизами меняются.
Ошибка 500 сразу после переезда
Первый запуск — белый экран и 500. В логе nginx:
PHP Fatal error: Uncaught OCP\HintException: [0]:
Memcache OC\Memcache\APCu not available for local cache
config.php переехал вместе с сайтом и требует APCu, которого на новом сервере нет. Ставим кэш целиком, раз уж всё равно настраиваем окружение:
apt install -y php8.3-fpm redis-server \
php8.3-{gd,mysql,curl,mbstring,intl,gmp,bcmath,xml,zip,imagick,apcu,redis}
Для APCu обязательна строка, разрешающая работу в CLI, иначе occ и cron-задания будут падать с той же ошибкой, что и веб:
; /etc/php/8.3/mods-available/apcu.ini
apc.enable_cli=1
Разделение ролей стандартное: APCu — локальный кэш, Redis — распределённый кэш и блокировки файлов.
'memcache.local' => '\OC\Memcache\APCu',
'memcache.distributed' => '\OC\Memcache\Redis',
'memcache.locking' => '\OC\Memcache\Redis',
'redis' => [
'host' => '/var/run/redis/redis-server.sock',
'port' => 0,
],
JIT, которого не было
Включил OPcache с JIT в /etc/php/8.3/fpm/php.ini, перезапустил, проверяю — JIT выключен. Проверяю ещё раз, уже подозревая в невнимательности себя, — выключен.
Разгадка в порядке загрузки конфигов. Файлы из conf.d читаются после основного php.ini и спокойно перебивают его значения. А в /etc/php/8.3/fpm/conf.d/10-opcache.ini, который приезжает вместе с пакетом, лежало opcache.jit=off.
Править надо именно этот файл:
; /etc/php/8.3/fpm/conf.d/10-opcache.ini
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=32
opcache.max_accelerated_files=20000
opcache.revalidate_freq=60
opcache.jit=1255
opcache.jit_buffer_size=64M
И проверять не содержимое файла, а эффективное значение:
php-fpm8.3 -i | grep -E "opcache.jit|jit_buffer"
Привычка смотреть на то, что PHP реально применил, а не на то, что записано в конфиге, экономит часы. Особенно в дистрибутивных сборках, где conf.d живёт своей жизнью.
nginx: два места, где легко ошибиться
Конфиг для Nextcloud есть в официальной документации, и брать надо оттуда — он большой и меняется от версии к версии. Но два блока стоит понимать отдельно, потому что именно они дают загадочные 403 и 404 уже после того, как сайт вроде бы заработал.
Скрытые файлы и .well-known. Классическое правило «резать всё, что начинается с точки» ломает WebFinger, и в проверках вылезает ошибка про /.well-known/webfinger:
# было
location ~ /\. { deny all; }
# стало
location ~ /\.(?!well-known) { deny all; }
Регулярка PHP-локации. Вариант с (?:$|/) в некоторых конфигурациях отдаёт PATH_INFO не целиком, и часть маршрутов Nextcloud отваливается. Рабочий вариант:
location ~ \.php(?:$|/.*) {
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
set $path_info $fastcgi_path_info;
try_files $fastcgi_script_name =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $path_info;
fastcgi_param HTTPS on;
fastcgi_param modHeadersAvailable true;
fastcgi_param front_controller_active true;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_intercept_errors on;
fastcgi_request_buffering off;
fastcgi_max_temp_file_size 0;
}
Сертификат — обычный Let’s Encrypt через certbot, тут ничего интересного. Интересное начнётся ниже, когда до этого сертификата доберётся coturn.
Тридцать пять пользователей одной командой
Создавать учётки руками через веб-интерфейс — занятие для человека, которому некуда девать вечер. У occ есть user:add, пароль передаётся через переменную окружения OC_PASS.
И вот здесь ловушка, из-за которой скрипт сначала молча не работал: sudo -u www-data вычищает окружение. Переменная, экспортированная в скрипте, до occ не доезжает, и команда падает на требовании пароля.
Рабочий вариант — экспортировать переменную внутри той же оболочки, которая запускает occ:
#!/bin/bash
NC=/var/www/html
while IFS=';' read -r login name email pass; do
sudo -u www-data bash -c "export OC_PASS='${pass}' && \
php ${NC}/occ user:add --password-from-env \
--display-name='${name}' \
--group='Сотрудники' '${login}'"
sudo -u www-data php ${NC}/occ user:setting "${login}" settings email "${email}"
done < users.csv
Формат users.csv — login;Имя Фамилия;email;пароль. Файл после отработки удалить, а не оставить в /root до лучших времён: там лежат пароли всех сотрудников в открытом виде.
Collabora в Docker и WOPI allowlist
Онлайн-редактор поднимается контейнером:
docker run -t -d -p 127.0.0.1:9980:9980 \
-e "aliasgroup1=https://cloud\\.example\\.com:443" \
-e "username=admin" -e "password=<пароль>" \
--restart always \
--name collabora collabora/code
Порт публикуется только на loopback — наружу его отдаёт nginx:
location ^~ /browser {
proxy_pass https://127.0.0.1:9980;
proxy_set_header Host $host;
proxy_ssl_verify off;
}
location ^~ /hosting/capabilities {
proxy_pass https://127.0.0.1:9980;
proxy_set_header Host $host;
proxy_ssl_verify off;
}
location ^~ /hosting/discovery {
proxy_pass https://127.0.0.1:9980;
proxy_set_header Host $host;
proxy_ssl_verify off;
}
location ^~ /cool {
proxy_pass https://127.0.0.1:9980;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_ssl_verify off;
}
Проверка, что бэкенд жив:
curl -sk https://127.0.0.1:9980/hosting/discovery | head -3
Должен прийти XML, начинающийся с <wopi-discovery>. Если пришёл — с Collabora всё в порядке, и дальнейшие ошибки будут не про неё.
Дальше — две вещи, без которых при открытии документа вылезет «Неавторизованный хост WOPI».
Контейнер обращается к Nextcloud со своего адреса в докеровской сети (172.17.0.0/16), и этот адрес должен быть в списке разрешённых:
sudo -u www-data php /var/www/html/occ config:app:set richdocuments wopi_allowlist \
--value="127.0.0.1/8,203.0.113.10/32,172.17.0.0/16"
Та же подсеть — в доверенных прокси в config.php:
'trusted_proxies' => ['127.0.0.1', '203.0.113.10', '172.17.0.0/16'],
И отдельно: приложение richdocumentscode — встроенный сервер редактирования — конфликтует с внешним Collabora. Если оно установлено, документы будут открываться то так, то этак:
sudo -u www-data php /var/www/html/occ app:disable richdocumentscode
Общее правило для этой связки: почти все ошибки Collabora сводятся к вопросу «с какого адреса компонент видит соседа». Проверять надо wopi_allowlist, trusted_proxies и overwrite.cli.url — в этом порядке.
notify_push
Без него клиенты опрашивают сервер по таймеру, с ним получают уведомления мгновенно и заметно снижают нагрузку на PHP-FPM. Бинарник идёт вместе с приложением notify_push, вешаем его юнитом:
# /etc/systemd/system/notify_push.service
[Unit]
Description=Nextcloud notify_push
After=network.target redis-server.service
[Service]
User=www-data
ExecStart=/var/www/html/apps/notify_push/bin/x86_64/notify_push /var/www/html/config/config.php --port 7867
Restart=on-failure
[Install]
WantedBy=multi-user.target
systemctl enable --now notify_push
sudo -u www-data php /var/www/html/occ notify_push:setup https://cloud.example.com/push
Команда notify_push:setup сама проверит доступность и подскажет, если nginx не проксирует нужный путь.
coturn: NAT и права на ключ
TURN нужен для звонков в Talk, когда участники за NAT. Две настройки отличают рабочий coturn от нерабочего.
Первая — формат external-ip на облачной виртуалке. У машины публичный адрес не на интерфейсе, а на границе сети, поэтому указывать надо пару «внешний/внутренний»:
# /etc/turnserver.conf
listening-port=3478
tls-listening-port=5349
fingerprint
use-auth-secret
static-auth-secret=<секрет>
realm=cloud.example.com
external-ip=203.0.113.10/10.130.0.34
cert=/etc/letsencrypt/live/cloud.example.com/fullchain.pem
pkey=/etc/letsencrypt/live/cloud.example.com/privkey.pem
no-cli
Вторая — доступ к приватному ключу. coturn работает не от root и по умолчанию читать ключи Let’s Encrypt не может. При этом служба стартует и выглядит живой: порт 3478 отвечает, а 5349 молчит.
chgrp -R turnserver /etc/letsencrypt/live /etc/letsencrypt/archive
chmod g+rx /etc/letsencrypt/live /etc/letsencrypt/archive
chmod g+r /etc/letsencrypt/archive/cloud.example.com/privkey*.pem
И ключевой момент, о котором вспоминают месяца через три, когда звонки внезапно ломаются: права слетают при обновлении сертификата. Поэтому сразу хук:
# /etc/letsencrypt/renewal-hooks/post/coturn.sh
#!/bin/bash
chgrp -R turnserver /etc/letsencrypt/live /etc/letsencrypt/archive
chmod g+r /etc/letsencrypt/archive/cloud.example.com/privkey*.pem
systemctl restart coturn
chmod +x /etc/letsencrypt/renewal-hooks/post/coturn.sh
Проверить, что TLS-порт реально ожил:
ss -tlnp | grep -E '3478|5349'
echo | openssl s_client -connect cloud.example.com:5349 2>/dev/null | openssl x509 -noout -dates
Мелочи, которые стоит сделать сразу
Swap. Даже если памяти хватает, четыре гигабайта подкачки с низкой агрессивностью спасают от OOM при пиковых операциях вроде массовой генерации превью:
fallocate -l 4G /swapfile && chmod 600 /swapfile
mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
sysctl -w vm.swappiness=10
InnoDB. Дефолтные 128 МБ буферного пула на реальной базе малы:
# /etc/mysql/mariadb.conf.d/50-server.cnf
innodb_buffer_pool_size = 512M
Cron. Через systemd-таймер, не через AJAX. AJAX-режим означает, что фоновые задачи выполняются, только когда кто-то открыл страницу.
Server ID. В «Проверке безопасности» вылезет требование его задать:
'server_id' => 'cloud01',
Несовместимые приложения. После обновления мажорной версии часть приложений отваливается молча и роняет интерфейс. У меня таким оказался side_menu — лечится occ app:disable.
Проверка после настройки
- «Проверка безопасности» в админке чистая, кроме осознанно оставленных пунктов
-
occ statusиocc config:list systemотрабатывают без ошибок -
php-fpm8.3 -iпоказывает, что OPcache и JIT реально включены - документ открывается в Collabora, правки сохраняются
- десктопный клиент получает изменения мгновенно (значит,
notify_pushработает) - звонок между двумя участниками из разных сетей проходит
- порт 5349 слушается и отдаёт актуальный сертификат
- в
nextcloud.logза сутки нет повторяющихся ошибок
Последний пункт удобно смотреть не глазами, а свёрткой по частоте:
cat /var/www/html/data/nextcloud.log | python3 -c "
import sys, json
from collections import Counter
msgs = Counter()
for line in sys.stdin:
try:
e = json.loads(line)
if e.get('level', 0) >= 3:
msgs[e.get('message','')[:80]] += 1
except Exception:
pass
for msg, cnt in msgs.most_common(10):
print(f'{cnt}x {msg}')
"
Один и тот же текст, повторённый двести раз, обычно означает одну конкретную недонастройку, а не двести проблем.
Что осталось за кадром
Высокопроизводительный бэкенд для Talk (HPB) — он нужен для звонков больше чем на три-четыре участника и просится на отдельную машину. Двухфакторная аутентификация — решение организационное, а не техническое. Обе строчки так и висят у меня в «Проверке безопасности» жёлтым, и это осознанный выбор, а не забывчивость.
Мораль
Первое. Перенос — это не копирование. Скрытые файлы, права на ключи, зависимости от модулей PHP и хуки certbot не переезжают сами, а обнаруживаются по одному, обычно в порядке возрастания неочевидности.
Второе. В PHP надо смотреть на эффективное значение параметра, а не на строчку в конфиге. conf.d перебивает php.ini, и знать это лучше до, а не после часа отладки JIT.
Третье. Все проблемы связки Nextcloud и Collabora сводятся к одному вопросу: с какого адреса компонент видит соседа. Docker-подсеть в wopi_allowlist и trusted_proxies лечит девять ошибок из десяти.
Четвёртое, и самое живучее. Права на сертификат, выставленные руками, живут ровно до следующего продления. Всё, что зависит от файлов Let’s Encrypt и работает не от root, обязано иметь renewal-hook — иначе поломка придёт через два месяца, когда связь с настройкой уже забудется.
#nextcloud #nginx #php #docker #collabora #coturn