SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-28

Установка Planka через Docker Compose на свой VPS

Разверните Planka с базой Postgres и Traefik. В статье разобраны критические переменные окружения, настройка BASE_URL и решение ошибки входа, возникающей при неверном адресе.

Что вы получаете при самостоятельном хостинге Planka

Самостоятельный хостинг Planka предоставляет вашей команде Kanban-доску с моделью карточек, списков и меток, знакомой пользователям Trello, которая работает на подконтрольном вам VPS. Здесь нет ограничений на количество мест и нет оплаты за каждого пользователя, так как единственные затраты — это сервер. В этом руководстве описывается развертывание с помощью Docker Compose за Traefik, использование Postgres для данных и именованного тома для всех загружаемых пользователями файлов.

Это руководство предназначено для команд из двух-пяти человек, которые переходят с бесплатного тарифа Trello. Если вы еще выбираете систему для управления задачами, сначала прочитайте сравнение альтернатив Trello для self-hosting. Данное руководство предполагает, что выбор уже сделан, и охватывает только процесс развертывания.

Вам потребуется VPS с установленным Docker Engine и плагином Compose, а также DNS A-запись, указывающая на этот сервер. Также необходимо, чтобы на сервере уже был настроен экземпляр Traefik, выполняющий TLS termination (transport layer security). Если Traefik еще не настроен, сначала выполните настройку обратного прокси-сервера Traefik перед несколькими приложениями Compose, а если приведенный ниже файл кажется незнакомым, прочитайте основы Docker Compose для VPS.

Сколько ресурсов VPS требуется для Planka?

Проект не публикует минимальные системные требования, поэтому любые цифры стоит рассматривать как отправную точку, а не как точное измерение. Значения 2 vCPU и 4 GB RAM, которые часто встречаются на страницах хостинг-провайдеров, — это стандартный комфортный минимум, а не результат замеров разработчиков. Для доски, с которой работают пять человек, это избыточный объем.

Фактически запускаются легкие процессы: один процесс Node.js, обслуживающий API и собранный фронтенд, и один процесс Postgres для хранения данных. Третий небольшой прокси-процесс работает внутри контейнера Planka для фильтрации исходящих запросов. Тариф с 1 vCPU и 2 GB RAM справится с нагрузкой от двух до пяти пользователей, при этом большая часть свободной памяти будет использоваться под кэш Postgres. Доска — «легкий» сосед, поэтому если на этом же VPS вы планируете хранить документы команды, сначала рассчитайте ресурсы для того приложения: запуск AFFiNE в качестве рабочего пространства в стиле Notion требует пару гигабайт оперативной памяти еще до того, как Planka начнет потреблять свои ресурсы.

Рассчитывайте объем диска до того, как определитесь с памятью, так как именно вложения занимают больше всего места. Проведите замеры на собственном экземпляре, вместо того чтобы полагаться на этот текст:

docker stats --no-stream
docker system df -v

Первая команда выводит текущее потребление памяти и CPU для каждого контейнера. Вторая показывает объем данных в каждом томе. Снимайте оба показателя после полноценной рабочей недели, а не в день установки, так как простаивающая доска не дает представления о реальной нагрузке вашей команды.

Создание файла Compose

Создайте каталог и назначьте его владельцем нужного пользователя, чтобы вам не приходилось редактировать эти файлы через sudo.

sudo mkdir -p /opt/planka
sudo chown "$USER":"$USER" /opt/planka
cd /opt/planka

Сгенерируйте секреты в файл .env рядом с файлом Compose. Compose автоматически считывает этот файл и подставляет значения.

umask 077
{
  printf 'SECRET_KEY=%s\n' "$(openssl rand -hex 64)"
  printf 'POSTGRES_PASSWORD=%s\n' "$(openssl rand -hex 24)"
  printf 'ADMIN_PASSWORD=%s\n' "$(openssl rand -hex 12)"
} > .env
chmod 600 .env

Использование openssl rand -hex выбрано намеренно. Шестнадцатеричная строка содержит только цифры и буквы от a до f, поэтому она не может нарушить строку подключения DATABASE_URL, в которую вставляется. Пароль в формате base64, содержащий косую черту или символ «собаки», вызывает ошибку подключения, которая выглядит как неверное имя хоста, что отнимет у вас час времени на отладку. Более общий подход описан в хранении секретов вне файла Compose.

Теперь выполните docker-compose.yml. Замените kanban.example.com на ваше имя хоста в обоих местах, где оно встречается.

services:
  planka:
    image: ghcr.io/plankanban/planka:2.1.1
    restart: unless-stopped
    volumes:
      - planka-data:/app/data
    environment:
      - BASE_URL=https://kanban.example.com
      - DATABASE_URL=postgresql://planka:${POSTGRES_PASSWORD}@postgres/planka
      - SECRET_KEY=${SECRET_KEY}
      - TRUST_PROXY=true
      - DEFAULT_ADMIN_EMAIL=you@example.com
      - DEFAULT_ADMIN_PASSWORD=${ADMIN_PASSWORD}
      - DEFAULT_ADMIN_NAME=Your Name
      - DEFAULT_ADMIN_USERNAME=admin
    networks:
      - proxy
      - internal
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy"
      - "traefik.http.routers.planka.rule=Host(`kanban.example.com`)"
      - "traefik.http.routers.planka.entrypoints=websecure"
      - "traefik.http.routers.planka.tls.certresolver=default"
      - "traefik.http.services.planka.loadbalancer.server.port=1337"
    depends_on:
      postgres:
        condition: service_healthy

  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=planka
      - POSTGRES_USER=planka
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    networks:
      - internal
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U planka -d planka"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  planka-data:
  db-data:

networks:
  proxy:
    external: true
  internal:

Четыре решения в этом файле требуют пояснения, так как именно их пользователи часто меняют, а затем жалеют об этом.

  • В сервисе Planka отсутствует блок ports:. Traefik обращается к контейнеру через сеть proxy, поэтому порт 1337 никогда не публикуется на хосте. Публикация порта позволила бы любому обойти ваш прокси и сертификат.
  • loadbalancer.server.port=1337 указывает порт внутри контейнера. Planka слушает порт 1337, а в примере выше обращение идет через 3000 только потому, что там выполняется маппинг порта на хост. Здесь маппинга нет, поэтому Traefik должен знать порт контейнера.
  • condition: service_healthy работает в связке с проверкой работоспособности (healthcheck) Postgres. Без этого параметра Planka запускается раньше, чем база данных начинает принимать соединения, выполняет первый запрос с ошибкой и завершается, что выглядит как цикл перезагрузок (crash loop). Механика процесса описана в проверках работоспособности и порядке запуска в Compose.
  • Сервис базы данных намеренно назван postgres. Planka 2 направляет свои исходящие запросы через внутренний фильтр, чей список блокировки по умолчанию включает localhost,postgres. Переименуйте сервис, и вы незаметно исключите свою базу данных из этого списка.

Перед запуском убедитесь, что Compose видит ваши секреты:

docker compose config | grep -E 'image:|BASE_URL|POSTGRES_USER'

Эта команда выведет содержимое файла с уже подставленными значениями .env. Пустое значение означает, что Compose не считывает файл .env, обычно это происходит, если вы запускаете команду из другого каталога.

Что на самом деле делают переменные начальной настройки администратора

Начиная с версии Planka 1.13, администратор не создается автоматически, поэтому в новой базе данных нет пользователей, которые могли бы войти в систему. Группа переменных DEFAULT_ADMIN_* — это один из двух способов решить эту проблему.

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

