SSD Nodes Learn Hosting plans →
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-28

Installare Planka su VPS con Docker Compose

Guida al deploy di Planka con Docker Compose, Postgres e Traefik: configura le variabili admin e correggi BASE_URL, l'impostazione che può bloccare il login.

Cosa ottieni eseguendo Planka in self-hosting

Il self-hosting di Planka fornisce 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 tuo controllo. Non ci sono limiti al numero di utenti né costi per utente, perché l’unico costo è quello del server. Questa guida esegue il deploy con Docker Compose dietro Traefik, usando Postgres per i dati e un volume denominato per ogni file caricato dagli utenti.

Il caso di riferimento è un team composto da due a cinque persone che sta uscendo dal piano gratuito di Trello. Se stai ancora decidendo quale bacheca utilizzare, leggi prima il confronto tra le alternative self-hosted a Trello. Questa guida presuppone che la scelta sia già stata fatta e tratta soltanto il deploy.

Ti serve un VPS con Docker Engine e il plugin Compose, oltre a un record DNS A che punti al VPS. Devi inoltre avere un’istanza di Traefik che gestisca già la terminazione TLS (Transport Layer Security) su quel server. Se Traefik non è ancora disponibile, configura prima un reverse proxy Traefik davanti a diverse applicazioni Compose e leggi le basi di Docker Compose per un VPS se il file seguente non ti è familiare.

Di quante risorse VPS ha bisogno Planka?

Il progetto non pubblica un requisito hardware minimo. Considerate quindi ogni 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 valore predefinito prudenziale del provider, non un requisito misurato dal progetto. È sovradimensionato per una bacheca utilizzata da cinque persone.

L'installazione effettiva è contenuta: un processo Node.js gestisce l'API e il frontend compilato, mentre un processo Postgres contiene i dati. Nel container Planka viene eseguito anche un piccolo processo proxy che filtra le richieste in uscita. Un piano con 1 vCPU e 2 GB è sufficiente per una bacheca utilizzata da due-cinque persone; la maggior parte della memoria libera viene usata da Postgres come cache. Una bacheca genera un carico ridotto. Se lo stesso VPS deve ospitare anche i documenti del team, dimensionatelo prima per quell'applicazione: eseguire AFFiNE come workspace in stile Notion richiede autonomamente un paio di gigabyte prima che Planka ne richieda altri.

Dimensionate il disco prima della memoria, perché gli allegati sono l'elemento che cresce. Misurate la vostra istanza invece di affidarvi a questo paragrafo:

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

Il primo comando mostra la memoria e la CPU utilizzate in tempo reale da ogni container. Il secondo mostra quanto spazio contiene ogni volume. Eseguite entrambe le misurazioni dopo una normale settimana lavorativa, non il giorno dell'installazione, perché una bacheca inattiva non fornisce informazioni utili sul carico del team.

Scrivere il file Compose

Create the directory and take ownership of it, so you never have to edit these files through sudo.

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

Generate the secrets into a .env file beside the Compose file. Compose reads that file automatically and substitutes the values.

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 deliberate. A hex string holds only digits and the letters a to f, so it cannot break the DATABASE_URL connection string it gets pasted into. A base64 password with a slash or an at sign in it produces a connection error that reads like a wrong hostname, and that costs you an hour. The wider pattern is covered in tenere i secret fuori dal file Compose.

Now docker-compose.yml. Replace kanban.example.com with your own hostname in both places it appears.

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:

Four decisions in that file are worth explaining, because they are the ones people change and then regret.

  • There is no ports: block on the Planka service. Traefik reaches the container across the proxy network, so port 1337 is never published on the host. Publishing it would give anyone a way around your proxy and your certificate.
  • loadbalancer.server.port=1337 names the port inside the container. Planka listens on 1337, and the upstream example only reaches it on 3000 because it maps the port to the host. There is no host mapping here, so Traefik has to be told the container port.
  • condition: service_healthy pairs with the Postgres healthcheck. Without it Planka starts before the database accepts connections, fails its first query and exits, which looks like a crash loop. The mechanics are in healthcheck Compose e ordine di avvio.
  • The database service is named postgres on purpose. Planka 2 routes its own outgoing requests through an internal filter whose default block list is localhost,postgres. Rename the service and you quietly remove your database from that list.

Check that Compose can see your secrets before you start anything:

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

That prints the file with .env values already substituted. An empty value means Compose is not reading the .env file, usually because you are running the command from a different directory.

Cosa fanno realmente le variabili di bootstrap dell'amministratore

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

All'avvio, Planka cerca un utente corrispondente a DEFAULT_ADMIN_EMAIL. Se non ne trova uno, ne crea uno usando la password, il nome visualizzato e il nome utente impostati insieme a quella variabile. Questo avviene al primo avvio su un database vuoto, quindi queste variabili eseguono il bootstrap di un account, ma non lo gestiscono.

