SSD Nodes Learn 🎉 VPS da $5.50/mese
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-13

Come installare Planka self-hosted con Docker Compose

Guida al deployment di Planka su VPS con Docker Compose: Postgres, Traefik, variabili per creare l'amministratore e BASE_URL, che può causare errori di login.

Cosa si ottiene con il self-hosting di Planka

Il self-hosting di Planka offre al team una bacheca Kanban con il modello di schede, liste ed etichette già noto agli utenti di Trello, eseguita su un VPS sotto il vostro controllo. Non ci sono limiti al numero di utenti né costi per singolo utente, perché l’unico costo è quello del server. Questa guida esegue il deployment con Docker Compose dietro Traefik, usando Postgres per i dati e un volume denominato per ogni file caricato dagli utenti.

Il destinatario di questa guida è un team composto da due a cinque persone che sta superando il piano gratuito di Trello. Se state ancora decidendo quale bacheca utilizzare, leggete prima il confronto tra le alternative self-hosted a Trello. Questa guida presuppone che la scelta sia già stata effettuata e tratta soltanto il deployment.

È necessario un VPS con Docker Engine e il plugin Compose, oltre a un record DNS A che punti al VPS. È inoltre necessaria un’istanza di Traefik che gestisca già la terminazione TLS (Transport Layer Security) sul server. Se Traefik non è ancora disponibile, configurate prima un reverse proxy Traefik davanti a più applicazioni Compose e consultate le basi di Docker Compose per un VPS se il file riportato di seguito non vi è familiare.

Di quante risorse VPS ha bisogno Planka?

Il progetto non pubblica requisiti hardware minimi. Considera quindi qualsiasi valore riportato come un punto di partenza, non come una misurazione. Il valore di 2 vCPU e 4 GB ripetuto nelle pagine dei provider è un'impostazione predefinita comoda del provider, non un requisito misurato dal progetto. È sovradimensionato per una bacheca utilizzata da cinque persone.

L'esecuzione effettiva è contenuta: un processo Node.js serve l'API e il frontend compilato, mentre un processo Postgres contiene i dati. Nel container Planka viene eseguito anche un terzo processo proxy di piccole dimensioni, che filtra le richieste in uscita. Un piano con 1 vCPU e 2 GB è sufficiente per una bacheca utilizzata da due a cinque persone; la maggior parte della memoria disponibile viene usata da Postgres come cache.

Dimensiona il disco prima della memoria, perché gli allegati sono l'elemento che cresce nel tempo. Misura la tua istanza invece di affidarti a questo paragrafo:

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

Il primo comando mostra in tempo reale l'utilizzo di memoria e CPU per ogni container. Il secondo mostra quanto spazio occupa ogni volume. Esegui entrambe le misurazioni dopo una normale settimana di lavoro, non il giorno dell'installazione: una bacheca inattiva non fornisce informazioni utili sull'utilizzo del team.

Scrivere il file Compose

Create la directory e assegnale la proprietà corretta, così non dovrai mai modificare questi file tramite sudo.

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

Genera i secret in un file .env accanto al file Compose. Compose legge automaticamente questo file e sostituisce i valori.

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 è una scelta intenzionale. Una stringa esadecimale contiene solo cifre e lettere da a a f, quindi non può danneggiare la stringa di connessione DATABASE_URL in cui viene inserita. Una password base64 contenente una barra o una chiocciola genera un errore di connessione che sembra indicare un hostname errato, con una conseguente perdita di tempo. Il principio generale è descritto in mantenere i secret fuori dal file Compose.

Ora docker-compose.yml. Sostituisci kanban.example.com con il tuo hostname in entrambe le occorrenze.

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:

Nel file ci sono 4 scelte che meritano una spiegazione, perché sono quelle che spesso vengono modificate causando problemi.

  • Nel servizio Planka non è presente alcun blocco ports:. Traefik raggiunge il container attraverso la rete proxy, quindi la porta 1337 non viene mai pubblicata sull'host. Pubblicarla permetterebbe a chiunque di aggirare il proxy e il certificato.
  • loadbalancer.server.port=1337 indica la porta interna del container. Planka è in ascolto sulla porta 1337 e l'esempio upstream la raggiunge sulla porta 3000 solo perché esegue il mapping della porta sull'host. Qui non esiste alcun mapping verso l'host, quindi è necessario indicare a Traefik la porta del container.
  • condition: service_healthy è associato all'healthcheck di Postgres. Senza questa impostazione, Planka si avvia prima che il database accetti le connessioni, non riesce a eseguire la prima query e termina. Il comportamento sembra un ciclo continuo di arresto e riavvio. I dettagli sono descritti in healthcheck di Compose e ordine di avvio.
  • Il servizio database si chiama intenzionalmente postgres. Planka 2 inoltra le proprie richieste in uscita attraverso un filtro interno, il cui elenco predefinito di blocco è localhost,postgres. Se rinomini il servizio, rimuovi silenziosamente il database da quell'elenco.

Verifica che Compose possa leggere i tuoi secret prima di avviare qualsiasi servizio:

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

Il comando stampa il file con i valori .env già sostituiti. Un valore vuoto indica che Compose non sta leggendo il file .env, di solito perché il comando viene eseguito da una directory diversa.

Cosa fanno realmente le variabili di bootstrap dell’amministratore

A partire da Planka 1.13 non viene creato automaticamente alcun amministratore, quindi un database appena inizializzato non contiene utenti che possano effettuare l’accesso. Il gruppo DEFAULT_ADMIN_* è uno dei due modi per risolvere il problema.

All'avvio, Planka cerca un utente che corrisponda a DEFAULT_ADMIN_EMAIL. Se non lo trova, ne crea uno usando la password, il nome visualizzato e il nome utente configurati insieme a quella variabile. Questo avviene al primo avvio su un database vuoto, quindi queste variabili servono per eseguire il bootstrap di un account, non per amministrarlo.

DEFAULT_ADMIN_EMAIL svolge anche una seconda funzione, che spesso causa confusione. Finché la variabile rimane impostata, nessuno può modificare o eliminare dall'interfaccia l'account indicato, nemmeno un altro amministratore. È una protezione contro il blocco dell'accesso e spiega anche perché non è possibile rinominare l'account o modificarne l'indirizzo email dall'interfaccia. Rimuovi la variabile e riavvia il servizio: l'account tornerà a essere un normale amministratore, modificabile come qualsiasi altro.

La riga della password richiede particolare attenzione. Qualsiasi valore sotto environment: è leggibile da chiunque possa eseguire docker inspect sul container, quindi DEFAULT_ADMIN_PASSWORD non dovrebbe rimanere lì in modo permanente. Effettua l'accesso, cambia la password dall'interfaccia, elimina quella riga, quindi esegui nuovamente docker compose up -d.

La soluzione più pulita evita completamente le variabili. Commenta l'intero gruppo DEFAULT_ADMIN_*, quindi crea l'account in modo interattivo:

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

Il comando richiede email, password, nome visualizzato e, facoltativamente, un nome utente, quindi scrive direttamente l'utente nel database. La password non viene mai salvata nel file Compose né nell'ambiente del container. Usa questa soluzione se più persone hanno accesso alla shell della VPS. Il comando avvia prima Postgres a causa di depends_on, quindi funziona anche su uno stack che non è mai stato avviato.

Entrambe le soluzioni richiedono di gestire manualmente le password di Planka. Se questo rappresenta il quarto insieme di credenziali raccolto dal team, Planka può invece delegare gli accessi a un provider OIDC come Authentik eseguito come server SSO personale, mantenendo l'amministratore di bootstrap come account di emergenza da utilizzare quando il provider non è disponibile.

Perché BASE_URL interrompe gli accessi quando non corrisponde al nome host

BASE_URL è l'indirizzo esatto che gli utenti inseriscono nel browser, con lo schema e senza slash finale. Per questo stack è https://kanban.example.com. Planka genera i propri link e la connessione WebSocket a partire da questo valore. Un valore errato di BASE_URL non produce quindi un errore chiaro. La pagina viene caricata, ma il caricamento non termina mai.

Il caso più comune è questo: si copia l'esempio upstream, si lascia BASE_URL=http://localhost:3000 invariato e si raggiunge il sito tramite HTTPS usando il dominio reale. Il modulo di accesso viene inviato e le credenziali vengono accettate. La bacheca non viene visualizzata. Nella console per sviluppatori del browser si vedono richieste a /socket.io/ non riuscite, perché al client è stato indicato di aprire la connessione in tempo reale verso localhost:3000, ma sul laptop quell'indirizzo non corrisponde a nulla.

TRUST_PROXY=true è l'altra parte dello stesso problema. Planka si trova dietro Traefik, quindi ogni richiesta lo raggiunge dall'indirizzo del proxy tramite HTTP non cifrato all'interno della rete Docker. Senza TRUST_PROXY, l'applicazione ignora gli header X-Forwarded-Proto e X-Forwarded-For impostati da Traefik. Ritiene quindi che la connessione non sia sicura e considera tutti i client come un unico indirizzo IP condiviso. Quando è impostato, l'applicazione legge questi header e concorda con il browser sullo schema della connessione.

Traefik esegue il proxy dei WebSocket senza configurazione aggiuntiva. Questo è uno dei motivi per cui è una scelta adatta in questo caso. Con nginx, socket.io richiede un blocco location dedicato che includa proxy_set_header Upgrade $http_upgrade e proxy_set_header Connection "upgrade". In caso contrario, si ottiene lo stesso indicatore di caricamento bloccato, ma per una causa diversa.

Spostare la bacheca su un nuovo nome host in un secondo momento richiede di modificare insieme due elementi: il valore BASE_URL e la regola Host() di Traefik. Se si modifica un solo elemento e si dimentica l'altro, l'indicatore di caricamento torna a bloccarsi. La pubblicazione di Planka in un sottopercorso come https://example.com/planka è supportata dalla versione 2.1.0, rilasciata a marzo 2026. Con i tag precedenti, assegnare all'applicazione un sottodominio dedicato.

Dove Planka conserva allegati e avatar

Planka 2 salva tutto ciò che un utente carica in un unico percorso all'interno del container: /app/data. Gli allegati, gli avatar degli utenti e le immagini di sfondo delle bacheche si trovano tutti in questo percorso. La versione 1 usava tre directory separate. Di conseguenza, un file Compose copiato da una guida precedente monta percorsi che non esistono più e lascia senza mount la directory che contiene i dati effettivi.

Questo singolo mount determina se una bacheca supera un aggiornamento senza problemi oppure se richiede un intervento impegnativo. Se /app/data non si trova su un volume, i file caricati vengono salvati nel writable layer del container. Questo layer viene eliminato quando il container viene ricreato. Il container viene ricreato ogni volta che si modifica il tag dell'immagine. La bacheca torna a essere visibile correttamente, tutte le schede sono presenti, ma ogni link agli allegati non funziona più, perché le righe del database continuano a fare riferimento a file che non esistono più.

Il volume denominato nel file Compose precedente evita questo problema. Anche un bind mount funziona e rende più semplice il backup dei file con gli strumenti standard, ma richiede un passaggio aggiuntivo. Il processo Node all'interno del container viene eseguito con UID 1000. Di conseguenza, una directory sull'host di proprietà di root genera un errore di autorizzazione al primo caricamento:

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

Il compromesso tra le due soluzioni è descritto in bind mount e volumi denominati.

Se gli allegati superano lo spazio disponibile nel piano scelto, Planka può salvarli su uno storage compatibile con S3 tramite S3_ENDPOINT, S3_BUCKET e le variabili per le chiavi corrispondenti. È possibile usare un bucket in hosting oppure un object store MinIO self-hosted su un altro server. Questa decisione va presa prima che il team riempia la bacheca, perché l'impostazione si applica ai nuovi caricamenti.

Avviare lo stack e verificare il funzionamento

docker compose pull
docker compose up -d
docker compose ps

docker compose ps dovrebbe mostrare postgres come healthy e planka come running. Se Planka si riavvia in un ciclo continuo, il primo controllo da eseguire riguarda la connessione al database, non l'applicazione.

docker compose logs -f planka

Un primo avvio corretto esegue le migrazioni del database e indica poi che il server è in ascolto sulla porta 1337. Verifica direttamente con Postgres che lo schema sia stato realmente creato, invece di affidarti ai log:

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

Un elenco di tabelle che include board e card indica che le migrazioni sono state eseguite. "Did not find any relations" indica che Planka non si è mai connesso. Confronta quindi DATABASE_URL con i valori POSTGRES_USER e POSTGRES_PASSWORD nel tuo .env.

Controlla quindi il percorso dalla tua macchina, non dal VPS:

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

HTTP/2 200 indica che Traefik dispone di un certificato e riesce a raggiungere il container. Un errore 404 restituito da Traefik indica che le label del router non corrispondono. Nella maggior parte dei casi, il container non è collegato alla rete proxy. Ora apri il sito e accedi con l'account amministratore.

Esegui pg_dump prima di ogni aggiornamento di versione

Due archivi separati contengono i dati della bacheca, quindi il backup deve includerli entrambi: il database Postgres e il volume planka-data. Esegui il dump del database mentre lo stack è in esecuzione.

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

L'opzione -T è obbligatoria. Senza questa opzione, Compose alloca uno pseudo-terminale e il livello del terminale riscrive i caratteri di fine riga nel flusso. Il file di dump risultante non può quindi essere ripristinato completamente. L'errore si manifesta settimane dopo, nel momento peggiore.

Poi esegui il backup degli upload. Prima individua il nome effettivo del volume, perché Compose lo antepone al nome della directory del progetto.

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 .

Il progetto include anche docker-backup.sh e docker-restore.sh nel proprio repository; la documentazione ufficiale indica di eseguirli tramite un cron job notturno. Entrambi gli approcci sono validi. Non è invece valido un backup che non hai mai ripristinato. Esegui quindi un ripristino su una VPS temporanea e verifica di poter effettuare l'accesso e aprire un allegato.

Esegui il dump immediatamente prima di ogni modifica della versione. Un backup della notte precedente non equivale a un backup eseguito prima della migrazione che stai per avviare.

Fissa i tag e leggi le note di rilascio

Entrambi i tag delle immagini in quel file sono fissati intenzionalmente.

ghcr.io/plankanban/planka:2.1.1 è una release specifica, aggiornata ad agosto 2026. latest cambia ogni volta che il progetto upstream pubblica una nuova versione, quindi un normale docker compose pull può introdurre una migrazione dello schema in un momento non pianificato. Leggi le note di rilascio prima di modificare quel numero: è lì che vengono descritti i cambiamenti incompatibili e le correzioni di sicurezza. La versione 2.0.3 è stata pubblicata come release di sicurezza. È esattamente il tipo di aggiornamento che devi leggere, invece di installarlo per errore.

postgres:16-alpine è fissato a una versione major per un motivo più importante. Postgres scrive la propria directory dei dati in un formato legato alla versione major e il server rifiuta di aprire una directory scritta da una versione diversa. Scrivi postgres:latest, lascia che il tag passi alla versione 17 e il container non si avvierà:

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.

Non viene perso nulla e neppure un riavvio risolve il problema. Il passaggio a una nuova versione major di Postgres richiede un dump dalla versione precedente e un ripristino in una nuova directory dei dati sulla versione nuova. È un'operazione pianificata, da eseguire con lo stack arrestato, non un effetto collaterale del download di un'immagine.

Se stai migrando un'installazione Planka 1.x esistente invece di partire da zero, l'aggiornamento segue una procedura documentata specifica nella documentazione del progetto. Non è possibile tornare alla versione 1 senza un backup creato in precedenza.

Modalità di errore e stringhe visualizzate

Planka si riavvia in loop e il log indica il database. Le credenziali in DATABASE_URL non corrispondono all'ambiente Postgres. POSTGRES_PASSWORD viene applicata solo quando la directory dei dati viene inizializzata per la prima volta. Correggere la variabile dopo un primo avvio errato non cambia nulla. È necessario rimuovere il volume db-data e avviare nuovamente lo stack.

Il login riesce, ma la bacheca non viene mai caricata. BASE_URL non corrisponde all'indirizzo visualizzato nella barra del browser oppure TRUST_PROXY è assente. La console del browser mostra richieste non riuscite verso /socket.io/.

I caricamenti non riescono, mentre tutto il resto funziona. Un bind mount è di proprietà di root. Eseguire sudo chown -R 1000:1000 sulla directory dell'host e riavviare il container.

Gli allegati sono scomparsi dopo un aggiornamento. /app/data non era associata a un volume, quindi i file si trovavano nel layer del container sostituito dall'aggiornamento. Ripristinare i file da un backup, quindi aggiungere il volume prima di modificare nuovamente il tag dell'immagine.

Traefik restituisce 404. Il container non si trova sulla rete proxy oppure la regola Host() non corrisponde al record DNS. docker compose config mostra le label dopo la sostituzione dei valori: è qui che gli errori di battitura diventano visibili.

Le notifiche o i webhook non arrivano mai. Planka 2 invia le richieste HTTP in uscita tramite un filtro interno, e l'elenco di blocco predefinito include localhost e postgres. Un webhook diretto a un altro container sullo stesso host può essere bloccato intenzionalmente. Modificare OUTGOING_ALLOWED_HOSTS invece di rimuovere il filtro.

Dopo l'avvio, il carico operativo è ridotto. Controllare le note di rilascio ed eseguire il dump del database prima di ogni aggiornamento. Un riavvio ripristina automaticamente lo stack grazie a restart: unless-stopped, purché il servizio Docker sia abilitato all'avvio del sistema. Stack Compose che vengono ripristinati dopo un riavvio descrive i casi in cui questo non avviene.

FAQ

Perché Planka continua a caricarsi dopo l'accesso?

Le credenziali sono state accettate, ma la connessione in tempo reale non è stata stabilita. Planka costruisce l'URL WebSocket a partire da BASE_URL. Se quella variabile contiene ancora http://localhost:3000 mentre si raggiunge il sito all'indirizzo https://kanban.example.com, il browser tenta di aprire un socket verso un indirizzo che non esiste sul computer. La console per sviluppatori mostra richieste non riuscite verso /socket.io/. Impostare BASE_URL sull'indirizzo pubblico esatto, senza slash finale, aggiungere TRUST_PROXY=true affinché l'applicazione utilizzi l'header X-Forwarded-Proto del reverse proxy, quindi eseguire docker compose up -d.

Come si crea il primo utente amministratore di Planka?

A partire dalla versione 1.13 non viene creato automaticamente alcun amministratore. È possibile impostare DEFAULT_ADMIN_EMAIL con le variabili corrispondenti per password, nome e nome utente, quindi avviare lo stack, oppure eseguire docker compose run --rm planka npm run db:create-admin-user e rispondere alle richieste. Il comando interattivo è più sicuro su un server condiviso, perché la password non entra nell'ambiente del container, dove docker inspect può leggerla. Lasciare DEFAULT_ADMIN_EMAIL impostata in seguito impedisce di modificare o eliminare quell'account dall'interfaccia.

Dove memorizza Planka gli allegati e gli avatar?

In Planka 2, tutti i file caricati vengono memorizzati nel container sotto /app/data, inclusi allegati, avatar degli utenti e sfondi delle bacheche. Montare quel percorso su un volume denominato. Se il volume non è montato, i file restano nel layer scrivibile del container e vengono eliminati alla successiva ricreazione del container, che avviene a ogni aggiornamento dell'immagine. Anche un bind mount è adatto, ma il processo Node viene eseguito con UID 1000. Eseguire quindi sudo chown -R 1000:1000 sulla directory host, altrimenti i caricamenti falliscono con un errore di autorizzazione.

Quanta RAM richiede Planka self-hosted?

Il progetto non pubblica un requisito hardware minimo. Il valore di 2 vCPU e 4 GB ripetuto sui siti di hosting è un'impostazione predefinita del provider, non il risultato di una misurazione, ed è sovradimensionato per una bacheca di piccole dimensioni. Il carico consiste interamente in un processo Node e un processo Postgres, quindi un piano con 1 vCPU e 2 GB è sufficiente per un team da due a cinque persone. Eseguire docker stats --no-stream dopo una settimana normale e dimensionare l'installazione in base ai propri dati. Controllare il disco più attentamente della memoria, perché sono gli allegati a crescere.

Come si aggiorna Planka senza perdere i dati?

Eseguire il dump del database e archiviare il volume degli upload immediatamente prima dell'aggiornamento, non utilizzando il backup programmato della notte precedente. Usare docker compose exec -T postgres pg_dump -U planka -d planka > planka-db.sql, mantenendo -T per evitare che lo pseudo-terminale corrompa l'output reindirizzato. Leggere le note di rilascio per ogni versione saltata, modificare il tag dell'immagine impostandolo su una release specifica invece di latest, quindi eseguire docker compose pull e docker compose up -d e monitorare il log durante la migrazione. Lasciare il tag di Postgres fissato alla relativa versione principale, perché il server rifiuta di aprire una directory dati scritta da una versione principale diversa.