Переменная DEFAULT_ADMIN_EMAIL выполняет вторую функцию, которая часто сбивает пользователей с толку. Пока эта переменная задана, учетную запись с указанным именем нельзя изменить или удалить через интерфейс. Это защитный механизм, предотвращающий блокировку доступа; именно поэтому вы не можете переименовать эту учетную запись или изменить её адрес электронной почты в UI. Удалите переменную и перезапустите контейнер — после этого учетная запись станет обычным администратором, которого можно редактировать как любого другого пользователя.

Будьте осторожны с полем пароля. Любой, кто может выполнить docker inspect в контейнере, способен прочитать содержимое environment:, поэтому DEFAULT_ADMIN_PASSWORD не должна оставаться там постоянно. Войдите в систему, измените пароль через интерфейс, удалите строку с паролем из конфигурации, а затем снова выполните docker compose up -d.

Более чистый способ позволяет полностью обойтись без этих переменных. Закомментируйте всю группу DEFAULT_ADMIN_*, а затем создайте учетную запись в интерактивном режиме:

docker compose run --rm planka npm run db:create-admin-user

Система запросит адрес электронной почты, пароль, отображаемое имя и (опционально) имя пользователя, после чего запишет данные напрямую в базу данных. Пароль при этом не попадает в файл Compose или переменные окружения контейнера. Используйте этот метод, если доступ к оболочке (shell) на VPS есть у нескольких человек. Команда сначала запускает Postgres из-за depends_on, поэтому она работает даже на стеке, который еще ни разу не был запущен.

Любой из этих способов предполагает ручное управление паролями в Planka. Если это уже четвертая учетная запись, которую приходится хранить вашей команде, Planka может делегировать авторизацию OIDC-провайдеру, например, Authentik, работающему как ваш собственный сервер единого входа (SSO). В этом случае первичный администратор останется «аварийной» учетной записью на случай сбоя провайдера.

Почему BASE_URL нарушает работу входа в систему, если он не совпадает с именем хоста

BASE_URL — это точный адрес, который пользователи вводят в браузере, включая схему и без косой черты в конце. Для данного стека это https://kanban.example.com. Planka формирует собственные ссылки и WebSocket-соединение на основе этого значения, поэтому неверный BASE_URL не выдает явную ошибку. Вместо этого страница загружается, но процесс загрузки никогда не завершается.

Типичный сценарий: вы копируете пример из upstream, оставляете BASE_URL=http://localhost:3000 как есть и заходите на сайт по HTTPS через свой реальный домен. Форма входа отправляет данные, учетные данные принимаются, но доска не отображается. Откройте консоль разработчика в браузере, и вы увидите, что запросы к /socket.io/ завершаются ошибкой, так как клиенту было указано открыть активное соединение с localhost:3000, а на вашем компьютере этот адрес никуда не ведет.

TRUST_PROXY=true — это вторая часть той же проблемы. Planka находится за Traefik, поэтому каждый запрос поступает от прокси по обычному HTTP внутри сети Docker. Без TRUST_PROXY приложение игнорирует заголовки X-Forwarded-Proto и X-Forwarded-For, которые устанавливает Traefik, поэтому оно считает соединение небезопасным и воспринимает всех клиентов как один общий IP-адрес. Если параметр задан, приложение считывает эти заголовки и «соглашается» с браузером относительно используемой схемы.

Traefik проксирует WebSockets без дополнительной настройки, что является одной из причин предпочесть его в данном случае. В nginx для socket.io требуется собственный блок location с параметрами proxy_set_header Upgrade $http_upgrade и proxy_set_header Connection "upgrade", иначе вы получите тот же бесконечный индикатор загрузки, но по другой причине.

Перенос доски на новое имя хоста в будущем требует одновременного изменения двух параметров: значения BASE_URL и правила Host() в Traefik. Если изменить один и забыть про другой, вы снова увидите индикатор загрузки. Развертывание Planka по подпути, например https://example.com/planka, работает начиная с версии 2.1.0, выпущенной в марте 2026 года. Для более старых версий используйте отдельный поддомен.

Где Planka хранит вложения и аватары

Planka 2 хранит все пользовательские загрузки по одному пути внутри контейнера: /app/data. Вложения, аватары пользователей и фоновые изображения досок находятся в этой директории. В версии 1 использовались три отдельные папки, поэтому Compose-файл, скопированный из старого руководства, монтирует пути, которых больше не существует, а реальная директория с данными остается несмонтированной.

Это единственное монтирование определяет разницу между доской, которая переживет обновление, и испорченным днем. Если /app/data не находится на томе (volume), загрузки попадают в записываемый слой контейнера. Этот слой уничтожается при пересоздании контейнера, а контейнер пересоздается каждый раз при смене тега образа. Доска после этого выглядит нормально, карточки на месте, но все ссылки на вложения не работают, так как записи в базе данных по-прежнему указывают на файлы, которых больше нет.

Именованный том в приведенном выше Compose-файле предотвращает эту проблему. Bind mount также подходит и упрощает резервное копирование файлов стандартными инструментами, но требует одного дополнительного шага. Процесс Node внутри контейнера работает от имени UID 1000, поэтому директория на хосте, принадлежащая root, приведет к ошибке прав доступа при первой же загрузке:

sudo chown -R 1000:1000 /opt/planka/data

Компромисс между этими двумя вариантами разобран в bind mounts против именованных томов.

Если объем вложений превышает доступное место на диске, Planka может записывать их в S3-совместимое хранилище через S3_ENDPOINT, S3_BUCKET и соответствующие переменные с ключами. Это может быть облачный бакет или собственное объектное хранилище MinIO на другом сервере. Определитесь с этим до того, как команда начнет заполнять доску, так как настройки применяются только к новым загрузкам.

Запуск стека и проверка работоспособности

docker compose pull
docker compose up -d
docker compose ps

docker compose ps должен показать postgres как healthy, а planka как running. Если Planka перезапускается в цикле, в первую очередь следует проверять подключение к базе данных, а не само приложение.

docker compose logs -f planka

При нормальном первом запуске выполняются миграции базы данных, после чего сервер сообщает о прослушивании порта 1337. Убедитесь, что схема действительно создана, обратившись напрямую к Postgres, вместо того чтобы полагаться только на логи:

docker compose exec postgres psql -U planka -d planka -c '\dt'

Список таблиц, включающий board и card, означает, что миграции прошли успешно. Сообщение "Did not find any relations" означает, что Planka не смогла подключиться; в этом случае сравните DATABASE_URL со значениями POSTGRES_USER и POSTGRES_PASSWORD в вашем .env.

Затем проверьте маршрут со своей локальной машины, а не с VPS:

curl -I https://kanban.example.com

HTTP/2 200 означает, что Traefik получил сертификат и успешно связывается с контейнером. Ошибка 404, возвращаемая Traefik, означает, что метки маршрутизатора не совпали; чаще всего это происходит из-за того, что контейнер не подключен к сети proxy. Теперь откройте сайт и войдите в систему под учетной записью администратора.

Выполняйте pg_dump перед каждым обновлением версии

Ваша доска использует два отдельных хранилища, поэтому резервная копия должна охватывать оба: базу данных Postgres и том planka-data. Выполняйте дамп базы данных, пока стек запущен.

docker compose exec -T postgres pg_dump -U planka -d planka > "planka-db-$(date +%F).sql"

Флаг -T обязателен. Без него Compose выделяет псевдотерминал, а терминальный уровень перезаписывает символы конца строки в потоке. В результате вы получите файл дампа, который будет поврежден при восстановлении. Ошибка проявится спустя недели, в самый неподходящий момент.

Затем сохраните загружаемые файлы. Сначала определите реальное имя тома, так как Compose добавляет к нему префикс в виде имени директории проекта.

docker volume ls | grep planka
docker run --rm -v planka_planka-data:/data -v "$PWD":/backup alpine \
  tar czf /backup/planka-files-$(date +%F).tgz -C /data .

