MAATRIX / Блог / Joplin Server на сервере: частые ошибки и решения

Joplin Server на сервере: частые ошибки и решения

MAATRIX

Joplin Server — штука простая на бумаге: поднял контейнер, прописал URL синхронизации в приложении, готово. На практике почти каждый, кто это делал, натыкался хотя бы на одну из трёх вещей: приложение крутит "Synchronising..." и ничего не происходит, вложения (картинки, PDF) не долетают до других устройств, или после обновления контейнера сервер вообще перестаёт стартовать с невнятной ошибкой миграции базы. Ниже — разбор конкретных сбоев Joplin Server и рабочие решения для каждого, без общих слов про "перезапустите и всё заработает".

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Почему вообще стоит свой Joplin Server

Joplin Cloud стоит подписку и данные лежат не у вас. WebDAV-синхронизация (через Nextcloud, например) работает, но медленнее и капризнее к конфликтам, чем родной протокол Joplin Server — он умеет дельта-синхронизацию и правильно резолвит конфликты правок заметок. Держать это на своём сервере имеет смысл, если:

  • заметки содержат рабочую или личную информацию, которую не хочется отдавать в чужое облако;
  • синхронизируете больше двух-трёх устройств и хотите предсказуемую скорость;
  • уже есть VPS под другие сервисы — Joplin Server легковесный (SQLite или PostgreSQL, минимум RAM), можно посадить рядом.

Минимальные требования комфортные: 1 vCPU, 512 МБ-1 ГБ RAM с головой хватает, диск зависит от объёма вложений (у активного пользователя с картинками в заметках база может вырасти до нескольких гигабайт за пару лет).

Установка через Docker Compose: минимальный рабочий вариант

Официальный образ — joplin/server. Разворачивать без Docker избыточно, проще собрать docker-compose.yml:

version: "3.8"

services:
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: замените_на_свой_пароль
      POSTGRES_USER: joplin
      POSTGRES_DB: joplin
    volumes:
      - ./pgdata:/var/lib/postgresql/data
    networks:
      - joplin_net

  app:
    image: joplin/server:latest
    restart: unless-stopped
    depends_on:
      - db
    ports:
      - "127.0.0.1:22300:22300"
    environment:
      APP_BASE_URL: https://notes.example.com
      APP_PORT: "22300"
      DB_CLIENT: pg
      POSTGRES_PASSWORD: замените_на_свой_пароль
      POSTGRES_DATABASE: joplin
      POSTGRES_USER: joplin
      POSTGRES_PORT: "5432"
      POSTGRES_HOST: db
      MAILER_ENABLED: "0"
    networks:
      - joplin_net

networks:
  joplin_net:

Важные моменты, которые обычно упускают:

  • APP_BASE_URL должен быть точным публичным адресом, по которому сервер доступен снаружи, с протоколом https:// и без слэша в конце. Это не косметика — Joplin Server сверяет его при обмене токенами, и рассинхрон здесь ломает авторизацию клиентов.
  • MAILER_ENABLED: "0" отключает почтовые уведомления, если не настраивали SMTP — иначе сервер будет пытаться слать письма и падать в логи ошибками, не критично, но засоряет диагностику.
  • SQLite (DB_CLIENT: sqlite3) годится только для одного-двух пользователей с небольшим объёмом заметок — при росте базы и параллельных запросах от нескольких устройств начинаются блокировки. Для реального использования берите PostgreSQL сразу, миграцию с SQLite на Postgres потом делать больнее, чем настроить один раз правильно. Разбор нюансов PostgreSQL под нагрузкой — в статье про настройку PostgreSQL на сервере.

Поднимаем:

docker compose up -d
docker compose logs -f app

Если в логах видно Server ready без ошибок — контейнер поднялся. Дальше — самое интересное, потому что 80% реальных проблем начинаются на этапе реверс-прокси и первого подключения клиента.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Ошибка 1: 502 Bad Gateway или "Cannot connect to server"

Классика: контейнер работает (docker compose ps показывает Up), но приложение Joplin на телефоне или ноутбуке пишет "не удалось подключиться" или в браузере по адресу сервера висит 502.

Причина почти всегда одна из трёх:

  1. Реверс-прокси не проксирует на правильный порт или адрес. Nginx-конфиг для Joplin Server должен указывать именно на 127.0.0.1:22300 (или порт, который вы открыли), а не на 80/443 внутри контейнера.