DEFAULT_ADMIN_EMAIL svolge una seconda funzione che spesso crea confusione. Finché la variabile resta impostata, l'account indicato non può essere modificato o eliminato dall'interfaccia da nessuno. È una protezione contro il blocco dell'accesso e spiega anche perché non è possibile rinominare quell'account o modificarne l'indirizzo email dall'interfaccia. Rimuovi la variabile e riavvia: l'account diventerà un normale amministratore, modificabile come gli altri.

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. Accedi, cambia la password dall'interfaccia, elimina quella riga, quindi esegui di nuovo 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 un nome utente facoltativo, quindi scrive direttamente l'utente nel database. La password non viene mai inserita nel file Compose né nell'ambiente del container. Usa questo metodo se più persone hanno accesso shell al VPS. Il comando avvia prima Postgres a causa di depends_on, quindi funziona anche su uno stack che non è mai stato avviato.

Entrambi i metodi lasciano a te la gestione manuale delle password di Planka. Se queste sono il quarto insieme di credenziali che il team deve gestire, Planka può invece delegare gli accessi a un provider OIDC come Authentik eseguito come server di single sign-on personale, mantenendo l'amministratore di bootstrap come account di emergenza da usare 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, incluso 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. Di conseguenza, un valore errato di BASE_URL non produce 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 compare. Aprendo la console per sviluppatori del browser, si vedono richieste a /socket.io/ che falliscono, perché al client è stato indicato di aprire la connessione in tempo reale verso localhost:3000. Sul laptop, quell'indirizzo non corrisponde ad alcun servizio.

TRUST_PROXY=true è l'altra parte dello stesso problema. Planka si trova dietro Traefik, quindi ogni richiesta gli arriva 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. Di conseguenza considera la connessione non sicura e tratta tutti i client come se provenissero dallo stesso indirizzo IP. Quando TRUST_PROXY è impostato, l'applicazione legge questi header e usa lo stesso schema del browser.

Traefik esegue il proxy delle connessioni WebSocket senza configurazione aggiuntiva. Questo è uno dei motivi per preferirlo in questo caso. Con nginx, socket.io richiede un blocco location dedicato che contenga 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 la modifica congiunta di due elementi: il valore BASE_URL e la regola Host() di Traefik. Se si modifica un elemento e si dimentica l'altro, l'indicatore di caricamento torna a bloccarsi. Pubblicare Planka da un sottopercorso come https://example.com/planka è supportato dalla versione 2.1.0, rilasciata a marzo 2026. Con i tag precedenti, assegnargli un sottodominio dedicato.

Dove Planka conserva allegati e avatar

Planka 2 conserva 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. Per questo, un file Compose copiato da una guida precedente monta percorsi che non esistono più e lascia senza mount la directory che contiene i dati reali.

Quel mount è ciò che determina se una bacheca sopravvive a un aggiornamento oppure no. Se /app/data non si trova su un volume, i file caricati finiscono nel layer scrivibile del container. Questo layer viene eliminato quando il container viene ricreato, e il container viene ricreato ogni volta che si modifica il tag dell'immagine. La bacheca viene ripristinata correttamente, tutte le schede sono presenti, ma ogni link agli allegati non funziona più, perché le righe del database continuano a puntare a file che non esistono.

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 opzioni è illustrato in bind mount e volumi denominati.

Se gli allegati superano lo spazio disponibile nel piano scelto, Planka può scriverli in uno storage compatibile con S3 tramite S3_ENDPOINT, S3_BUCKET e le variabili per le chiavi corrispondenti. È possibile indicare un bucket in hosting oppure un object store MinIO self-hosted su un altro server. Decidi questa configurazione 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 continuamente, il primo controllo deve riguardare 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, senza affidarti soltanto al log:

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

Un elenco delle tabelle che include board e card indica che le migrazioni sono state eseguite. Il messaggio "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 raggiunge il container. Un errore 404 restituito da Traefik indica che le label del router non corrispondono, nella maggior parte dei casi perché il container non è collegato alla rete proxy. Ora apri il sito ed esegui l'accesso con l'account amministratore.

Esegui un pg_dump prima di ogni aggiornamento di versione

La board viene archiviata in due store distinti, quindi il backup deve includere 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"

-T non è facoltativo. Senza questa opzione, Compose alloca uno pseudo-terminale e il livello del terminale riscrive i caratteri di fine riga nel flusso. Il risultato è un file di dump che si interrompe durante il ripristino. L’errore compare settimane dopo, nel momento peggiore.