Проект также содержит docker-backup.sh и docker-restore.sh в своем репозитории, а официальная документация рекомендует запускать их по ночному расписанию cron. Оба подхода допустимы. Недопустимо иметь резервную копию, которую вы никогда не восстанавливали. Восстановите её на тестовом VPS хотя бы раз и убедитесь, что можете войти в систему и открыть вложение. Та же пара хранилищ встречается в каждом приложении Compose, которое принимает загрузки. Поэтому, если позже вы разместите Chatwoot на том же сервере, что и вашу службу поддержки, отработанная здесь процедура будет применима с минимальными изменениями имен томов.

Выполняйте дамп непосредственно перед каждым изменением версии. Резервная копия, сделанная вчера ночью, не эквивалентна копии, сделанной перед миграцией, которую вы собираетесь запустить.

Фиксация тегов и чтение примечаний к выпуску

Оба тега образов в этом файле зафиксированы намеренно.

ghcr.io/plankanban/planka:2.1.1 — это конкретный выпуск, актуальный на август 2026 года. latest меняется каждый раз, когда разработчики публикуют обновление, поэтому обычная команда docker compose pull может привести к миграции схемы базы данных в самый неподходящий момент. Прочитайте примечания к выпуску перед тем, как менять этот номер, так как именно там описаны критические изменения и исправления безопасности. Версия 2.0.3 была выпущена как обновление безопасности — это именно тот случай, когда лучше ознакомиться с информацией заранее, чем столкнуться с последствиями случайно. Фиксация версий здесь проста, так как разработчики публикуют образы. Если проект не поставляет готовые образы, вам следует придерживаться той же дисциплины с дополнительным шагом, как в случае с сборкой openGym из checkout-версии git-тега.

postgres:16-alpine зафиксирован на мажорной версии по более веской причине. Postgres записывает каталог данных в формате, привязанном к мажорной версии, и сервер отказывается открывать каталог, созданный другой версией. Укажите postgres:latest, позвольте тегу обновиться до 17, и контейнер не запустится:

FATAL:  database files are incompatible with server
DETAIL:  The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17.

Данные не теряются, и перезапуск проблему не решит. Переход на новую мажорную версию Postgres требует создания дампа из старой версии и восстановления в чистый каталог данных новой версии. Это плановая задача, выполняемая при остановленном стеке, а не побочный эффект обновления образа.

Если вы переносите существующую установку Planka 1.x, а не начинаете с нуля, для этого обновления предусмотрена собственная процедура, описанная в документации проекта. Откат к версии 1 невозможен без предварительно созданной резервной копии.

Типичные сбои и сообщения об ошибках

Planka перезапускается в цикле, а в логах упоминается база данных. Учетные данные в DATABASE_URL не совпадают с переменными окружения Postgres. Учтите, что POSTGRES_PASSWORD применяется только при первой инициализации каталога данных, поэтому изменение переменной после неудачного первого запуска ничего не даст. Необходимо удалить том db-data и начать заново.

Вход выполнен, но доска не загружается. Значение BASE_URL не совпадает с адресом в строке браузера, либо отсутствует TRUST_PROXY. В консоли браузера видны неудачные запросы к /socket.io/.

Загрузка файлов не работает, остальное функционирует нормально. Точка монтирования (bind mount) принадлежит пользователю root. Выполните sudo chown -R 1000:1000 для каталога на хосте и перезапустите контейнер.

Вложения исчезли после обновления. Каталог /app/data не был вынесен на том, поэтому файлы находились в слое контейнера, который был заменен при обновлении. Восстановите файлы из резервной копии, затем добавьте том, прежде чем снова менять тег образа.

Traefik возвращает 404. Контейнер не подключен к сети proxy, либо правило Host() не соответствует вашей DNS-записи. Команда docker compose config показывает метки после подстановки переменных — именно там становятся видны опечатки.

Уведомления или вебхуки не приходят. Planka 2 отправляет исходящие HTTP-запросы через внутренний фильтр, и список блокировки по умолчанию включает localhost и postgres. Вебхук, направленный на другой контейнер на том же хосте, может быть заблокирован по умолчанию. Отредактируйте OUTGOING_ALLOWED_HOSTS вместо отключения фильтра.

После запуска нагрузка на обслуживание минимальна. Следите за примечаниями к выпуску (release notes) и делайте дамп базы данных перед каждым обновлением. Перезагрузка сервера автоматически поднимает стек благодаря restart: unless-stopped, если служба Docker включена в автозагрузку. В статье Compose-стеки, которые восстанавливаются после перезагрузки описаны случаи, когда этого не происходит.

FAQ

Почему Planka бесконечно загружается после входа в систему?

Учетные данные были приняты, но соединение в реальном времени — нет. Planka формирует URL для WebSocket на основе BASE_URL. Если эта переменная по-прежнему содержит http://localhost:3000, а вы заходите на сайт по адресу https://kanban.example.com, браузер пытается открыть сокет по адресу, которого нет на вашей машине. В консоли разработчика будут видны неудачные запросы к /socket.io/. Установите BASE_URL в точности как публичный адрес без косой черты в конце, добавьте TRUST_PROXY=true, чтобы приложение учитывало заголовок X-Forwarded-Proto от вашего reverse proxy, а затем выполните docker compose up -d.

Как создать первого администратора в Planka?

Начиная с версии 1.13 администратор не создается автоматически. Либо установите DEFAULT_ADMIN_EMAIL вместе с соответствующими переменными для пароля, имени и имени пользователя и запустите стек, либо выполните docker compose run --rm planka npm run db:create-admin-user и ответьте на вопросы. Интерактивная команда безопаснее на общем сервере, так как пароль не попадает в среду контейнера, где его может прочитать docker inspect. Если оставить DEFAULT_ADMIN_EMAIL установленной после этого, учетная запись будет заблокирована для редактирования и удаления через интерфейс.

Где Planka хранит вложения и аватары?

В Planka 2 все загруженные файлы находятся по пути /app/data внутри контейнера, включая вложения, аватары пользователей и фоны досок. Примонтируйте этот путь к именованному тому. Если он не примонтирован, файлы остаются в записываемом слое контейнера и будут удалены при следующем пересоздании контейнера, что происходит при каждом обновлении образа. Bind mount также подходит, но процесс Node работает от имени UID 1000, поэтому выполните sudo chown -R 1000:1000 для директории на хосте, иначе загрузка завершится ошибкой доступа.

Сколько оперативной памяти нужно для self-hosted Planka?

Проект не устанавливает минимальных системных требований. Цифры 2 vCPU и 4 GB, которые встречаются на страницах хостинг-провайдеров, являются стандартными настройками, а не результатами измерений, и для небольшой доски это избыточно. Вся нагрузка состоит из одного процесса Node и одного процесса Postgres, поэтому плана с 1 vCPU и 2 GB хватит для команды из двух-пяти человек. Выполните docker stats --no-stream после обычной рабочей недели и подбирайте ресурсы на основе собственных показателей. Следите за диском внимательнее, чем за памятью, так как именно вложения занимают место.

Как обновить Planka без потери данных?

Сделайте дамп базы данных и заархивируйте том с загрузками непосредственно перед обновлением, а не по расписанию за прошлую ночь. Используйте docker compose exec -T postgres pg_dump -U planka -d planka > planka-db.sql, сохраняя -T, чтобы псевдотерминал не повредил перенаправленный вывод. Прочитайте примечания к выпуску для каждой версии, которую вы пропускаете, измените тег образа на конкретный релиз вместо latest, затем выполните docker compose pull и docker compose up -d и следите за логом миграции. Оставьте тег Postgres зафиксированным на его мажорной версии, так как сервер откажется открывать директорию данных, созданную другой мажорной версией.