Как подключить storage VPS к основному серверу
Оптимизируйте расходы на хранение данных, разделив вычислительные задачи и дисковое пространство. Настройте связку через WireGuard и NFS или rclone с защитой от сбоев монтирования.
Зачем использовать VPS для хранения данных в связке с основным VPS
Используйте VPS для хранения данных вместе с основным VPS, чтобы каждый сервер выполнял ту задачу, для которой он предназначен. Небольшой быстрый сервер запускает приложение, его базу данных и кэш. Большой и недорогой сервер хранит терабайты данных и отдает их по сети. Частный туннель WireGuard объединяет их, и приложение видит удаленный диск как обычную директорию по пути /mnt.
Причина разделения — стоимость гигабайта. VPS для хранения данных жертвует мощностью CPU и IOPS ради объема, поэтому за те же ежемесячные расходы вы получаете гораздо больше места, если не платите за быстрые вычисления для каждого терабайта. Если вы еще не выбрали продукт для хранения, сначала изучите сравнение VPS для хранения данных с блочными томами и объектным хранилищем, так как варианты подключения, описанные ниже, зависят от выбранного типа хранилища.
Цена такого разделения — один сетевой запрос (round trip) перед каждой операцией с файлом. Локальный stat() — это обращение к оперативной памяти. Тот же stat() на удаленном диске — это запрос, ожидание и ответ. Никакие параметры монтирования не устранят этот сетевой запрос. Вы можете лишь сократить их количество, совершаемое приложением.
Два решения определяют, будет ли работать такая связка. Оба сервера должны находиться в одном регионе. Базы данных, кэши и временные файлы для транскодирования должны оставаться на локальном диске. Если ошибиться в любом из этих пунктов, дальнейшие инструкции из этого руководства не помогут.
Почему оба сервера должны находиться в одном регионе
Время кругового обхода (RTT) — это весь ваш бюджет. Внутри одного центра обработки данных время обмена пакетами между двумя VPS составляет доли миллисекунды. Между двумя континентами оно превышает это значение более чем в сто раз, и этот множитель применяется к каждой операции, а не только к медленным.
Рассмотрим работу сканера медиафайлов. Он считывает каталог, затем вызывает stat() для каждой записи, а после открывает некоторые из них для чтения встроенных метаданных. Таким образом, библиотека из десяти тысяч файлов требует как минимум десяти тысяч круговых обходов до того, как начнут передаваться данные видео. Проблема здесь не в общей пропускной способности. Потоковая передача одного большого файла по длинному каналу работает нормально, так как операции чтения крупные и последовательные, а ядро выполняет упреждающее чтение. Проблема заключается в обходе дерева каталогов, так как каждая мелкая операция ожидает ответа на предыдущую.
Итог: один провайдер, один регион, в идеале — один центр обработки данных. Проверьте это перед тем, как что-либо переносить. Арендуйте оба сервера на один месяц, поднимите туннель и выполните замеры с помощью ping -c 100 10.9.0.2. Если среднее значение не составляет малую долю миллисекунды, не создавайте эту связку. Размещайте приложение рядом с данными или переходите на объектное хранилище, где шаблон доступа предполагает один крупный запрос за раз.
Что никогда не должно находиться на удаленном хранилище
Четыре типа файлов должны располагаться на локальном диске сервера приложений.
- Файлы баз данных. SQLite, PostgreSQL и MariaDB требуют, чтобы
fsync()означало именно то, что написано, и им необходима блокировка файлов для корректной работы между процессами. При использовании удаленного монтирования оба компонента зависят от удаленной стороны и стабильности сети. Когда соединение прерывается, база данных не переходит в режим ожидания. Она зависает или повреждается. - Кэши, индексы и миниатюры. Это небольшие файлы, которые постоянно считываются и легко восстанавливаются. Размещение их за сетевым соединением — худшее решение. Каждая страница галереи превращается в серию мелких удаленных запросов на чтение.
- Временные файлы транскодирования. Транскодер записывает и удаляет данные с высокой интенсивностью и небольшими порциями. Укажите его временную директорию на локальном диске и убедитесь, что настройка действительно применена.
- Файлы блокировок и unix-сокеты. Файл
.pidили сокет в общей директории не имеют смысла при работе на двух разных машинах, к тому же некоторые удаленные файловые системы обрабатывают их некорректно.
На удаленной стороне хранятся объемные файлы, которые записываются один раз и считываются целиком: видео, оригиналы фотографий, архивы, репозитории резервных копий.
Для медиасервера такое разделение очевидно. Jellyfin, запущенный на VPS, хранит свою конфигурацию и метаданные в /var/lib/jellyfin на локальном диске, а видео считывает с примонтированного хранилища. Перенесите /var/lib/jellyfin на удаленный сервер, и каждый клик в веб-интерфейсе будет ожидать ответа от удаленной стороны. С фотографиями ситуация аналогична: в Immich библиотека загрузок и база данных Postgres — это разные вещи. Библиотека может находиться на примонтированном диске. База данных — нет.
Соединение двух узлов с помощью WireGuard
Не экспортируйте файловую систему на публичный IP-адрес. По умолчанию NFS не использует шифрование транспорта, а его аутентификация сводится к доверию числовому идентификатору пользователя (UID), который передает клиент. Поместите трафик внутрь туннеля.
Полная настройка описана в руководстве по самостоятельному развертыванию WireGuard на VPS. Ниже приведена версия для двух узлов. Сгенерируйте пару ключей на каждом сервере:
sudo apt update && sudo apt install -y wireguard
sudo install -d -m 700 /etc/wireguard
sudo sh -c 'umask 077; wg genkey > /etc/wireguard/priv.key'
sudo sh -c 'wg pubkey < /etc/wireguard/priv.key > /etc/wireguard/pub.key'На сервере приложений выполните /etc/wireguard/wg0.conf:
[Interface]
Address = 10.9.0.1/24
ListenPort = 51820
PrivateKey = <application box private key>
[Peer]
PublicKey = <storage box public key>
AllowedIPs = 10.9.0.2/32
Endpoint = <storage box public IP>:51820
PersistentKeepalive = 25На сервере хранения данных выполните аналогичные действия: Address = 10.9.0.2/24, укажите AllowedIPs = 10.9.0.1/32 узла-партнера, а в параметре endpoint задайте публичный адрес сервера приложений. Оба файла должны иметь права доступа 600. Затем поднимите туннель на обоих узлах:
sudo chmod 600 /etc/wireguard/wg0.conf
sudo systemctl enable --now wg-quick@wg0
sudo wg show
ping -c 3 10.9.0.2Команда wg show должна отобразить партнера с недавней строкой latest handshake, а команда ping должна отвечать. Если в информации о партнере отсутствует строка handshake, значит, пакеты не доходят. Проверьте порт UDP 51820 в локальных файерволах обоих хостов, а также в сетевом файерволе вашего провайдера — в большинстве панелей управления это отдельная настройка.
В данной конфигурации могут возникнуть две проблемы с туннелем, о которых стоит знать заранее. Если крупные последовательные операции чтения зависают, в то время как ping продолжает работать, это указывает на проблему с MTU пути. Решение заключается в поиске реального MTU пути методом деления пополам вместо подбора случайных чисел. Если вы обращаетесь к серверу хранения по имени хоста, а не по его адресу в туннеле, разрешение DNS через туннель станет еще одной точкой отказа при загрузке. Используйте прямой адрес 10.9.0.2 в каждом определении монтирования, и эта проблема не возникнет.
Option 1: NFS over the tunnel
NFS is the strongest option for a filesystem in the same region, because the kernel client caches attributes and because its behaviour under failure is tunable rather than fixed.
On the storage box:
sudo apt update && sudo apt install -y nfs-kernel-server
sudo install -d -m 755 /srv/media
sudo chown 1000:1000 /srv/mediaAdd one line to /etc/exports, naming the application box's tunnel address:
/srv/media 10.9.0.1(rw,sync,no_subtree_check,root_squash)Apply and inspect it:
sudo exportfs -ra
sudo exportfs -vexportfs -v prints the options the kernel actually applied, which is not always what you typed, because defaults are filled in around your list.
The export line restricts which client address may mount, but the NFS server still listens on TCP 2049 on every interface. A port scan of the public address finds it. The export list is not a firewall, so add one:
sudo ss -lnt | grep 2049
sudo ufw status verbose
sudo ufw allow in on wg0 to any port 2049 proto tcpss shows the listener bound to every address, which is the point. ufw status verbose should print Default: deny (incoming). With that default in place, the single tunnel rule is enough: 2049 answers on wg0 and nowhere else. If the default is allow, fix that before going further.
NFSv4 without Kerberos maps ownership by numeric user id. If the application runs as uid 1000 on one box and the files are owned by uid 1000 on the other, it works. If the numbers differ, files show as owned by nobody or writes fail with Permission denied, and no amount of chmod helps. Make the ids match on both boxes, or add all_squash,anonuid=1000,anongid=1000 to the export so every client access lands on one owner.
On the application box, mount it:
sudo apt install -y nfs-common
sudo install -d -m 755 /mnt/media
sudo mount -t nfs4 -o vers=4.2,nconnect=4 10.9.0.2:/srv/media /mnt/media
findmnt /mnt/mediafindmnt prints the source, the type and the options in effect. Then make it permanent in /etc/fstab, with an automount so that a boot with the tunnel down does not hang the boot:
10.9.0.2:/srv/media /mnt/media nfs4 _netdev,x-systemd.automount,x-systemd.mount-timeout=30,x-systemd.idle-timeout=600,vers=4.2,nconnect=4,hard,timeo=100,retrans=3,acdirmin=60,acdirmax=600 0 0sudo systemctl daemon-reload
sudo systemctl start mnt-media.automount
systemctl status mnt-media.automountThe unit name comes from the path: /mnt/media becomes mnt-media.automount. With x-systemd.automount, systemd only installs a trigger at boot. The real mount happens on first access, so the storage box being slow to boot cannot delay your application box.
Three option choices are worth understanding rather than copying. nconnect=4 opens several TCP connections to the same server, up to a documented limit of 16, which helps when a single connection's window caps throughput on a link with real latency. acdirmin and acdirmax set how long the client trusts its cached directory attributes before asking again, and raising them from the short defaults is the single most effective change for metadata-heavy scans. The trade is that a file added on the storage box by some other process appears late. If files only ever arrive through the application box, that trade costs you nothing, because the client updates its own cache when it writes. And noatime does nothing here, whatever the blog post you copied the line from says: the nfs man page states plainly that the atime and noatime options have no effect on NFS mounts.
Вариант 2: SSHFS
Для работы SSHFS на сервере хранения не требуется ничего, кроме SSH-аккаунта. Утилита работает в пространстве пользователя через FUSE, и каждая файловая операция преобразуется в SFTP-запрос внутри SSH-соединения.
Осознавайте, что вы используете. По состоянию на август 2026 года в README проекта указано, что у него «нет активных постоянных участников, и имеется ряд известных проблем», хотя мейнтейнер по-прежнему принимает pull requests и выпускает релизы. Пакет доступен во всех основных дистрибутивах и работает. Рассматривайте его как стабильный, а не как развивающийся продукт.
sudo apt install -y sshfs
sudo install -d -m 755 /mnt/media
sshfs -o reconnect,ServerAliveInterval=15,ServerAliveCountMax=3,uid=1000,gid=1000,allow_other,default_permissions storage@10.9.0.2:/srv/media /mnt/media
ls /mnt/mediaРазмонтируйте файловую систему с помощью fusermount -u /mnt/media, а не umount. Опции uid и gid преобразуют владельца удаленного файла в локальный ID, что позволяет обойти проблему несовпадения идентификаторов, характерную для NFS. Использование reconnect вместе с двумя настройками keepalive позволяет точке монтирования пережить кратковременный разрыв туннеля. Без них любой обрыв соединения навсегда «убивает» монтирование, и при каждом обращении будет возвращаться ошибка Transport endpoint is not connected, пока вы не выполните размонтирование и повторное монтирование.
Для записи в fstab требуется ключ без парольной фразы, доступный для чтения пользователю root:
storage@10.9.0.2:/srv/media /mnt/media fuse.sshfs _netdev,x-systemd.automount,IdentityFile=/root/.ssh/id_ed25519,reconnect,ServerAliveInterval=15,ServerAliveCountMax=3,uid=1000,gid=1000,allow_other,default_permissions 0 0В чем недостатки: каждая операция с метаданными требует полного цикла обмена данными через одно соединение, при этом отсутствует кэш атрибутов ядра, который в NFS-клиенте работает автоматически. Список файлов в директории, который NFS выдает из кэша, для SSHFS означает отдельный запрос на каждый элемент. Кроме того, вы выполняете двойное шифрование — один раз для SSH и один раз для WireGuard, что является бесполезной тратой ресурсов процессора на маломощном устройстве. Это указывает на оправданную альтернативу: отключите туннель и используйте SSHFS через публичный адрес сервера хранения, так как SSH безопасно выставлять наружу, в отличие от NFS. Выберите один вариант архитектуры. Не используйте оба одновременно.
Вариант 3: использование rclone mount
rclone также выполняет монтирование через FUSE, при этом удаленным ресурсом может выступать SFTP на сервере хранения или бакет объектного хранилища. Это подходящее решение, если на удаленной стороне находится объектное хранилище. Это неверный выбор, если удаленная сторона представляет собой файловую систему, которую можно примонтировать стандартными средствами.
sudo apt install -y rclone
rclone config
rclone lsd storage:rclone lsd вывод списка каталогов подтверждает, что удаленный ресурс настроен корректно, прежде чем вы попытаетесь его примонтировать. Точка монтирования должна быть существующим пустым каталогом в Linux, что прямо указано в документации rclone.
rclone mount storage:media /mnt/media --vfs-cache-mode full --vfs-cache-max-size 20G --dir-cache-time 72h --allow-other --daemon--vfs-cache-mode full обеспечивает работу обычных приложений с точкой монтирования и создает локальный кэш. Этот кэш размещается на небольшом быстром диске, который вы старались не переполнить, поэтому --vfs-cache-max-size не является опциональным параметром. --dir-cache-time 72h хранит списки каталогов в оперативной памяти в течение трех дней; именно это позволяет просматривать большие библиотеки, но означает, что файл, добавленный другим процессом на удаленной стороне, может не появиться в списке в течение трех дней. Для юнита systemd в документации отмечено, что Type=notify позволяет сервису перейти в состояние «запущен» только после того, как точка монтирования будет подготовлена, что важно для настройки порядка запуска других юнитов.
В чем заключаются недостатки: здесь отсутствуют семантики POSIX. В объектном хранилище операция переименования — это копирование с последующим удалением, а запись небольшого фрагмента в середину существующего большого файла приводит к перезаписи всего объекта целиком. Приложения, которые рассчитывают на работу с полноценной файловой системой (включая все медиасерверы), ведут себя непредсказуемо, и документация не может гарантировать корректность их работы. rclone оправдан для чтения больших файлов и для использования удаленного сервера в качестве площадки для внешнего резервного копирования. Он не подходит на роль диска общего назначения.
Поведение приложения при разрыве соединения
Определитесь с этим до начала настройки, так как поведение по умолчанию отличается от ожиданий большинства пользователей.
При использовании NFS hard, что является настройкой по умолчанию, разрыв соединения переводит любой процесс, обращающийся к точке монтирования, в состояние неубиваемого сна (uninterruptible sleep). ps показывает состояние D, kill -9 не выполняет никаких действий, а df зависает. Если ваша система мониторинга запускает df, мониторинг также зависнет. Ядро записывает в лог nfs: server 10.9.0.2 not responding, still trying. Когда соединение восстанавливается, вы получаете nfs: server 10.9.0.2 OK, и каждый процесс возобновляет работу с того же места без потери данных при записи. Это правильный выбор для любых операций записи.
При использовании NFS soft запросы завершаются с ошибкой ввода-вывода (I/O error) после того, как исчерпаны timeo и retrans, и приложение получает EIO, при этом процессы остаются доступными для завершения (killable). Предупреждение из man-страницы стоит процитировать дословно, так как оно серьезнее, чем принято считать: мягкий таймаут (soft timeout) «в определенных случаях может привести к скрытому повреждению данных», поэтому используйте его «только тогда, когда отзывчивость клиента важнее целостности данных». Это применимо для монтирования носителей только для чтения. Для путей записи это не подходит. Не пытайтесь использовать intr, чтобы смягчить жесткое монтирование (hard mount). В man-странице указано, что начиная с ядра 2.6.25 этот параметр игнорируется, поэтому в вашем fstab он является лишь комментарием и ничем более.
softreval занимает промежуточное положение. Он позволяет клиенту продолжать предоставлять пути и атрибуты из кэша после истечения времени попыток ревалидации, поэтому дерево каталогов остается доступным для просмотра, даже если сами данные недоступны.
SSHFS с reconnect самостоятельно выполняет повторные попытки подключения по SSH. Монтирование rclone с --vfs-cache-mode full продолжает отдавать файлы, уже находящиеся в кэше, и ставит операции записи в очередь для последующей выгрузки, в то время как операции чтения, отсутствующие в кэше, завершаются ошибкой.
Что бы вы ни выбрали, письменно ответьте на один вопрос: восстановится ли приложение самостоятельно при перезагрузке хранилища для обновления ядра, или потребуется вмешательство администратора? Протестируйте это, намеренно перезагрузив хранилище, прежде чем доверять этой связке какие-либо важные данные.
Первая проблема, с которой вы столкнетесь: запись в пустую точку монтирования
Эта ошибка приводит к реальной потере данных, и протокол здесь не имеет значения.
/mnt/media — это каталог. Когда монтирование активно, он отображает файлы хранилища. Когда монтирование неактивно, это пустой каталог на вашем небольшом локальном диске. Приложение не видит разницы. Оно записывает данные в /mnt/media, запись проходит успешно, и данные попадают на корневую файловую систему сервера приложения. Вы узнаете об этом, когда корневой диск переполнится и сервисы начнут аварийно завершаться. Ситуация усложняется при восстановлении: повторное монтирование скрывает ошибочно записанные файлы, поэтому диск остается заполненным, а du /mnt/media показывает пустую директорию, в которой нечего удалять.
Запретите запись в пустой каталог. Размонтируйте его, а затем установите неизменяемый атрибут:
sudo umount /mnt/media
sudo chattr +i /mnt/media
lsattr -d /mnt/medialsattr -d выводит флаги, и i появляется, пока поверх пути ничего не смонтировано. Теперь запись в несмонтированный путь завершается ошибкой Operation not permitted, поэтому приложение записывает ошибку в лог вместо того, чтобы незаметно заполнять ваш диск. Монтирование по-прежнему работает, так как оно не изменяет базовый каталог. Позже вы сможете снять атрибут с помощью chattr -i, если потребуется. Это работает в ext4 и XFS, которые используются в корневых файловых системах стандартных образов VPS.
Затем настройте зависимость сервисов от точки монтирования. Добавьте drop-in файл для systemd:
sudo systemctl edit jellyfin[Unit]
RequiresMountsFor=/mnt/mediaБудьте реалистичны в отношении того, что это дает. RequiresMountsFor= запускает сервис после юнита монтирования для этого пути и активирует его. С x-systemd.automount systemd лишь устанавливает триггер, поэтому сервис может запуститься, даже если хранилище недоступно. Зависимость делает сбой видимым в systemctl status. Неизменяемый каталог — это то, что предотвращает повреждение данных.
Docker — это отдельная ловушка, так как демон создает отсутствующий источник для bind-монтирования автоматически. В Compose отключите это поведение:
services:
jellyfin:
volumes:
- type: bind
source: /mnt/media
target: /media
read_only: true
bind:
create_host_path: falseДокументация Compose описывает create_host_path как параметр, который создает каталог по исходному пути на хосте, если он отсутствует (по умолчанию включено). Установка значения false запрещает Compose создавать путь самостоятельно. Это не помогает, если путь существует и пуст (что является типичным случаем), поэтому сохраняйте неизменяемый атрибут каталога.
Наконец, защитите каждый скрипт, который выполняет запись в точку монтирования:
mountpoint -q /mnt/media || { echo "/mnt/media is not mounted" >&2; exit 1; }Какой сервер является источником истины?
Определите это для каждого набора данных в письменном виде до запуска первого задания синхронизации.
Обычно каждому серверу отводится своя роль. Сервер приложений владеет небольшим изменяемым состоянием: базой данных, конфигурацией, ключами. Сервер хранения владеет основным объемом данных: медиафайлами, оригиналами, архивами. Ни один из серверов не является источником истины для всего сразу, поэтому резервное копирование выполняется в обоих направлениях, и запуск одного из них в обратную сторону легко может привести к ошибке.
Опасное направление — это rsync --delete push с сервера приложений на сервер хранения. Если точка монтирования недоступна, исходный каталог выглядит пустым, и --delete добросовестно удаляет содержимое в месте назначения. В результате терабайты данных исчезают за то время, которое требуется rsync для обхода списка файлов.
Две привычки позволяют этого избежать. Защищайте каждую синхронизацию проверкой mountpoint -q, описанной выше. И отдавайте предпочтение pull вместо push: запускайте задание на том сервере, где хранится копия, чтобы он сам подключался и забирал данные. Задание типа pull также защищает от последствий компрометации сервера приложений, так как учетные данные, позволяющие удалять резервные копии, не находятся на машине, которую злоумышленник взломал первой.
Используйте инструмент, который сохраняет снимки (snapshots), а не создает зеркало. restic и borg хранят историю, поэтому файл, удаленный или зашифрованный на источнике, можно восстановить из более раннего снимка, в то время как зеркало rsync в точности копирует удаление. Если сервер хранения является копией, а не оригиналом, настройте его как полноценную удаленную цель для резервного копирования с доступом только на добавление, чтобы источник мог добавлять снимки, но не мог их удалять.
Существует одно направление, о котором часто забывают. Если на сервере хранения находится единственная копия вашей библиотеки фотографий, сервер хранения сам нуждается в резервной копии. Восстановление должно охватывать обе части пары, выполненные достаточно близко по времени, чтобы они соответствовали друг другу. Снимок базы данных за вторник плюс библиотека фотографий за четверг при восстановлении приведут к нерабочей установке, где записи в базе данных ссылаются на файлы, которых там еще нет.
Что измерять на вашей паре узлов
Не доверяйте опубликованным показателям пропускной способности или задержки для этой архитектуры, включая те, что вы прочитали в других источниках. Результат зависит от вашего провайдера, региона и двух выбранных тарифных планов. Измерьте показатели на своей паре узлов. Каждую команду ниже вы выполняете самостоятельно с сервера приложения, если не указано иное.
Круговая задержка (round trip) через туннель, которая умножает время выполнения каждой операции с метаданными:
ping -c 100 10.9.0.2Чистая пропускная способность канала до подключения файловой системы. Запустите серверную часть на узле хранения:
iperf3 -siperf3 -c 10.9.0.2 -t 30Если это значение значительно ниже заявленного в вашем тарифе, проблема в сети или туннеле, и никакие параметры монтирования её не исправят. Проверьте MTU, прежде чем винить NFS.
Последовательное чтение больших объемов через точку монтирования для сравнения с этим числом:
fio --name=seqread --rw=read --bs=1M --size=2G --numjobs=1 --direct=1 --filename=/mnt/media/fio-test --end_fsync=1
rm -f /mnt/media/fio-testПри использовании FUSE-монтирования уберите --direct=1, если fio сообщает, что этот флаг не поддерживается, так как обработка O_DIRECT зависит от драйвера. Большой разрыв между этим показателем и значением iperf3 указывает на проблемы с монтированием, а не с каналом связи.
Стоимость операций с метаданными — показатель, который фактически определяет пригодность архитектуры:
time find /mnt/media -type f | wc -lЗапустите тест дважды. Первый проход выполняется «на холодную», второй показывает эффективность кэша атрибутов. Затем выполните ту же команду find локально на узле хранения и сравните результаты. Отношение этих двух временных показателей — это «налог», который данная архитектура накладывает на ваше приложение; измеряйте его на своем оборудовании, а не полагайтесь на предположения.
Если вы выбрали NFS, оцените задержку каждой операции напрямую:
nfsiostat 5 3
nfsstat -cУтилита nfsiostat поставляется в комплекте с nfs-common и отображает количество операций в секунду вместе со средним временем круговой задержки для каждой операции на каждой точке монтирования. nfsstat -c детализирует вызовы клиента по типам операций. Если вызовы getattr преобладают в статистике, ваша рабочая нагрузка ограничена скоростью обработки метаданных: увеличьте acdirmin и acdirmax, выполните перемонтирование и снова измерьте тот же показатель find.
Затем выполните тест, который многие пропускают. Перезагрузите узел хранения, пока приложение обрабатывает трафик, и наблюдайте за результатом. Это единственный способ узнать, соответствует ли поведение системы при сбоях тому, что вы настроили, или тому, что вы предполагали.
FAQ
Can I put a database on a storage VPS mount?
No. Database engines depend on fsync() reaching real storage and on file locking working across processes, and neither is reliable over NFS, SSHFS or an rclone mount. A stalled link does not pause a database politely: it hangs the process or corrupts the file. Keep the data directory of SQLite, PostgreSQL or MariaDB on the local disk of the application box. The mount holds bulk files that are written once and read whole.
NFS, SSHFS or rclone: which one should I use?
Use NFS over a WireGuard tunnel when the far side is a filesystem in the same region, because the kernel client caches attributes and because hard, soft and softreval let you choose the failure behaviour. Use SSHFS when you want no server-side setup and can accept a round trip for every metadata operation. Use rclone when the far side is object storage rather than a filesystem, where its VFS cache and a long --dir-cache-time are doing real work.
What happens to my application if the storage VPS goes offline?
With a default hard NFS mount, processes touching the mount block in uninterruptible sleep, kill -9 does not reach them, and the kernel logs nfs: server 10.9.0.2 not responding, still trying. They resume with no lost writes when the link returns. With soft, calls fail with an I/O error instead, at the documented risk of silent data corruption on writes. SSHFS with reconnect retries by itself, and an rclone mount keeps serving whatever is already in its VFS cache.
Why did my root disk fill up when the mount was down?
Because an unmounted mountpoint is still a writable empty directory on the local disk, so the application wrote into it and the data landed on the root filesystem. Mounting again hides those files underneath the mount, so the space stays used while the path looks empty. Unmount, delete what is underneath, then run sudo chattr +i /mnt/media on the empty directory so future writes fail with Operation not permitted instead of succeeding in the wrong place.
Do both servers really need to be in the same region?
Yes, for this design. Every metadata operation costs at least one round trip, and a library scan makes one per file, so a cross-region round trip multiplies the slowest part of the workload by a hundred or more. Measure with ping -c 100 over the tunnel before you migrate data. If the average is not a small fraction of a millisecond, keep the application next to its data instead.