Passa quindi agli upload. Individua prima il nome reale 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 repository, e la documentazione ufficiale prevede che vengano eseguiti tramite un cron job notturno. Entrambi gli approcci sono validi. Non è invece valido un backup che non hai mai ripristinato. Esegui quindi una volta il ripristino su una VPS di test e verifica di riuscire ad accedere e ad aprire un allegato. La stessa coppia di store è presente in ogni applicazione Compose che accetta upload. Se in seguito installi Chatwoot sullo stesso server del tuo help desk, la procedura che hai definito qui sarà riutilizzabile con poche modifiche, limitate soprattutto ai nomi dei volumi.

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

Blocca i tag e leggi le note di rilascio

Entrambi i tag delle immagini in quel file sono bloccati 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, perché descrivono le modifiche 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 accidentalmente. Qui bloccare il tag è semplice perché il progetto upstream pubblica immagini. Quando un progetto non ne pubblica, devi applicare la stessa disciplina con un passaggio aggiuntivo, come nel caso di openGym compilato sul server a partire da un tag git estratto.

postgres:16-alpine è bloccato a una versione principale per un motivo più importante. Postgres scrive la propria directory dei dati in un formato associato alla versione principale 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 principale di Postgres richiede un dump dalla versione precedente e un ripristino in una nuova directory dei dati sulla versione aggiornata. È un'attività pianificata, da eseguire con lo stack arrestato, non un effetto collaterale del download di un'immagine.

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

Modalità di errore e stringhe visualizzate

Planka si riavvia in un ciclo e il log cita 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.

L'accesso 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 a /socket.io/.

I caricamenti non funzionano, mentre tutto il resto funziona. Un bind mount appartiene a 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 configurata su 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 è collegato alla rete proxy oppure la regola Host() non corrisponde al record DNS. docker compose config mostra le label dopo la sostituzione dei valori: è qui che diventano visibili gli errori di digitazione.

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 indirizzato 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. Consultare le note di rilascio ed eseguire un 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. Gli stack Compose che si ripristinano 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 live non è stata stabilita. Planka costruisce l'URL WebSocket a partire da BASE_URL. Se questa variabile indica ancora http://localhost:3000 mentre raggiungi il sito tramite https://kanban.example.com, il browser tenta di aprire un socket verso un indirizzo che non esiste sulla macchina. La console per sviluppatori mostra richieste non riuscite a /socket.io/. Imposta BASE_URL sull'indirizzo pubblico esatto, senza slash finale, aggiungi TRUST_PROXY=true affinché l'applicazione tenga conto dell'header X-Forwarded-Proto del reverse proxy, quindi esegui docker compose up -d.

Come si crea il primo utente amministratore di Planka?

Dalla versione 1.13 non viene creato automaticamente alcun amministratore. Puoi impostare DEFAULT_ADMIN_EMAIL con le variabili corrispondenti per password, nome e username e avviare lo stack, oppure eseguire docker compose run --rm planka npm run db:create-admin-user e rispondere alle richieste. Il comando interattivo è l'opzione più sicura 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 archivia Planka gli allegati e gli avatar?

In Planka 2, tutti i file caricati vengono salvati in /app/data all'interno del container, inclusi allegati, avatar degli utenti e sfondi delle bacheche. Monta questo percorso su un volume denominato. Se il volume non è montato, i file restano nel livello scrivibile del container e vengono eliminati alla successiva ricreazione del container, che avviene a ogni aggiornamento dell'immagine. Funziona anche un bind mount, ma il processo Node viene eseguito con UID 1000. Esegui quindi sudo chown -R 1000:1000 sulla directory dell'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 nelle pagine dei provider è un'impostazione predefinita del provider, non una misurazione, ed è sovradimensionato per una bacheca di piccole dimensioni. Il carico completo consiste in un processo Node e un processo Postgres. Per questo, un piano con 1 vCPU e 2 GB è sufficiente per un team da due a cinque persone. Esegui docker stats --no-stream dopo una settimana normale e dimensiona l'istanza in base ai tuoi dati. Controlla il disco più attentamente della memoria, perché sono gli allegati a occupare spazio nel tempo.

Come si aggiorna Planka senza perdere i dati?

Esegui il dump del database e archivia il volume degli upload subito prima dell'aggiornamento, non utilizzando la pianificazione della notte precedente. Usa docker compose exec -T postgres pg_dump -U planka -d planka > planka-db.sql e mantieni -T, in modo che lo pseudo-terminale non corrompa l'output reindirizzato. Leggi le note di rilascio per ogni versione saltata, modifica il tag dell'immagine impostandolo su una release specifica invece di latest, quindi esegui docker compose pull e docker compose up -d e monitora il log per verificare la migrazione. Mantieni il tag di Postgres bloccato sulla relativa versione major, perché il server rifiuta di aprire una directory dati scritta da una versione major diversa.