Як розгорнути Planka на VPS через Docker Compose
Покрокове розгортання Planka на VPS: Postgres, Traefik, змінні для першого адміністратора та BASE_URL, через яку можуть не працювати входи.
Що ви отримуєте, розгортаючи Planka на власному сервері
Власний хостинг Planka дає вашій команді Kanban-дошку з моделлю карток, списків і міток, знайомою користувачам Trello, але запущену на VPS під вашим контролем. Обмежень на кількість користувачів немає, і плата за кожного користувача не стягується, оскільки єдина витрата — це сервер. У цьому посібнику Planka розгортається за допомогою Docker Compose за Traefik. Для даних використовується Postgres, а для кожного завантаженого файла — іменований volume.
Цей посібник призначений для команди з двох-п’яти людей, яка переходить із безкоштовного тарифу Trello. Якщо ви ще вирішуєте, яку дошку використовувати, спочатку прочитайте порівняння self-hosted альтернатив Trello. У цьому посібнику вибір уже зроблено, тому розглядається лише розгортання.
Вам потрібен VPS із Docker Engine і Compose plugin, а також DNS A-запис, що вказує на цей сервер. Крім того, на сервері вже має працювати екземпляр Traefik, який завершує TLS (transport layer security). Якщо Traefik ще не налаштований, спочатку налаштуйте reverse proxy Traefik перед кількома застосунками Compose, а якщо наведений нижче файл незрозумілий, прочитайте основи Docker Compose для VPS.
Скільки ресурсів VPS потребує Planka?
Проєкт не публікує мінімальні вимоги до обладнання, тому сприймайте будь-які наведені цифри як початкову оцінку, а не як виміряне значення. Значення 2 vCPU і 4 GB, яке повторюють на сторінках хостинг-провайдерів, є комфортним типовим варіантом провайдера, а не вимогою, визначеною вимірюваннями проєкту. Для дошки, якою користуються п’ятеро людей, це надлишковий запас.
Фактично запускаються невеликі компоненти: один процес Node.js обслуговує API і зібраний frontend, а один процес Postgres зберігає дані. Усередині контейнера Planka також працює ще один невеликий proxy-процес, який фільтрує вихідні запити. План із 1 vCPU і 2 GB достатній для дошки, якою користуються від двох до п’яти людей. Більша частина вільної пам’яті зазвичай використовується як кеш Postgres. Дошка створює невелике навантаження, тому якщо на тому самому VPS мають також зберігатися документи вашої команди, спочатку визначайте розмір VPS за вимогами цього застосунку: запуск AFFiNE як робочого простору в стилі Notion потребує кількох гігабайтів лише для себе, перш ніж Planka почне використовувати ресурси.
Спочатку визначте потрібний обсяг диска, а вже потім обсяг пам’яті, оскільки саме вкладення зростають з часом. Замість того щоб покладатися на цей абзац, виміряйте власний екземпляр:
docker stats --no-stream
docker system df -vПерша команда виводить поточне використання пам’яті та CPU для кожного контейнера. Друга показує, скільки місця займає кожен volume. Виконайте обидва вимірювання після звичайного робочого тижня, а не в день встановлення, оскільки неактивна дошка нічого не скаже про навантаження вашої команди.
Напишіть файл 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 .envopenssl rand -hex використано навмисно. Шістнадцятковий рядок містить лише цифри та літери від a до f, тому він не може пошкодити рядок підключення DATABASE_URL, у який його підставляють. Пароль у форматі base64 зі скісною рискою або символом at спричиняє помилку підключення, яка виглядає як неправильне ім’я хоста. На пошук такої помилки можна витратити годину. Загальний підхід описано в матеріалі як не зберігати секрети у файлі 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 запускається до того, як база даних починає приймати підключення, завершує роботу після першого запиту, і це виглядає як цикл падіння та перезапуску. Механізм описано в матеріалі healthcheck у 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. Якщо такого користувача немає, Planka створює його, використовуючи пароль, відображуване ім’я та ім’я користувача, задані поруч із цією змінною. Це відбувається під час першого запуску з порожньою базою даних, тому ці змінні створюють початковий обліковий запис, а не керують ним.
DEFAULT_ADMIN_EMAIL виконує ще одну функцію, про яку часто не знають. Поки змінна задана, ніхто не може редагувати або видалити вказаний нею обліковий запис через інтерфейс. Це захист від блокування доступу. Саме тому в інтерфейсі не можна перейменувати цей обліковий запис або змінити його адресу електронної пошти. Видаліть змінну й перезапустіть Planka — після цього обліковий запис стане звичайним адміністратором, якого можна редагувати на загальних підставах.
Рядок із паролем потребує особливої уваги. Усе, що міститься в environment:, може прочитати кожен, хто має змогу виконати docker inspect у контейнері, тому 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, який працює як ваш власний сервер єдиного входу. Початковий обліковий запис адміністратора при цьому слід залишити як аварійний обліковий запис на випадок недоступності провайдера.
Чому BASE_URL ламає вхід, якщо не відповідає імені хоста
BASE_URL — це точна адреса, яку користувачі вводять у браузері, зі схемою та без косої риски наприкінці. Для цього стека це https://kanban.example.com. Planka формує власні посилання та WebSocket-з’єднання на основі цього значення. Тому неправильний BASE_URL не спричиняє зрозумілої помилки. Сторінка завантажується, але процес завантаження не завершується.
Типовий випадок: ви копіюєте приклад від розробника, залишаєте BASE_URL=http://localhost:3000 без змін і відкриваєте сайт через HTTPS на своєму реальному домені. Форма входу надсилається, а облікові дані приймаються. Дошка не з’являється. Відкрийте консоль розробника браузера. Ви побачите невдалі запити до /socket.io/, оскільки клієнту повідомили, що він має відкрити активне з’єднання з localhost:3000, а на вашому ноутбуці цієї адреси взагалі немає.
TRUST_PROXY=true — інша частина тієї самої проблеми. Planka працює за Traefik, тому кожен запит надходить до нього з адреси проксі через незашифрований HTTP у Docker network. Без TRUST_PROXY застосунок ігнорує заголовки X-Forwarded-Proto і X-Forwarded-For, які встановлює Traefik. Через це він вважає з’єднання незахищеним і сприймає всіх клієнтів як одну спільну IP-адресу. Якщо цей параметр увімкнено, застосунок читає ці заголовки, і схема з’єднання в ньому відповідає схемі в браузері.
Traefik проксуює WebSocket без додаткового налаштування. Це одна з причин використовувати його в цьому випадку. У nginx для socket.io потрібен окремий блок location із параметрами proxy_set_header Upgrade $http_upgrade і proxy_set_header Connection "upgrade". Інакше ви побачите той самий завислий індикатор завантаження, але з іншої причини.
Якщо пізніше перенести дошку на нове ім’я хоста, потрібно одночасно змінити дві речі: значення BASE_URL і правило Traefik Host(). Якщо змінити лише одне з них, індикатор завантаження знову зависне. Обслуговування Planka з підшляху, наприклад https://example.com/planka, підтримується починаючи з версії 2.1.0, випущеної в March 2026. У старіших тегах використовуйте для Planka окремий субдомен.
Де Planka зберігає вкладення та аватари
Planka 2 зберігає все, що завантажує користувач, за одним шляхом усередині контейнера: /app/data. Там зберігаються вкладення, аватари користувачів і фонові зображення дошок. У версії 1 використовувалися три окремі каталоги, тому файл Compose зі старого матеріалу монтує шляхи, яких більше не існує, а фактичний каталог даних залишається не змонтованим.
Саме це визначає, чи переживе дошка оновлення без втрати даних. Якщо /app/data не розміщено на томі, завантажені файли потрапляють у доступний для запису шар контейнера. Цей шар видаляється під час повторного створення контейнера. Контейнер створюється заново щоразу, коли ви змінюєте тег образу. Дошка відкривається без видимих проблем, усі картки залишаються на місці, але кожне посилання на вкладення стає недійсним, оскільки рядки в базі даних усе ще посилаються на файли, яких більше немає.
Іменований том у наведеному вище файлі Compose запобігає цьому. Bind mount також працює та спрощує резервне копіювання файлів за допомогою звичайних інструментів, але потребує додаткового кроку. Процес Node усередині контейнера працює з UID 1000, тому каталог на хості, власником якого є root, спричинить помилку доступу під час першого завантаження:
sudo chown -R 1000:1000 /opt/planka/dataВідмінності між цими варіантами розглянуто в розділі bind mount порівняно з іменованими томами.
Якщо для вкладень не вистачає дискового простору у вашому тарифному плані, Planka може записувати їх у S3-сумісне сховище через S3_ENDPOINT, S3_BUCKET і відповідні змінні для ключів. Це може бути хмарний bucket або об’єктне сховище MinIO на власному сервері в іншій системі. Прийміть це рішення до того, як команда заповнить дошку, оскільки параметр застосовується лише до нових завантажень.
Запустіть стек і перевірте результат
docker compose pull
docker compose up -d
docker compose psdocker 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.comHTTP/2 200 означає, що Traefik отримав сертифікат і підключається до контейнера. Помилка 404, яку повертає Traefik, означає, що labels маршрутизатора не збігаються. Найчастіше це трапляється, коли контейнер не підключено до мережі 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 — це конкретний випуск, актуальний станом на August 2026. latest змінюється щоразу, коли upstream публікує новий випуск, тому звичайне docker compose pull може виконати міграцію схеми в момент, який ви не обирали. Прочитайте примітки до випуску, перш ніж змінювати це число. Саме там описано несумісні зміни та виправлення безпеки. Version 2.0.3 було опубліковано як випуск із виправленнями безпеки. Саме такі зміни потрібно прочитати, а не отримати випадково. У цьому випадку зафіксувати версію легко, оскільки upstream взагалі публікує образи. Якщо проєкт їх не публікує, потрібно дотримуватися такої самої дисципліни, додавши ще один крок, як у випадку з openGym, зібраним на сервері з checkout git-тега.
postgres:16-alpine зафіксовано на major version з важливішої причини. Postgres записує data directory у форматі, пов’язаному з major version, і сервер відмовляється відкривати directory, записаний іншою версією. Вкажіть 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.Дані не буде втрачено, і перезапуск також нічого не виправить. Перехід на нову major version Postgres потребує dump зі старої версії та restore у новий data directory на новій версії. Це запланована операція, яку виконують при зупиненому stack, а не побічний ефект завантаження образу.
Якщо ви переносите наявну інсталяцію Planka 1.x, а не встановлюєте систему з нуля, для такого оновлення в документації проєкту описано окрему процедуру. Повернутися до version 1 без попередньо створеної backup-копії неможливо.
Режими відмов і повідомлення, які ви побачите
Planka циклічно перезапускається, а в журналі згадується база даних. Облікові дані у DATABASE_URL не відповідають змінним середовища Postgres. Зверніть увагу: POSTGRES_PASSWORD застосовується лише під час першої ініціалізації каталогу даних, тому зміна змінної після невдалого першого запуску нічого не змінить. Потрібно видалити том db-data і запустити стек знову.
Вхід виконується успішно, але дошка не завантажується. BASE_URL не відповідає адресі в адресному рядку браузера або відсутній TRUST_PROXY. У консолі браузера відображаються невдалі запити до /socket.io/.
Завантаження файлів не працює, а все інше працює. Bind mount належить root. Виконайте sudo chown -R 1000:1000 для каталогу на хості та перезапустіть контейнер.
Вкладення зникли після оновлення. /app/data не було розміщено на томі, тому файли зберігалися у шарі контейнера, який замінило оновлення. Відновіть файли з резервної копії, а потім додайте том, перш ніж знову змінювати тег image.
Traefik повертає 404. Контейнер не підключений до мережі proxy або правило Host() не відповідає вашому DNS-запису. docker compose config показує labels після підстановки, і саме там стають видимими помилки в написанні.
Сповіщення або webhooks не надходять. Planka 2 надсилає вихідні HTTP-запити через внутрішній фільтр, а стандартний список блокування охоплює localhost і postgres. Webhook до іншого контейнера на тому самому хості може бути заблокований навмисно. Змініть OUTGOING_ALLOWED_HOSTS, а не видаляйте фільтр.
Після запуску обслуговування стека не потребує значних зусиль. Стежте за release notes і створюйте дамп бази даних перед кожним оновленням. Після перезавантаження стек запускається самостійно завдяки restart: unless-stopped, якщо сам Docker service увімкнений під час завантаження системи, а в розділі 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 разом із відповідними змінними для пароля, імені та username і запустіть stack, або виконайте docker compose run --rm planka npm run db:create-admin-user та дайте відповіді на запити. Інтерактивна команда безпечніша на спільному сервері, оскільки пароль не потрапляє в середовище контейнера, де його може прочитати docker inspect. Якщо залишити DEFAULT_ADMIN_EMAIL заданою надалі, цей обліковий запис буде захищено від редагування та видалення через інтерфейс.
Де Planka зберігає вкладення та аватари?
У Planka 2 усі завантажені файли, зокрема вкладення, аватари користувачів і фонові зображення дошок, зберігаються в контейнері за шляхом /app/data. Підключіть цей шлях до named volume. Якщо volume не змонтовано, файли залишаються у writable layer контейнера та будуть знищені під час наступного повторного створення контейнера. Це відбувається під час кожного оновлення image. Bind mount також підходить, але Node process працює від UID 1000. Тому виконайте sudo chown -R 1000:1000 для директорії на host, інакше завантаження завершаться помилкою прав доступу.
Скільки RAM потрібно self-hosted Planka?
Проєкт не публікує мінімальних вимог до обладнання. Значення 2 vCPU і 4 GB, яке часто наводять на сторінках хостинг-провайдерів, є типовою конфігурацією провайдера, а не результатом вимірювань. Для невеликої дошки цього більше, ніж потрібно. Усе навантаження складається з одного Node process і одного Postgres process. Тому plan із 1 vCPU і 2 GB підходить для команди з двох-п’яти осіб. Виконайте docker stats --no-stream після звичайного тижня роботи й визначте ресурси на основі власних показників. Уважніше стежте за диском, ніж за пам’яттю, оскільки найбільше зростають вкладення.
Як оновити Planka без втрати даних?
Безпосередньо перед оновленням створіть дамп бази даних і заархівуйте uploads volume. Не покладайтеся на розклад минулої ночі. Використайте docker compose exec -T postgres pg_dump -U planka -d planka > planka-db.sql, зберігши -T, щоб pseudo-terminal не пошкодив перенаправлений вивід. Перегляньте release notes для кожної пропущеної версії, змініть image tag на конкретний release замість latest, потім виконайте docker compose pull і docker compose up -d та monitor журнал на наявність міграції. Не змінюйте Postgres tag за межами його major version, оскільки сервер відмовляється відкривати data directory, записану іншою major version.