SSD Nodes Learn 🎉 VPS vanaf $5.50/mnd
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-08-13

Planka zelf hosten met Docker Compose: handleiding

Installeer Planka op uw eigen VPS met Docker Compose en Postgres. Leer hoe u Traefik configureert en voorkom inlogfouten door de juiste BASE_URL en admin variabelen in te stellen.

Wat u krijgt door Planka zelf te hosten

Door Planka zelf te hosten beschikt uw team over een Kanban-bord met het model van kaarten, lijsten en labels dat bekend is van Trello, draaiend op een VPS die u beheert. Er zijn geen limieten voor het aantal gebruikers en geen facturatie per gebruiker, aangezien de enige kosten de server zijn. Deze handleiding implementeert de software met Docker Compose achter Traefik, waarbij Postgres wordt gebruikt voor de data en een named volume voor elk bestand dat gebruikers uploaden.

De doelgroep voor deze handleiding is een team van twee tot vijf personen dat het gratis abonnement van Trello verlaat. Als u nog beslist welk bord u wilt gebruiken, lees dan eerst de vergelijking van zelf-gehoste Trello-alternatieven. Deze handleiding gaat ervan uit dat de keuze al is gemaakt en behandelt uitsluitend de implementatie.

U heeft een VPS nodig waarop Docker Engine met de Compose-plugin draait, en een DNS A-record dat naar deze server verwijst. U heeft ook een Traefik-instantie nodig die al TLS (transport layer security) afhandelt op die server. Als Traefik nog niet aanwezig is, stel dan eerst een Traefik reverse proxy voor meerdere Compose-applicaties in, en lees de basisprincipes van Docker Compose voor een VPS als het onderstaande bestand onbekend voorkomt.

Hoeveel VPS-capaciteit heeft Planka nodig?

Het project publiceert geen minimale hardwarevereisten, dus beschouw elk getal dat u leest als een uitgangspunt in plaats van een harde maatstaf. De 2 vCPU en 4 GB die hostingpagina's vaak noemen, is een comfortabele standaard van de provider, geen vereiste die door het project is vastgesteld. Het is ruim bemeten voor een bord dat door vijf personen wordt gebruikt.

Wat er daadwerkelijk draait is klein: één Node.js-proces dat de API en de gebouwde frontend serveert, en één Postgres-proces dat de data beheert. Een derde, klein proxy-proces draait binnen de Planka-container om uitgaande verzoeken te filteren. Een plan met 1 vCPU en 2 GB is voldoende voor een bord voor twee tot vijf personen, waarbij het grootste deel van het resterende geheugen wordt gebruikt als Postgres-cache.

Bepaal de schijfgrootte voordat u het geheugen bepaalt, omdat bijlagen de component zijn die groeit. Meet uw eigen instantie in plaats van op deze paragraaf te vertrouwen:

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

Het eerste commando toont het actuele geheugen- en CPU-gebruik per container. Het tweede laat zien hoeveel ruimte elk volume inneemt. Voer beide metingen uit na een normale werkweek, niet op de dag van installatie, omdat een inactief bord niets zegt over het gebruik door uw team.

Het Compose-bestand schrijven

Maak de map aan en neem het eigendom ervan over, zodat u deze bestanden nooit via sudo hoeft te bewerken.

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

Genereer de geheimen in een .env-bestand naast het Compose-bestand. Compose leest dat bestand automatisch en vervangt de waarden.

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 is een bewuste keuze. Een hex-string bevat alleen cijfers en de letters a tot en met f, waardoor deze de DATABASE_URL-verbindingsreeks waarin deze wordt geplakt niet kan verbreken. Een base64-wachtwoord met een slash of een apenstaartje erin veroorzaakt een verbindingsfout die lijkt op een onjuiste hostnaam, wat u een uur aan tijd kost. Het bredere patroon wordt behandeld in geheimen buiten het Compose-bestand houden.

Voer nu docker-compose.yml uit. Vervang kanban.example.com op beide plaatsen waar het voorkomt door uw eigen hostnaam.

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:

Vier beslissingen in dat bestand zijn de toelichting waard, omdat dit de punten zijn die mensen wijzigen en waar ze vervolgens spijt van krijgen.

  • Er is geen ports:-blok bij de Planka-service. Traefik bereikt de container via het proxy-netwerk, dus poort 1337 wordt nooit gepubliceerd op de host. Publicatie zou iedereen een manier geven om uw proxy en uw certificaat te omzeilen.
  • loadbalancer.server.port=1337 benoemt de poort binnen de container. Planka luistert op 1337, en het upstream-voorbeeld bereikt het alleen op 3000 omdat het de poort naar de host mapt. Er is hier geen host-mapping, dus Traefik moet de containerpoort weten.
  • condition: service_healthy vormt een paar met de Postgres-healthcheck. Zonder dit start Planka voordat de database verbindingen accepteert, faalt de eerste query en sluit het programma af, wat eruitziet als een crash-loop. De werking staat in Compose healthchecks en opstartvolgorde.
  • De databaseservice is met opzet postgres genoemd. Planka 2 routeert zijn eigen uitgaande verzoeken via een intern filter waarvan de standaard blokkeerlijst localhost,postgres is. Hernoem de service en u verwijdert stilletjes uw database uit die lijst.

Controleer of Compose uw geheimen kan zien voordat u iets start:

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

Dit drukt het bestand af met de reeds door .env vervangen waarden. Een lege waarde betekent dat Compose het .env-bestand niet leest, meestal omdat u het commando vanuit een andere map uitvoert.

Wat de admin bootstrap-variabelen precies doen

Sinds Planka 1.13 wordt er niet langer automatisch een beheerder aangemaakt. Een nieuwe database bevat daarom standaard geen gebruikers die kunnen inloggen. De DEFAULT_ADMIN_*-groep is een van de twee manieren om dit op te lossen.

Bij het opstarten zoekt Planka naar een gebruiker die overeenkomt met DEFAULT_ADMIN_EMAIL. Als deze niet bestaat, wordt er een aangemaakt met het wachtwoord, de weergavenaam en de gebruikersnaam die daarbij zijn ingesteld. Dit gebeurt alleen bij de eerste keer opstarten met een lege database; deze variabelen dienen dus voor het bootstrappen van een account en niet voor het beheer ervan.

DEFAULT_ADMIN_EMAIL vervult een tweede functie die vaak voor verwarring zorgt. Zolang deze variabele is ingesteld, kan het account met die naam via de interface door niemand worden bewerkt of verwijderd. Dit is een beveiliging tegen buitensluiting. Dit is ook de reden waarom u de naam of het e-mailadres van dat account niet in de UI kunt wijzigen. Verwijder de variabele en start de container opnieuw op; het account wordt dan een normale beheerder die u net als elk ander account kunt bewerken.

Wees voorzichtig met de wachtwoordregel. Alles onder environment: is leesbaar voor iedereen die docker inspect op de container kan uitvoeren. Daarom hoort DEFAULT_ADMIN_PASSWORD daar niet permanent te staan. Log in, wijzig uw wachtwoord in de interface, verwijder de regel en voer daarna opnieuw docker compose up -d uit.

De schonere methode slaat de variabelen volledig over. Zet de gehele DEFAULT_ADMIN_*-groep in commentaar en maak het account interactief aan:

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

Het systeem vraagt om een e-mailadres, wachtwoord, weergavenaam en een optionele gebruikersnaam, en schrijft de gebruiker direct naar de database. Het wachtwoord komt hierbij nooit in het Compose-bestand of de containeromgeving terecht. Gebruik deze methode als meerdere personen shell-toegang hebben tot de VPS. Het commando start eerst Postgres vanwege depends_on, waardoor het ook werkt op een stack die nog nooit actief is geweest.

Beide methoden vereisen dat u Planka-wachtwoorden handmatig beheert. Als dit de vierde set inloggegevens is die uw team moet bijhouden, kan Planka inloggegevens delegeren aan een OIDC-provider, zoals Authentik als uw eigen single sign-on server. De bootstrap-beheerder blijft dan beschikbaar als noodaccount voor het geval de provider onbereikbaar is.

Waarom BASE_URL inlogproblemen veroorzaakt wanneer deze niet overeenkomt met de hostnaam

BASE_URL is het exacte adres dat gebruikers in de browser typen, inclusief het protocol en zonder afsluitende slash. Voor deze stack is dat https://kanban.example.com. Planka genereert zijn eigen links en de WebSocket-verbinding op basis van deze waarde. Een onjuiste BASE_URL resulteert daarom niet in een duidelijke foutmelding. De pagina laadt wel, maar het laden wordt nooit voltooid.

De meest voorkomende situatie: u kopieert het voorbeeld van de upstream, laat BASE_URL=http://localhost:3000 staan en bezoekt de site via HTTPS op uw eigen domein. Het inlogformulier wordt verzonden en uw inloggegevens worden geaccepteerd. Het dashboard verschijnt echter niet. Open de ontwikkelaarsconsole van uw browser en u zult zien dat verzoeken aan /socket.io/ falen. De client heeft namelijk de instructie gekregen om de live-verbinding te openen naar localhost:3000, en dat adres bestaat niet op uw lokale systeem.

TRUST_PROXY=true is de andere helft van hetzelfde probleem. Planka draait achter Traefik, waardoor elk verzoek de applicatie bereikt vanaf het adres van de proxy via onversleuteld HTTP binnen het Docker-netwerk. Zonder TRUST_PROXY negeert de applicatie de X-Forwarded-Proto en X-Forwarded-For headers die Traefik meestuurt. Hierdoor denkt de applicatie dat de verbinding onveilig is en behandelt ze elke client als één gedeeld IP-adres. Wanneer deze optie is ingeschakeld, leest de applicatie de headers en stemt ze overeen met de browser over het gebruikte protocol.

Traefik proxiet WebSockets zonder extra configuratie; dit is een reden om de voorkeur aan Traefik te geven. Bij nginx heeft socket.io een eigen location-blok nodig met proxy_set_header Upgrade $http_upgrade en proxy_set_header Connection "upgrade", anders krijgt u hetzelfde probleem met een oneindig ladende pagina, maar dan met een andere oorzaak.

Het verplaatsen van het dashboard naar een nieuwe hostnaam vereist dat u twee zaken tegelijk aanpast: de waarde van BASE_URL en de Host()-regel in Traefik. Wijzigt u de een en vergeet u de ander, dan krijgt u opnieuw te maken met de ladende pagina. Het aanbieden van Planka vanaf een subpad zoals https://example.com/planka werkt vanaf versie 2.1.0, uitgebracht in maart 2026. Gebruik bij oudere versies een eigen subdomein.

Waar Planka bijlagen en avatars opslaat

Planka 2 slaat alles wat een gebruiker uploadt op onder één pad binnen de container: /app/data. Bijlagen, gebruikersavatars en achtergrondafbeeldingen van borden bevinden zich hier allemaal. Versie 1 gebruikte drie afzonderlijke mappen, waardoor een Compose-bestand dat is gekopieerd uit een oudere handleiding paden koppelt die niet langer bestaan, en de werkelijke datamap niet wordt gekoppeld.

Die ene koppeling maakt het verschil tussen een bord dat een upgrade overleeft en een verloren middag. Als /app/data niet op een volume staat, komen uploads terecht in de beschrijfbare laag van de container. Die laag wordt vernietigd wanneer de container opnieuw wordt aangemaakt, en de container wordt elke keer opnieuw aangemaakt wanneer u de image-tag wijzigt. Het bord komt terug en ziet er goed uit, de kaarten zijn er allemaal, maar elke link naar een bijlage is defect omdat de databaserijen nog steeds verwijzen naar bestanden die niet langer bestaan.

Het benoemde volume in het bovenstaande Compose-bestand voorkomt dit. Een bind mount werkt ook en maakt het eenvoudiger om de bestanden te back-uppen met standaardtools, maar dit vereist één extra stap. Het Node-proces in de container draait als UID 1000, dus een hostmap die eigendom is van root geeft een toegangs-foutmelding bij de eerste upload:

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

De afweging tussen beide wordt besproken in bind mounts versus benoemde volumes.

Als bijlagen meer schijfruimte in beslag nemen dan uw abonnement toelaat, kan Planka deze wegschrijven naar S3-compatibele opslag via S3_ENDPOINT, S3_BUCKET en de bijbehorende sleutelvariabelen. Dit kan verwijzen naar een gehoste bucket of naar een zelfgehoste MinIO object store op een andere machine. Beslis dit voordat het team het bord vult, omdat de instelling alleen geldt voor nieuwe uploads.

Start de stack en controleer de werking

docker compose pull
docker compose up -d
docker compose ps

docker compose ps zou postgres moeten tonen als healthy en planka als running. Als Planka in een lus blijft herstarten, controleer dan eerst de databaseverbinding in plaats van de applicatie zelf.

docker compose logs -f planka

Een succesvolle eerste start voert de database-migraties uit en meldt vervolgens dat de server luistert op poort 1337. Bevestig dat het schema daadwerkelijk is aangemaakt door Postgres direct te bevragen in plaats van enkel op het logbestand te vertrouwen:

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

Een lijst met tabellen die board en card bevat, betekent dat de migraties zijn uitgevoerd. "Did not find any relations" betekent dat Planka geen verbinding kon maken; vergelijk in dat geval DATABASE_URL met de POSTGRES_USER en POSTGRES_PASSWORD waarden in uw .env.

Controleer vervolgens de route vanaf uw eigen machine, niet vanaf de VPS:

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

HTTP/2 200 betekent dat Traefik een certificaat bezit en de container bereikt. Een 404-foutmelding van Traefik betekent dat de router-labels niet overeenkomen, meestal omdat de container niet is gekoppeld aan het proxy-netwerk. Open nu de website en log in met het beheerdersaccount.

Maak een pg_dump vóór elke versie-upgrade

Uw board wordt op twee afzonderlijke locaties opgeslagen, dus een back-up moet beide omvatten: de Postgres-database en het planka-data-volume. Maak de dump van de database terwijl de stack actief is.

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

De -T is niet optioneel. Zonder deze vlag wijst Compose een pseudo-terminal toe en herschrijft de terminal-laag de regeleinden in de stream. Hierdoor krijgt u een dumpbestand dat halverwege een restore faalt. Dit falen komt pas weken later aan het licht, wat het slechtst denkbare moment is.

Vervolgens de uploads. Zoek eerst de werkelijke volumenaam op, omdat Compose deze voorziet van een voorvoegsel met de naam van de projectmap.

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 .

Het project levert ook docker-backup.sh en docker-restore.sh in de repository, en de officiële documentatie adviseert deze via een dagelijkse cron-job uit te voeren. Beide benaderingen zijn prima. Wat niet acceptabel is, is een back-up die u nog nooit heeft teruggezet. Zet er daarom een terug op een test-VPS en bevestig dat u kunt inloggen en een bijlage kunt openen.

Voer de dump direct uit vóór elke versie-wijziging. Een back-up van gisteravond is niet hetzelfde als een back-up van vóór de migratie die u op het punt staat uit te voeren.

Pin de tags en lees de release notes

Beide image-tags in dat bestand zijn bewust vastgezet (pinned).

ghcr.io/plankanban/planka:2.1.1 is een specifieke release, actueel per augustus 2026. latest wijzigt zodra de upstream-partij een nieuwe versie publiceert, waardoor een routine-docker compose pull op een ongelegen moment een schema-migratie kan introduceren. Lees de release notes voordat u dat nummer wijzigt, aangezien daar ingrijpende wijzigingen en beveiligingsupdates worden beschreven. Versie 2.0.3 werd gepubliceerd als een beveiligingsrelease; dit is precies het type informatie dat u wilt lezen in plaats van dat het onverwacht wordt doorgevoerd.

postgres:16-alpine is om een dwingendere reden vastgezet op een major-versie. Postgres schrijft de datamap in een formaat dat gekoppeld is aan de major-versie, en de server weigert een map te openen die door een andere versie is geschreven. Schrijft u postgres:latest, dan zal de tag naar 17 verspringen en zal de container niet starten:

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.

Er gaat niets verloren en een herstart lost dit evenmin op. Overstappen naar een nieuwe Postgres major-versie vereist een dump van de oude versie en een restore naar een nieuwe datamap in de nieuwe versie. Dit is een geplande taak waarbij de stack offline is, geen bijwerking van een image pull.

Indien u een bestaande Planka 1.x-installatie verplaatst in plaats van een nieuwe start maakt, volgt u de gedocumenteerde procedure in de projectdocumentatie. Er is geen weg terug naar versie 1 zonder een vooraf gemaakte back-up.

Foutmodi en de meldingen die u zult zien

Planka start continu opnieuw op en het logbestand noemt de database. De inloggegevens in DATABASE_URL komen niet overeen met de Postgres-omgeving. Let op: POSTGRES_PASSWORD wordt alleen toegepast wanneer de datamap voor de eerste keer wordt geïnitialiseerd. Het aanpassen van de variabele na een mislukte eerste opstart heeft daarom geen effect. U moet het db-data-volume verwijderen en opnieuw beginnen.

Inloggen lukt, maar het bord laadt niet. BASE_URL komt niet overeen met het adres in de browserbalk, of TRUST_PROXY ontbreekt. De browserconsole toont mislukte verzoeken aan /socket.io/.

Uploads mislukken terwijl de rest werkt. Een bind mount is eigendom van root. Voer sudo chown -R 1000:1000 uit op de hostmap en herstart de container.

Bijlagen zijn verdwenen na een upgrade. /app/data stond niet op een volume, waardoor de bestanden in de containerlaag stonden die door de upgrade is vervangen. Herstel de bestanden vanuit een back-up en voeg het volume toe voordat u de image-tag opnieuw wijzigt.

Traefik geeft een 404-fout. De container bevindt zich niet in het proxy-netwerk, of de Host()-regel komt niet overeen met uw DNS-record. docker compose config toont de labels na substitutie; hier worden typefouten zichtbaar.

Notificaties of webhooks komen nooit aan. Planka 2 verstuurt uitgaande HTTP-verzoeken via een intern filter en de standaard blokkeerlijst dekt localhost en postgres. Een webhook die gericht is op een andere container op dezelfde host kan door dit ontwerp worden geblokkeerd. Pas OUTGOING_ALLOWED_HOSTS aan in plaats van het filter te verwijderen.

Zodra het systeem draait, is de operationele belasting gering. Houd de release notes in de gaten en maak een dump van de database vóór elke upgrade. Een herstart brengt de stack automatisch terug dankzij restart: unless-stopped, mits de Docker-service zelf is ingeschakeld bij het opstarten. Compose-stacks die terugkeren na een herstart behandelt de gevallen waarin dit niet gebeurt.

FAQ

Waarom blijft Planka oneindig laden na het inloggen?

De inloggegevens werden geaccepteerd, maar de live-verbinding niet. Planka bouwt de WebSocket-URL op basis van BASE_URL. Als die variabele nog steeds http://localhost:3000 bevat terwijl u de site benadert via https://kanban.example.com, probeert de browser een socket te openen naar een adres dat niet op uw machine bestaat. De console van de browser toont mislukte verzoeken naar /socket.io/. Stel BASE_URL in op het exacte publieke adres zonder afsluitende slash, voeg TRUST_PROXY=true toe zodat de applicatie de X-Forwarded-Proto-header van uw reverse proxy respecteert, en voer vervolgens docker compose up -d uit.

Hoe maak ik de eerste Planka-beheerder aan?

Sinds versie 1.13 wordt er niet langer automatisch een beheerder aangemaakt. Stel ofwel DEFAULT_ADMIN_EMAIL in met de bijbehorende variabelen voor wachtwoord, naam en gebruikersnaam en start de stack, of voer docker compose run --rm planka npm run db:create-admin-user uit en beantwoord de vragen. Het interactieve commando is veiliger op een gedeelde server, omdat het wachtwoord dan niet in de containeromgeving terechtkomt waar docker inspect het kan uitlezen. Als u DEFAULT_ADMIN_EMAIL daarna ingeschakeld laat, kan dat account niet via de interface worden bewerkt of verwijderd.

Waar slaat Planka bijlagen en avatars op?

In Planka 2 bevinden alle geüploade bestanden zich onder /app/data in de container, inclusief bijlagen, gebruikersavatars en achtergronden voor borden. Koppel dat pad aan een named volume. Als dit niet gekoppeld is, staan de bestanden in de beschrijfbare laag van de container en gaan ze verloren zodra de container opnieuw wordt aangemaakt, wat gebeurt bij elke image-upgrade. Een bind mount werkt ook, maar het Node-proces draait als UID 1000. Voer daarom sudo chown -R 1000:1000 uit op de host-directory, anders mislukken uploads door een rechtenfout.

Hoeveel RAM heeft een zelfgehoste Planka nodig?

Het project specificeert geen minimale hardwarevereisten. De 2 vCPU en 4 GB die vaak op hostingpagina's worden genoemd, zijn een standaard van de provider en geen meting; voor een klein bord is dit ruim voldoende. De volledige werklast bestaat uit één Node-proces en één Postgres-proces, dus een plan met 1 vCPU en 2 GB is toereikend voor een team van twee tot vijf personen. Voer docker stats --no-stream uit na een normale week en bepaal de grootte op basis van uw eigen cijfers. Houd de schijfruimte nauwkeuriger in de gaten dan het geheugen, aangezien bijlagen de voornaamste bron van groei zijn.

Hoe upgrade ik Planka zonder dataverlies?

Maak direct voor de upgrade een dump van de database en archiveer het volume met uploads; vertrouw niet op de back-up van de vorige nacht. Gebruik docker compose exec -T postgres pg_dump -U planka -d planka > planka-db.sql en behoud de -T-vlag zodat de pseudo-terminal de omgeleide uitvoer niet beschadigt. Lees de release notes voor elke versie die u overslaat, wijzig de image-tag naar een specifieke release in plaats van latest, voer vervolgens docker compose pull en docker compose up -d uit en monitor het logbestand voor de migratie. Houd de Postgres-tag vastgezet op de major-versie, omdat de server weigert een datadirectory te openen die door een andere major-versie is geschreven.