Як розгорнути Planka на VPS через Docker Compose
Покрокове розгортання Planka на VPS: Postgres, Traefik, змінні bootstrap адміністратора та BASE_URL, через яку неправильне значення ламає вхід.
Що ви отримуєте, розгорнувши Planka на власному сервері
Розгортання Planka на власному сервері дає вашій команді Kanban-дошку з моделлю карток, списків і міток, яку користувачі вже знають за Trello, але на VPS під вашим контролем. Обмежень на кількість користувачів немає, і плата за кожного користувача не стягується, оскільки єдина витрата — це сервер. У цьому посібнику Planka розгортається за допомогою Docker Compose за Traefik, використовується Postgres для зберігання даних і окремий named volume для кожного завантаженого файла.
Цей посібник розрахований на команду з двох-п’яти людей, яка переходить із безкоштовного тарифу Trello. Якщо ви ще обираєте, яку дошку розгортати, спочатку прочитайте порівняння self-hosted альтернатив Trello. У цьому посібнику передбачається, що вибір уже зроблено; описано лише розгортання.
Вам потрібен VPS із встановленими Docker Engine і плагіном Compose, а також 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.
Спочатку визначте потрібний обсяг диска, а вже потім — обсяг пам’яті, оскільки найбільше зростають вкладення. Виміряйте власний екземпляр, а не покладайтеся на цей абзац:
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 не спричиняє зрозумілої помилки. Сторінка завантажується, але процес завантаження ніколи не завершується.
Типовий випадок: ви копіюєте приклад із upstream, залишаєте 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 не розміщено у volume, завантажені файли потрапляють у доступний для запису шар контейнера. Цей шар видаляється під час повторного створення контейнера, а контейнер створюється заново щоразу, коли ви змінюєте тег образу. Дошка відкривається без видимих проблем, усі картки залишаються на місці, але всі посилання на вкладення не працюють, оскільки рядки в базі даних і далі вказують на файли, яких більше немає.
Іменований volume у наведеному вище Compose-файлі запобігає цьому. Bind mount також працює та спрощує резервне копіювання файлів звичайними інструментами, але потребує ще одного кроку. Процес Node усередині контейнера працює від UID 1000, тому каталог на хості, власником якого є root, спричинить помилку доступу під час першого завантаження:
sudo chown -R 1000:1000 /opt/planka/dataВідмінності між цими варіантами розглянуто в порівнянні bind mount та іменованих volume.
Якщо вкладення перевищують обсяг диска у вашому тарифному плані, Planka може записувати їх у S3-сумісне сховище через S3_ENDPOINT, S3_BUCKET і відповідні змінні для ключів. Це може бути hosted bucket або self-hosted об’єктне сховище 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, означає, що мітки маршрутизатора не збігаються. Найчастіше це відбувається тому, що контейнер не підключений до мережі 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 і переконайтеся, що можете увійти та відкрити вкладення.
Запускайте створення дампу безпосередньо перед кожною зміною версії. Резервна копія, створена минулої ночі, — це не те саме, що резервна копія, створена перед міграцією, яку ви збираєтеся виконати.
Зафіксуйте теги та прочитайте release notes
Обидва теги образів у цьому файлі навмисно зафіксовані.
ghcr.io/plankanban/planka:2.1.1 — це конкретний реліз, актуальний станом на August 2026. latest змінюється щоразу, коли upstream публікує новий реліз, тому звичайний docker compose pull може несподівано запустити міграцію схеми. Перш ніж змінювати це число, прочитайте release notes, оскільки саме там описано несумісні зміни та виправлення безпеки. Version 2.0.3 було опубліковано як security release. Саме такі зміни потрібно прочитати, а не отримати випадково.
postgres:16-alpine зафіксовано на major version з важливішої причини. Postgres записує каталог даних у форматі, пов’язаному з major version, і сервер відмовляється відкривати каталог, створений іншою версією. Запишіть 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 у новий порожній каталог даних. Це запланована операція, під час якої стек має бути зупинений, а не побічний ефект завантаження образу.
Якщо ви переносите наявну інсталяцію 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 не зберігався в томі, тому файли перебували в шарі контейнера, який замінило оновлення. Відновіть файли з резервної копії, а потім додайте том, перш ніж знову змінювати тег образу.
Traefik повертає 404. Контейнер не підключений до мережі proxy або правило Host() не відповідає вашому DNS-запису. docker compose config показує мітки після підстановки значень. Саме там стають помітними помилки в написанні.
Сповіщення або webhook-запити не надходять. Planka 2 надсилає вихідні HTTP-запити через внутрішній фільтр, а стандартний список блокування охоплює localhost і postgres. Webhook-запит до іншого контейнера на тому самому хості може блокуватися навмисно. Змініть OUTGOING_ALLOWED_HOSTS, а не видаляйте фільтр.
Після запуску обслуговування не потребує значних зусиль. Стежте за нотатками до випусків і створюйте дамп бази даних перед кожним оновленням. Після перезавантаження стек запускається автоматично завдяки restart: unless-stopped, якщо сам Docker service увімкнений під час завантаження системи, а в розділі Compose-стеки, які запускаються після перезавантаження описано випадки, коли цього не відбувається.
FAQ
Чому Planka завантажується без кінця після входу?
Облікові дані прийнято, але live-з’єднання не встановлено. 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 разом із відповідними змінними для пароля, імені та імені користувача й запустіть stack, або виконайте docker compose run --rm planka npm run db:create-admin-user та дайте відповіді на запити. Інтерактивна команда безпечніша на спільному сервері, оскільки пароль не потрапляє в середовище контейнера, де його може прочитати docker inspect. Якщо залишити DEFAULT_ADMIN_EMAIL установленою, цей обліковий запис буде захищено від редагування та видалення через інтерфейс.
Де Planka зберігає вкладення та аватари?
У Planka 2 усі завантажені файли, зокрема вкладення, аватари користувачів і фонові зображення дошок, зберігаються в контейнері за шляхом /app/data. Підключіть цей шлях до іменованого тому. Якщо том не змонтовано, файли залишаються у доступному для запису шарі контейнера й будуть знищені під час наступного створення контейнера. Це відбувається під час кожного оновлення image. Bind mount також підходить, але процес Node працює з UID 1000, тому виконайте sudo chown -R 1000:1000 для каталогу на host. Інакше завантаження завершуватимуться помилкою прав доступу.
Скільки RAM потрібно 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, щоб псевдотермінал не пошкодив перенаправлений вивід. Перегляньте примітки до випуску для кожної пропущеної версії, змініть тег image на конкретний випуск замість latest, потім виконайте docker compose pull і docker compose up -d та стежте за журналом міграції. Залиште тег Postgres закріпленим за його major-версією, оскільки сервер відмовляється відкривати каталог даних, записаний іншою major-версією.