Cara Self-Host Planka dengan Docker Compose
Deploy Planka di VPS dengan Docker Compose, Postgres, dan Traefik. Atur variabel bootstrap admin serta BASE_URL dengan benar agar login tidak gagal.
Yang Anda dapatkan dari self-hosting Planka
Self-hosting Planka memberikan tim Anda papan Kanban dengan model kartu, daftar, dan label yang sudah dikenal pengguna Trello, yang berjalan pada VPS yang Anda kendalikan. Tidak ada batas jumlah pengguna dan tidak ada tagihan per pengguna karena satu-satunya biaya adalah server. Panduan ini men-deploy Planka dengan Docker Compose di belakang Traefik, menggunakan Postgres untuk data dan named volume untuk setiap file yang diunggah pengguna.
Panduan ini ditujukan untuk tim yang terdiri dari dua hingga lima orang dan akan beralih dari tingkat gratis Trello. Jika Anda masih menentukan papan yang akan digunakan, baca perbandingan alternatif Trello yang di-host sendiri terlebih dahulu. Panduan ini mengasumsikan pilihan tersebut sudah ditetapkan dan hanya membahas deployment.
Anda memerlukan VPS yang menjalankan Docker Engine dengan Compose plugin serta DNS A record yang mengarah ke VPS tersebut. Anda juga memerlukan instance Traefik yang sudah melakukan TLS termination (transport layer security) pada server itu. Jika Traefik belum tersedia, siapkan reverse proxy Traefik di depan beberapa aplikasi Compose terlebih dahulu, dan baca dasar-dasar Docker Compose untuk VPS jika file di bawah ini belum Anda kenal.
Berapa kapasitas VPS yang dibutuhkan Planka?
Proyek ini tidak menerbitkan kebutuhan minimum perangkat keras. Karena itu, anggap angka apa pun yang Anda baca sebagai titik awal, bukan hasil pengukuran. Angka 2 vCPU dan 4 GB yang sering dicantumkan pada halaman penyedia hosting adalah konfigurasi bawaan yang nyaman dari penyedia, bukan kebutuhan yang diukur oleh proyek. Konfigurasi tersebut cukup besar untuk board yang digunakan oleh lima orang.
Komponen yang benar-benar berjalan berukuran kecil: satu proses Node.js yang melayani API dan frontend yang sudah dibangun, serta satu proses Postgres yang menyimpan data. Satu proses proxy kecil juga berjalan di dalam container Planka untuk memfilter request keluar. Paket dengan 1 vCPU dan 2 GB dapat menangani board untuk dua hingga lima orang. Sebagian besar memori yang tersisa akan digunakan sebagai cache Postgres.
Tentukan ukuran disk sebelum menentukan ukuran memori karena attachment adalah komponen yang terus bertambah. Ukur instance Anda sendiri, bukan berdasarkan paragraf ini:
docker stats --no-stream
docker system df -vPerintah pertama menampilkan penggunaan memori dan CPU setiap container secara langsung. Perintah kedua menunjukkan kapasitas yang digunakan setiap volume. Lakukan kedua pengukuran setelah satu minggu kerja normal, bukan pada hari instalasi, karena board yang tidak aktif tidak memberikan informasi tentang kebutuhan tim Anda.
Tulis file Compose
Buat direktori tersebut dan ubah kepemilikannya, sehingga Anda tidak perlu mengedit file ini melalui sudo.
sudo mkdir -p /opt/planka
sudo chown "$USER":"$USER" /opt/planka
cd /opt/plankaBuat secret dalam file .env di samping file Compose. Compose membaca file tersebut secara otomatis dan mengganti variabel dengan nilainya.
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 memang disengaja. String heksadesimal hanya berisi digit dan huruf a hingga f, sehingga tidak dapat merusak string koneksi DATABASE_URL tempat string tersebut ditempelkan. Password base64 yang berisi garis miring atau tanda at dapat menyebabkan error koneksi yang terlihat seperti hostname yang salah, dan masalah ini dapat menghabiskan waktu satu jam. Pola yang lebih luas dibahas dalam menyimpan secret di luar file Compose.
Sekarang docker-compose.yml. Ganti kanban.example.com dengan hostname Anda sendiri di kedua tempat kemunculannya.
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:Empat keputusan dalam file tersebut perlu dijelaskan karena biasanya orang mengubahnya lalu menyesal.
- Tidak ada blok
ports:pada service Planka. Traefik menjangkau container melalui jaringanproxy, sehingga port 1337 tidak pernah dipublikasikan pada host. Memublikasikannya akan memberi siapa pun cara untuk melewati proxy dan sertifikat Anda. loadbalancer.server.port=1337menetapkan port di dalam container. Planka mendengarkan pada port 1337, sedangkan contoh upstream hanya dapat menjangkaunya melalui port 3000 karena port tersebut dipetakan ke host. Di sini tidak ada pemetaan ke host, sehingga Traefik harus diberi tahu tentang port container.condition: service_healthyberpasangan dengan healthcheck Postgres. Tanpanya, Planka akan start sebelum database menerima koneksi, gagal pada query pertamanya, lalu keluar. Kondisi ini terlihat seperti crash loop. Mekanismenya dijelaskan dalam healthcheck Compose dan pengurutan startup.- Service database sengaja diberi nama
postgres. Planka 2 mengarahkan request keluarnya sendiri melalui filter internal yang daftar blokir default-nya adalahlocalhost,postgres. Jika service tersebut diganti namanya, database Anda diam-diam dihapus dari daftar itu.
Periksa apakah Compose dapat melihat secret Anda sebelum menjalankan apa pun:
docker compose config | grep -E 'image:|BASE_URL|POSTGRES_USER'Perintah tersebut menampilkan file dengan nilai .env yang sudah diganti. Nilai yang kosong berarti Compose tidak membaca file .env, biasanya karena Anda menjalankan perintah dari direktori yang berbeda.
Fungsi sebenarnya variabel bootstrap admin
Sejak Planka 1.13, tidak ada administrator yang dibuat secara otomatis. Karena itu, database baru tidak memiliki pengguna yang dapat login. Grup DEFAULT_ADMIN_* adalah salah satu dari dua cara untuk mengatasinya.
Saat startup, Planka mencari pengguna yang cocok dengan DEFAULT_ADMIN_EMAIL. Jika pengguna tersebut tidak ada, Planka membuatnya menggunakan password, display name, dan username yang ditetapkan di samping variabel tersebut. Proses ini terjadi saat boot pertama terhadap database kosong. Jadi, variabel ini digunakan untuk melakukan bootstrap akun, bukan untuk mengelolanya.
DEFAULT_ADMIN_EMAIL memiliki fungsi kedua yang sering mengecoh pengguna. Selama variabel ini masih ditetapkan, akun yang namanya tercantum di dalamnya tidak dapat diedit atau dihapus dari interface oleh siapa pun. Ini adalah perlindungan untuk mencegah akun terkunci. Hal ini juga menyebabkan Anda tidak dapat mengganti nama akun atau mengubah alamat emailnya melalui UI. Hapus variabel tersebut lalu lakukan restart. Setelah itu, akun menjadi admin biasa yang dapat Anda edit seperti akun lainnya.
Baris password harus ditangani dengan hati-hati. Apa pun yang berada di bawah environment: dapat dibaca oleh siapa pun yang dapat menjalankan docker inspect pada container. Karena itu, DEFAULT_ADMIN_PASSWORD tidak boleh disimpan di sana secara permanen. Login, ubah password melalui interface, hapus baris tersebut, lalu jalankan docker compose up -d lagi.
Cara yang lebih aman tidak menggunakan variabel tersebut sama sekali. Beri komentar pada seluruh grup DEFAULT_ADMIN_*, lalu buat akun secara interaktif:
docker compose run --rm planka npm run db:create-admin-userPerintah tersebut meminta email, password, display name, dan username opsional, lalu langsung menulis pengguna ke database. Password tidak pernah masuk ke file Compose atau environment container. Gunakan cara ini jika lebih dari satu orang memiliki akses shell ke VPS. Perintah tersebut menjalankan Postgres terlebih dahulu karena depends_on. Jadi, cara ini dapat digunakan pada stack yang belum pernah dijalankan.
Kedua cara tersebut tetap mengharuskan Anda mengelola password Planka secara manual. Jika kredensial ini menjadi set keempat yang harus dikelola tim Anda, Planka dapat mendelegasikan login ke penyedia OIDC seperti Authentik yang dijalankan sebagai server single sign-on Anda sendiri, sementara admin bootstrap tetap digunakan sebagai akun break-glass ketika penyedia tersebut sedang tidak tersedia.
Mengapa BASE_URL menyebabkan login gagal jika tidak sesuai dengan hostname
BASE_URL adalah alamat persis yang diketik pengguna di browser, termasuk skema dan tanpa garis miring di akhir. Untuk stack ini, nilainya adalah https://kanban.example.com. Planka membuat link dan koneksi WebSocket sendiri berdasarkan nilai tersebut. Karena itu, BASE_URL yang salah tidak menghasilkan error yang jelas. Halaman akan terbuka, tetapi proses pemuatan tidak pernah selesai.
Kasus yang umum terjadi: Anda menyalin contoh dari upstream, membiarkan BASE_URL=http://localhost:3000, lalu mengakses situs melalui HTTPS pada domain sebenarnya. Form login dikirim dan kredensial Anda diterima. Namun, board tidak pernah muncul. Buka konsol developer browser. Anda akan melihat request ke /socket.io/ gagal karena client diperintahkan membuka koneksi langsung ke localhost:3000, sedangkan alamat tersebut tidak ada di laptop Anda.
TRUST_PROXY=true adalah bagian lain dari masalah yang sama. Planka berjalan di belakang Traefik, sehingga setiap request tiba dari alamat proxy melalui HTTP biasa di dalam jaringan Docker. Tanpa TRUST_PROXY, aplikasi mengabaikan header X-Forwarded-Proto dan X-Forwarded-For yang ditetapkan Traefik. Akibatnya, aplikasi menganggap koneksi tidak aman dan memperlakukan semua client sebagai satu alamat IP yang sama. Jika opsi tersebut ditetapkan, aplikasi membaca header itu dan menggunakan skema yang sama seperti yang dilihat browser.
Traefik mem-proxy WebSocket tanpa konfigurasi tambahan. Ini adalah salah satu alasan untuk memilihnya dalam kasus ini. Pada nginx, socket.io memerlukan blok location tersendiri yang berisi proxy_set_header Upgrade $http_upgrade dan proxy_set_header Connection "upgrade". Jika tidak, spinner akan tetap macet karena penyebab yang berbeda.
Memindahkan board ke hostname baru di kemudian hari berarti mengubah dua hal secara bersamaan: nilai BASE_URL dan aturan Traefik Host(). Jika hanya satu yang diubah, spinner akan kembali muncul. Menyajikan Planka dari subpath seperti https://example.com/planka dapat dilakukan mulai versi 2.1.0, yang dirilis pada March 2026. Pada tag yang lebih lama, gunakan subdomain khusus untuk Planka.
Lokasi Planka menyimpan lampiran dan avatar
Planka 2 menyimpan semua yang diunggah pengguna di bawah satu path di dalam container: /app/data. Lampiran, avatar pengguna, dan gambar latar board semuanya disimpan di sana. Versi 1 menggunakan tiga direktori terpisah, sehingga file Compose yang disalin dari panduan lama memasang path yang sudah tidak ada dan direktori data sebenarnya tidak dipasang sebagai mount.
Mount tunggal itu menentukan apakah board dapat bertahan setelah upgrade atau menimbulkan masalah besar. Jika /app/data tidak berada pada volume, file yang diunggah akan masuk ke writable layer milik container. Layer tersebut dihapus saat container dibuat ulang, dan container dibuat ulang setiap kali Anda mengubah image tag. Board kembali dan tampak normal, semua card masih ada, tetapi setiap link lampiran tidak lagi berfungsi karena baris database masih menunjuk ke file yang sudah tidak ada.
Named volume dalam file Compose di atas mencegah masalah ini. Bind mount juga dapat digunakan dan memudahkan pencadangan file dengan tool biasa, tetapi memerlukan satu langkah tambahan. Proses Node di dalam container berjalan sebagai UID 1000, sehingga direktori pada host yang dimiliki root akan menyebabkan permission error saat upload pertama:
sudo chown -R 1000:1000 /opt/planka/dataPerbedaan antara keduanya dibahas dalam bind mount dibandingkan named volume.
Jika jumlah lampiran melebihi kapasitas disk pada paket Anda, Planka dapat menulisnya ke storage yang kompatibel dengan S3 melalui S3_ENDPOINT, S3_BUCKET, dan variabel key yang sesuai. Storage tersebut dapat menunjuk ke bucket ter-host atau ke object store MinIO yang di-host sendiri pada server lain. Tentukan pilihan ini sebelum tim memenuhi board, karena pengaturan tersebut hanya berlaku untuk upload baru.
Jalankan stack dan periksa hasilnya
docker compose pull
docker compose up -d
docker compose psdocker compose ps seharusnya menampilkan postgres sebagai healthy dan planka sebagai running. Jika Planka terus melakukan restart dalam loop, hal pertama yang harus diperiksa adalah koneksi database, bukan aplikasi.
docker compose logs -f plankaBoot pertama yang berhasil akan menjalankan migrasi database, lalu melaporkan bahwa server listening pada port 1337. Pastikan schema benar-benar sudah dibuat dengan meminta Postgres memeriksanya secara langsung, bukan hanya mengandalkan log:
docker compose exec postgres psql -U planka -d planka -c '\dt'Daftar tabel yang mencakup board dan card berarti migrasi telah berjalan. Pesan "Did not find any relations" berarti Planka tidak pernah terhubung. Karena itu, bandingkan DATABASE_URL dengan nilai POSTGRES_USER dan POSTGRES_PASSWORD dalam .env Anda.
Berikutnya, periksa route dari komputer Anda sendiri, bukan dari VPS:
curl -I https://kanban.example.comHTTP/2 200 berarti Traefik memegang sertifikat dan berhasil menjangkau container. Respons 404 yang disajikan Traefik berarti label router tidak cocok. Penyebab yang paling sering adalah container tidak terhubung ke network proxy. Sekarang buka situs tersebut dan login menggunakan akun admin.
Ambil pg_dump sebelum setiap peningkatan versi
Dua penyimpanan terpisah menyimpan board Anda, sehingga backup harus mencakup keduanya: database Postgres dan volume planka-data. Buat dump database saat stack sedang berjalan.
docker compose exec -T postgres pg_dump -U planka -d planka > "planka-db-$(date +%F).sql"-T wajib digunakan. Tanpanya, Compose mengalokasikan pseudo-terminal, lalu lapisan terminal mengubah akhiran baris dalam stream. Akibatnya, file dump gagal di tengah proses restore. Kegagalan ini baru terlihat beberapa minggu kemudian, pada waktu yang paling buruk.
Selanjutnya, backup upload. Temukan nama volume sebenarnya terlebih dahulu karena Compose menambahkan nama direktori project sebagai prefix.
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 .Project ini juga menyediakan docker-backup.sh dan docker-restore.sh di repository-nya. Dokumentasi resmi menjadwalkannya melalui cron nightly. Keduanya dapat digunakan. Namun, backup yang belum pernah direstore tidak dapat dianggap memadai. Restore backup tersebut sekali ke VPS sementara, lalu pastikan Anda dapat login dan membuka attachment.
Jalankan dump tepat sebelum setiap perubahan versi. Backup dari tadi malam tidak sama dengan backup yang dibuat sebelum migrasi yang akan Anda jalankan.
Sematkan tag dan baca catatan rilis
Kedua tag image dalam file tersebut sengaja disematkan.
ghcr.io/plankanban/planka:2.1.1 adalah rilis tertentu yang masih berlaku per Agustus 2026. latest berubah setiap kali upstream menerbitkan rilis baru, sehingga docker compose pull rutin dapat menerapkan migrasi skema pada waktu yang tidak Anda tentukan. Baca catatan rilis sebelum mengubah angka tersebut, karena perubahan yang tidak kompatibel dan perbaikan keamanan dijelaskan di sana. Version 2.0.3 diterbitkan sebagai rilis keamanan. Rilis seperti inilah yang sebaiknya Anda baca, bukan terapkan secara tidak sengaja.
postgres:16-alpine disematkan ke versi mayor karena alasan yang lebih penting. Postgres menulis direktori datanya dalam format yang terkait dengan versi mayor, dan server menolak membuka direktori yang ditulis oleh versi berbeda. Tulis postgres:latest, biarkan tag berubah ke 17, dan container tidak akan start:
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.Tidak ada data yang hilang, dan me-restart juga tidak memperbaiki masalah ini. Beralih ke versi mayor Postgres baru memerlukan dump dari versi lama dan restore ke direktori data baru pada versi baru. Ini adalah pekerjaan terencana yang dilakukan saat stack dihentikan, bukan efek samping dari image pull.
Jika Anda memindahkan instalasi Planka 1.x yang sudah ada, bukan memulai dari awal, upgrade tersebut memiliki prosedur terdokumentasi tersendiri di dokumentasi project, dan tidak ada cara untuk kembali ke version 1 tanpa backup yang dibuat sebelumnya.
Mode kegagalan dan teks yang akan Anda lihat
Planka terus melakukan restart dan log menyebutkan database. Kredensial dalam DATABASE_URL tidak cocok dengan environment Postgres. Perhatikan bahwa POSTGRES_PASSWORD hanya diterapkan saat direktori data pertama kali diinisialisasi. Jadi, memperbaiki variabel tersebut setelah boot pertama yang gagal tidak mengubah apa pun. Anda harus menghapus volume db-data lalu memulai kembali.
Login berhasil, tetapi board tidak pernah dimuat. BASE_URL tidak cocok dengan alamat pada bilah alamat browser, atau TRUST_PROXY tidak ada. Console browser menampilkan request yang gagal ke /socket.io/.
Upload gagal, sedangkan fungsi lainnya berjalan normal. Penyebabnya adalah bind mount yang dimiliki oleh root. Jalankan sudo chown -R 1000:1000 pada direktori host, lalu restart container.
Lampiran hilang setelah upgrade. /app/data tidak berada pada volume. Akibatnya, file tersimpan di container layer yang diganti oleh proses upgrade. Pulihkan file dari backup, lalu tambahkan volume sebelum mengubah image tag lagi.
Traefik mengembalikan 404. Container tidak berada pada network proxy, atau rule Host() tidak cocok dengan record DNS Anda. docker compose config menampilkan label setelah substitusi. Di sinilah typo dapat terlihat.
Notifikasi atau webhook tidak pernah tiba. Planka 2 mengirim request HTTP keluar melalui filter internal, dan block list default mencakup localhost serta postgres. Webhook yang diarahkan ke container lain pada host yang sama dapat diblokir secara sengaja. Sesuaikan OUTGOING_ALLOWED_HOSTS, bukan menghapus filter tersebut.
Setelah berjalan, beban operasionalnya kecil. Pantau catatan rilis dan dump database sebelum setiap upgrade. Reboot akan menjalankan kembali stack secara otomatis karena restart: unless-stopped, selama service Docker sendiri diaktifkan saat boot. Stack Compose yang kembali berjalan setelah reboot membahas kondisi ketika service tersebut tidak aktif.
FAQ
Mengapa Planka terus memuat setelah saya login?
Kredensial diterima, tetapi koneksi live gagal. Planka membentuk URL WebSocket dari BASE_URL. Jika variabel tersebut masih berisi http://localhost:3000, sementara Anda mengakses situs melalui https://kanban.example.com, browser mencoba membuka socket ke alamat yang tidak ada di mesin Anda. Konsol developer menampilkan permintaan yang gagal ke /socket.io/. Atur BASE_URL ke alamat publik yang tepat tanpa garis miring di akhir. Tambahkan TRUST_PROXY=true agar aplikasi mematuhi header X-Forwarded-Proto dari reverse proxy Anda. Kemudian jalankan docker compose up -d.
Bagaimana cara membuat pengguna admin Planka pertama?
Sejak versi 1.13, pengguna administrator tidak dibuat secara otomatis. Anda dapat menetapkan DEFAULT_ADMIN_EMAIL beserta variabel password, nama, dan username yang sesuai, lalu menjalankan stack. Pilihan lainnya adalah menjalankan docker compose run --rm planka npm run db:create-admin-user dan menjawab prompt. Perintah interaktif lebih aman pada server bersama karena password tidak pernah masuk ke environment container yang dapat membacanya melalui docker inspect. Jika DEFAULT_ADMIN_EMAIL tetap ditetapkan setelahnya, akun tersebut tidak dapat diedit atau dihapus melalui antarmuka.
Di mana Planka menyimpan lampiran dan avatar?
Semua file yang diunggah disimpan di /app/data di dalam container pada Planka 2. Ini mencakup lampiran, avatar pengguna, dan latar belakang board. Mount path tersebut pada named volume. Jika path itu tidak di-mount, file berada di writable layer container dan akan dihapus saat container dibuat ulang. Hal ini terjadi pada setiap upgrade image. Bind mount juga dapat digunakan, tetapi proses Node berjalan sebagai UID 1000. Karena itu, jalankan sudo chown -R 1000:1000 pada direktori host. Jika tidak, proses upload gagal dengan permission error.
Berapa banyak RAM yang dibutuhkan Planka yang di-host sendiri?
Proyek ini tidak menerbitkan batas minimum hardware. Angka 2 vCPU dan 4 GB yang sering dicantumkan pada halaman hosting merupakan default provider, bukan hasil pengukuran. Angka tersebut juga cukup besar untuk board kecil. Beban kerja hanya terdiri atas satu proses Node dan satu proses Postgres. Karena itu, paket dengan 1 vCPU dan 2 GB dapat mendukung tim beranggotakan dua hingga lima orang. Jalankan docker stats --no-stream setelah satu minggu penggunaan normal. Tentukan ukuran resource berdasarkan data Anda sendiri. Pantau disk lebih ketat daripada memory karena lampiran terus bertambah.
Bagaimana cara meng-upgrade Planka tanpa kehilangan data?
Dump database dan arsipkan volume upload segera sebelum upgrade. Jangan mengandalkan jadwal backup tadi malam. Gunakan docker compose exec -T postgres pg_dump -U planka -d planka > planka-db.sql dan pertahankan -T agar pseudo-terminal tidak merusak output yang dialihkan. Baca release notes untuk setiap versi yang Anda lewati. Ubah image tag ke release tertentu, bukan latest. Kemudian jalankan docker compose pull dan docker compose up -d. Pantau log untuk melihat proses migration. Biarkan tag Postgres tetap dikunci pada major version-nya karena server menolak membuka data directory yang ditulis oleh major version berbeda.