server {
    listen 443 ssl http2;
    server_name notes.example.com;

    ssl_certificate     /etc/letsencrypt/live/notes.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/notes.example.com/privkey.pem;

    client_max_body_size 200M;

    location / {
        proxy_pass http://127.0.0.1:22300;
        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_read_timeout 300s;
    }
}
  1. client_max_body_size не выставлен — по умолчанию Nginx режет тело запроса на 1 МБ, а вложения (фото, PDF) в Joplin легко превышают это. Итог — синхронизация "зависает" именно на файлах с вложениями, текстовые заметки при этом синхронизируются нормально. Это частая и неочевидная причина, потому что ошибка выглядит как рандомный сбой, а не как явный лимит.
  1. Контейнер app перезапускается в цикле из-за недоступности db. Проверяется командой docker compose logs db — если Postgres не успел подняться до старта app, помогает depends_on с condition: service_healthy и healthcheck у сервиса db, либо просто restart: unless-stopped у app, который со временем сам переподключится.

Если используете Caddy вместо Nginx — там client_max_body_size не нужен, лимит по умолчанию отсутствует, но стоит свериться с общим сравнением в статье Caddy или Nginx: что выбрать для сервера, если решаете, на чём разворачивать прокси с нуля.

Ошибка 2: CORS и "Invalid origin" при работе через Joplin Web Clipper или API

Если помимо самого приложения используете Joplin Web Clipper (расширение браузера для сохранения страниц) или обращаетесь к API сервера напрямую — можно словить ошибки CORS в консоли браузера или 403 Forbidden от API.

Важно понимать: сам Joplin Server не работает как API для веб-клиппера напрямую — Web Clipper обращается к локальному Joplin-приложению (порт 41184 на компьютере пользователя), а не к серверу синхронизации. Путаница здесь — источник половины "CORS-ошибок", которые на самом деле являются неправильной настройкой самого клиппера, а не сервера.

Реальные CORS-проблемы с сервером синхронизации возникают, если вы пишете собственный скрипт-интеграцию к Joplin Server API (например, для автоматического бэкапа заметок или экспорта). В этом случае:

  • убедитесь, что запросы идут с того же APP_BASE_URL, что указан в конфиге сервера;
  • добавьте заголовки в Nginx явно, если обращаетесь с другого домена:
location /api/ {
    add_header 'Access-Control-Allow-Origin' 'https://ваш-фронтенд.example.com' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
    proxy_pass http://127.0.0.1:22300;
}

Это нужно только для кастомных интеграций — штатный клиент десктопа/мобильного Joplin CORS не касается вообще, он работает не через браузер.

Ошибка 3: вложения не синхронизируются или "битые" на других устройствах

Симптом: заметка синхронизируется, текст на месте, но картинка вместо превью показывает битую иконку или крутится бесконечная загрузка вложения.

Причины по частоте:

  1. Недокачанное вложение из-за таймаута. Большие файлы (сканы, видео) не успевают выгрузиться за время proxy_read_timeout. Увеличьте таймаут в Nginx (см. конфиг выше, proxy_read_timeout 300s уже с запасом) и убедитесь, что client_max_body_size перекрывает реальный размер файлов.
  1. Диск сервера заполнился. Joplin Server хранит вложения либо в базе (при DB_CLIENT: pg и включённом хранении в БД — по умолчанию так и есть в новых версиях), либо на файловой системе, в зависимости от конфигурации STORAGE_DRIVER. Если диск под pgdata заполнен, запись вложений молча обрывается, а в логах Postgres будет no space left on device. Проверка:
df -h
docker exec -it <container_db> psql -U joplin -c "SELECT pg_size_pretty(pg_database_size('joplin'));"
  1. Конфликт версий приложения и сервера. Joplin активно развивается, и клиент заметно новее сервера (или наоборот) иногда не может корректно синхронизировать новые типы ресурсов. Держите joplin/server:latest актуальным и обновляйте клиенты синхронно — резкий разрыв версий на 5-6 релизов часто и есть причина "битых" вложений.

Для регулярного бэкапа базы с вложениями логика та же, что и для любого сервиса на Postgres — подход из статьи про бэкап и восстановление в Nextcloud переносится почти один в один: дамп базы плюс архив тома с данными по расписанию.

Ошибка 4: сервер не стартует после обновления образа — ошибка миграции

После docker compose pull && docker compose up -d иногда контейнер app падает в рестарт-луп, а в логах — ошибка вида Migration failed или relation already exists.

Это происходит, если:

  • миграция базы была прервана на середине (например, контейнер убили во время апдейта);
  • вы откатились на более старую версию образа после того, как новая уже применила свои миграции — Joplin Server не умеет откатывать схему автоматически.

Что делать:

  1. Сначала — бэкап базы, прежде чем что-либо трогать:
docker exec <container_db> pg_dump -U joplin joplin > joplin_backup_$(date +%F).sql
  1. Проверьте логи на конкретную таблицу/миграцию, которая упала:
docker compose logs app | grep -i migration
  1. Если миграция зависла на середине — зайдите в базу и проверьте таблицу knex_migrations, где Joplin Server ведёт учёт применённых миграций. Ручное вмешательство в неё рискованно и должно быть последним средством — прежде стоит попробовать просто повторить docker compose up -d, часть таких ошибок самоустраняется при повторном запуске, потому что миграция идемпотентна.
  1. Если ничего не помогает и данные некритичны для сохранения истории — проще поднять новый контейнер на чистой базе и восстановить синхронизацию с клиента, у которого сохранилась актуальная локальная копия заметок (Joplin умеет полный ре-аплоад базы на новый сервер синхронизации).

Golden rule здесь: никогда не обновляйте продакшн-сервер синхронизации без свежего дампа базы — Joplin Server, в отличие от какого-нибудь статического сайта, хранит единственную копию правок, если у клиентов давно не было полной синхронизации.

Настройка HTTPS и первое подключение клиента без граблей

Joplin-клиенты (десктоп, мобильные) отказываются синхронизироваться с self-signed сертификатом без явного разрешения "Ignore TLS errors", что небезопасно оставлять постоянно. Ставьте нормальный сертификат через Let's Encrypt:

sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d notes.example.com

Сравнение certbot и альтернативы — в статье Certbot или acme.sh: что выбрать для сервера, если сертификатов на сервере уже несколько и хочется единого подхода.

После этого в клиенте Joplin: Настройки → Синхронизация → Joplin Server, указываете:

  • Sync target: Joplin Server
  • Sync target URL: https://notes.example.com
  • Email и Password — учётка администратора, которую нужно создать при первом заходе на веб-интерфейс сервера (по умолчанию admin@localhost / admin, обязательно смените сразу после первого входа — это первое, что проверяют боты при сканировании открытых портов).

Проверка синхронизации: создайте тестовую заметку, нажмите "Синхронизировать" вручную и смотрите статус в нижнем углу — если висит на "Uploading resources..." дольше пары минут при небольшом файле, возвращайтесь к разделу про таймауты и client_max_body_size выше.

Ограничение доступа и базовая защита сервера

Joplin Server сам по себе не имеет встроенной защиты от перебора паролей и открыт для регистрации новых пользователей, если явно не отключить. Что стоит сделать сразу:

  • отключить открытую регистрацию, если сервер личный: SIGNUP_ENABLED: "0" в переменных окружения app;
  • закрыть порт 22300 от внешнего мира (127.0.0.1:22300:22300 в docker-compose, как в примере выше — наружу торчит только Nginx на 443);
  • настроить fail2ban на логи Nginx, чтобы банить IP за подбор паролей к веб-интерфейсу — конкретные правила описаны в статье Fail2Ban для Nginx: настройка правил;
  • держать firewall с разрешёнными портами только 22 (SSH, лучше на нестандартном), 80, 443 — базовая настройка в статье про UFW на сервере.

Отдельно: если сервер на нескольких пользователей (семья, небольшая команда) — заведите каждому отдельный аккаунт через админку, не шарьте один логин на всех. Joplin Server поддерживает несколько независимых пользователей с изолированными данными на одном инстансе, это штатный сценарий, а не хак.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Можно ли синхронизировать через WebDAV вместо Joplin Server?

Да, Joplin поддерживает WebDAV как альтернативный протокол (например, через Nextcloud). Он проще в настройке, но медленнее на больших базах и хуже резолвит конфликты одновременных правок — Joplin Server создан специально под этот сценарий и предпочтителен, если решаете с нуля.

Сколько RAM реально нужно на 3-5 устройств с обычным объёмом заметок?

Ориентировочно 512 МБ-1 ГБ хватает с запасом вместе с PostgreSQL на том же сервере — но это зависит от объёма вложений и частоты синхронизации, точных цифр без замера на своей нагрузке дать нельзя.

Что делать, если забыл пароль администратора Joplin Server?

Пароль хранится в базе как хеш, восстановить его напрямую нельзя. Проще всего зайти в базу через psql и вручную сбросить пароль пользователя через встроенную команду сервера, либо создать нового администратора через переменные окружения при первом деплое, если это ещё не продакшн с данными.

Нужен ли отдельный сервер только под Joplin Server, или можно на общий VPS?

Можно на общий — сервис лёгкий и не требует выделенной машины, если ресурсы позволяют. Главное — не забыть выделенный volume под базу и настроить бэкапы отдельно от остальных сервисов, чтобы обновление одного контейнера не задело другой.

Почему после смены APP_BASE_URL клиенты перестали синхронизироваться?

Joplin Server привязывает токены и часть логики к APP_BASE_URL на момент выдачи. При смене адреса иногда нужно выйти из аккаунта на клиентах и войти заново — простой перезапуск контейнера это не чинит.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →