Plane на сервере: частые ошибки и решения
Plane — открытая альтернатива Linear и Jira: канбан-доски, спринты, циклы, roadmap, свой API, без месячной платы за каждого участника команды. Официальный install-скрипт обещает поднять весь стек одной командой, но на практике self-hosted Plane — это восемь-девять контейнеров (веб, API, воркеры, планировщик, база, Redis, MinIO, прокси), и застрять можно на любом стыке между ними: миграции не проходят, домен отдаёт белый экран, вложения не грузятся, приглашения не долетают. Разберём эти проблемы по порядку — с конкретными командами и переменными окружения.
Содержание
- Установка через install-скрипт падает или зависает
- Backend не подключается к базе или Redis: `ECONNREFUSED` и пустые логи API
- Белый экран или CORS-ошибки при открытии через свой домен
- Вложения, аватары и обложки задач не загружаются
- Приглашения по email не приходят участникам команды
- Обновление версии и бэкап без потери данных
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Установка через install-скрипт падает или зависает
Официальный способ развернуть community-версию — скрипт, который тянет docker-compose.yml и .env из репозитория makeplane/plane и запускает всё через обёртку plane-app:
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/selfhost/install.sh -o install.sh
chmod +x install.sh
./install.sh install
Первое, на чём спотыкаются — скрипт молча завершается или зависает на шаге Pulling images, если на VPS меньше 2 vCPU и 4 ГБ RAM. Стек тянет сразу Next.js-фронтенды (web, admin, space), Django-backend, Celery-воркер, beat-scheduler, Postgres, Redis и MinIO — на 1–2 ГБ RAM контейнеры начинают падать по OOM ещё до того, как вы успели открыть браузер. Проверяйте сразу:
dmesg -T | grep -i "out of memory" | tail -10
docker compose -f docker-compose.yml ps
Второе — контейнер migrator завершается с ненулевым кодом и утаскивает за собой api, который в логах пишет relation does not exist или просто не стартует. Смотрите его логи отдельно, он выполняет Django-миграции один раз и должен выйти с кодом 0:
docker compose logs migrator --tail=100
docker compose ps migrator
Если статус Exited (1), почти всегда причина в том, что Postgres ещё не готов принимать соединения в момент запуска миграций (первый холодный старт тома базы занимает время), либо в .env перепутаны PGUSER/PGPASSWORD/PGDATABASE относительно того, что прописано в сервисе plane-db. Пересоздайте migrator после того, как plane-db перейдёт в healthy:
docker compose up -d plane-db
docker compose logs -f plane-db # ждём "database system is ready to accept connections"
docker compose up -d migrator
Третье — install-скрипт спрашивает домен и порт интерактивно, и если на сервере уже что-то слушает 80/443 (свой nginx, Caddy, другое приложение), встроенный контейнер proxy падает с bind: address already in use. Решение — не отдавать Plane публичные порты напрямую, а поставить перед ним свой reverse-proxy (см. ниже) и сменить LISTEN_HTTP_PORT/LISTEN_HTTPS_PORT на непубличные в .env.
Backend не подключается к базе или Redis: `ECONNREFUSED` и пустые логи API
Если вы разворачиваете Plane не через install-скрипт, а собственным docker-compose (например, чтобы завести Postgres или Redis отдельным стеком — по гайду по установке PostgreSQL на VPS или по установке Redis на VPS), первая ошибка при старте api обычно такая:
django.db.utils.OperationalError: could not connect to server: Connection refused
или в контейнере worker:
redis.exceptions.ConnectionError: Error 111 connecting to plane-redis:6379
Разбирайте по пунктам:
- Сервисы в разных docker-сетях. Если база и Redis подняты отдельным compose-файлом, а Plane — своим, имена хостов
plane-dbиplane-redisиз.envпросто не резолвятся из сети Plane. Либо объединяйте сервисы в один compose-файл, либо создавайте общую внешнюю сеть и подключайте к ней оба стека:
docker network create plane_shared
networks:
default:
external: true
name: plane_shared
depends_onбез healthcheck. Compose гарантирует только порядок запуска контейнеров, а не готовность Postgres принимать соединения —apiиworkerстартуют раньше и падают в первые секунды. Добавьте условие:
plane-db:
image: postgres:15.5-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${PGUSER}"]
interval: 5s
retries: 10
api:
depends_on:
plane-db:
condition: service_healthy
plane-redis:
condition: service_started
- Рассинхронизация переменных между сервисами.
DATABASE_URLвapi/worker/beat-workerдолжен буквально совпадать сPOSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB, заданными в сервисеplane-db— это два независимых блока в.env, и при ручном редактировании легко поправить один и забыть про другой. Сверяйте одной командой:
docker compose exec api env | grep -E "DATABASE_URL|REDIS_URL"
Если у вас Postgres не в Docker, а системный сервис на том же хосте, проверьте ещё pg_hba.conf и listen_addresses — по умолчанию Postgres слушает только localhost, а контейнеру нужен доступ по IP docker-сети или через host.docker.internal.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверБелый экран или CORS-ошибки при открытии через свой домен
Контейнеры подняты, docker compose ps показывает Up у всех, но открыв домен, вы видите либо белый экран без ошибок в интерфейсе, либо в консоли браузера — Access-Control-Allow-Origin и падающие запросы к /api/. Причина почти всегда в рассинхроне между тем, что реально видит браузер, и тем, что прописано в переменных окружения:
NEXT_PUBLIC_API_BASE_URLиWEB_URLуказывают не туда. Фронтенд собирается статически с зашитым в бандл адресом API — если вы поменяли домен после первой сборки образов, недостаточно поправить.envи перезапустить контейнер, нужно пересобрать фронтенд-образы заново (docker compose build web space adminлибо пересоздание через install-скрипт с новым доменом).CORS_ALLOWED_ORIGINSне включает ваш домен. Backend Plane явно проверяет Origin запроса, и если в списке осталсяhttp://localhostиз дефолтного.env, любой запрос с реального домена будет отклонён на уровне CORS-middleware ещё до бизнес-логики.- Встроенный контейнер
proxyконфликтует с вашим внешним nginx. Если вы, как советовали выше, вынесли Plane за собственный reverse-proxy, убедитесь, что проксируете именно на порт встроенногоproxy-контейнера (обычно 80 внутри docker-сети, наружу — то, что вы указали вLISTEN_HTTP_PORT), а не напрямую наwebилиapi— иначе часть путей (загрузка файлов, WebSocket для realtime) просто не найдёт нужный сервис. Пример конфига для nginx перед Plane:
server {
listen 443 ssl;
server_name plane.example.com;
client_max_body_size 100m;
location / {
proxy_pass http://127.0.0.1:8080;
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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
}
}
Если предпочитаете Caddy вместо связки nginx + certbot — апгрейд соединений и сертификат он получает автоматически, конфиг короче, но и у самого Caddy за реальным доменом хватает своих нюансов — они разобраны в частых ошибках Caddy с авто-SSL на сервере.
Вложения, аватары и обложки задач не загружаются
Plane хранит файлы (вложения к задачам, аватары, обложки, экспорт) в S3-совместимом хранилище — по умолчанию это встроенный контейнер MinIO. Типичный симптом: загрузка файла зависает на спиннере или падает с ошибкой 403 Forbidden/SignatureDoesNotMatch, хотя сама задача сохраняется нормально.
Причина 1 — публичный и внутренний адрес MinIO не совпадают. Backend генерирует presigned URL для загрузки файла напрямую из браузера в MinIO, и если AWS_S3_ENDPOINT_URL в .env указывает на внутренний docker-адрес (http://plane-minio:9000), а браузер физически не может достучаться до этого адреса снаружи, загрузка не пройдёт. Для внешнего доступа нужен либо публично доступный MinIO за собственным доменом/поддоменом, либо явный проброс через reverse-proxy с сохранением пути /uploads.
Причина 2 — bucket не создан или credentials не совпадают. Проверьте, что бакет действительно существует и переменные AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_S3_BUCKET_NAME одинаковы во всех сервисах, которые их используют (api, worker):
docker compose exec plane-minio mc alias set local http://localhost:9000 <access_key> <secret_key>
docker compose exec plane-minio mc ls local/
Подробнее про начальную настройку самого MinIO — в гайде по установке MinIO на VPS.
Причина 3 — лимит на размер запроса у reverse-proxy. Если вы проксируете загрузку файлов через свой nginx, дефолтный лимит в 1 МБ режет любое вложение крупнее. Директива client_max_body_size 100m; из конфига выше решает это для большинства случаев — увеличивайте дальше, если реально грузите файлы весом в сотни мегабайт.
Если вместо встроенного MinIO вы используете внешний S3-совместимый провайдер, симптомы те же, а проверять нужно AWS_REGION и корректность AWS_S3_ENDPOINT_URL для конкретного провайдера — у части из них endpoint отличается от привычного AWS-формата.
Приглашения по email не приходят участникам команды
Создание workspace и первого администратора работает без почты — она нужна только для приглашения новых участников и уведомлений. Если после отправки приглашения человек не получает письмо, а в интерфейсе просто крутится «Invitation sent», проверяйте цепочку от простого к сложному:
- SMTP вообще не настроен. Дефолтный
.envиз репозитория оставляетEMAIL_HOSTпустым — в этом случае Plane тихо проглатывает ошибку отправки, ничего не показывая в интерфейсе. Задайте реальные параметры:
EMAIL_HOST=smtp.yourprovider.com
EMAIL_PORT=587
EMAIL_HOST_USER=notifications@example.com
EMAIL_HOST_PASSWORD=change_me
EMAIL_USE_TLS=1
EMAIL_FROM=Plane <notifications@example.com>
После правки .env пересоздайте api и worker — само письмо на приглашение отправляется фоновой задачей Celery, а не синхронно из api.
- Worker не обрабатывает очередь. Отправка почты в Plane асинхронная — если контейнер
workerупал или не запускался, приглашение зависнет в очереди навсегда без видимой ошибки пользователю. Проверьте:
docker compose ps worker beat-worker
docker compose logs worker --tail=50
- Провайдер режет исходящий SMTP. Если письма не уходят даже с правильными EMAIL_-переменными, вероятная причина не в Plane, а в порте 25/587, который многие облачные провайдеры блокируют по умолчанию для новых VPS — тогда помогает либо запрос на разблокировку у провайдера, либо переход на внешний транзакционный сервис (SendGrid, Mailgun, Postmark) через тот же
EMAIL_HOST.
Обновление версии и бэкап без потери данных
Community-версия Plane развивается быстро, и между релизами иногда меняется схема базы — обновление «просто новым образом» без плана отката может закончиться половиной применённых миграций.
Перед любым обновлением — снимайте дамп базы и архив файлового хранилища:
docker compose exec plane-db pg_dump -U ${PGUSER} ${PGDATABASE} > plane_$(date +%F).sql
docker compose exec plane-minio tar -czf - -C /export . > plane_minio_$(date +%F).tar.gz
Не полагайтесь на снапшот диска у провайдера как единственную страховку — на живой базе снапшот может зафиксировать несогласованное состояние между Postgres и MinIO, если они не остановлены синхронно. Для регулярных дампов на удалённое хранилище с ротацией и шифрованием пригодится обычный pg_dump по расписанию через cron, вынесенный за пределы сервера с самим приложением.
Само обновление:
./install.sh upgrade
или для ручного compose — docker compose pull && docker compose up -d, после чего обязательно проверьте логи migrator, как в первом разделе: контейнер должен выйти с кодом 0 до того, как api начнёт принимать трафик.
Если между вашей версией и целевой пропущено несколько релизов — переходите поэтапно, а не сразу на последний тег: миграции пишутся с расчётом на предыдущую версию схемы, и большой прыжок повышает риск упасть посередине с базой в промежуточном состоянии, откуда откат образа уже не поможет — потребуется восстановление из дампа.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Сколько ресурсов реально нужно для команды из 5–10 человек?
Ориентировочно 2 vCPU и 4 ГБ RAM — это минимум, при котором весь стек (два-три Next.js-фронтенда, Django API, Celery worker и beat, Postgres, Redis, MinIO) стартует и не падает по памяти. С ростом числа задач и вложений закладывайте запас на диск под MinIO и на RAM под Postgres — конкретные цифры сильно зависят от объёма данных, ориентируйтесь по факту через docker stats.
Можно ли подключить Plane к уже существующему внешнему PostgreSQL и Redis вместо встроенных контейнеров?
Да, стандартный способ — заменить plane-db/plane-redis в compose на переменные DATABASE_URL/REDIS_URL, указывающие на внешние сервисы. Удобно, если под управляемую базу уже настроены бэкапы и мониторинг отдельно от приложения.
Почему после установки не получается зайти под первым администратором?
Чаще всего причина в том, что домен в WEB_URL/APP_DOMAIN не совпадает с тем, откуда вы реально открываете интерфейс — фронтенд шлёт запрос авторизации не туда, куда backend ждёт, и получает CORS-отказ вместо логина. Проверьте раздел про белый экран выше.
Что делать, если migrator падает даже при чистой установке на пустой базе?
Проверьте версию образа Postgres — Plane тестируется на конкретной мажорной версии (обычно 15.x), и если вы указали в compose произвольную более новую или более старую версию postgres, часть миграций может вести себя иначе. Возьмите версию образа из официального docker-compose.yml репозитория как есть, не заменяя её на "более свежую".
Нужен ли отдельный сервер под Plane, или можно на одном VPS с другими сервисами?
Можно, но закладывайте ресурсы отдельно под каждый стек и разводите порты/сети явно — на общем nginx-хосте главный источник проблем именно конфликт портов 80/443 у встроенного proxy-контейнера Plane с уже работающими сайтами, разобранный в разделе про белый экран.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →