SSD Nodes Learn Hosting plans →
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-10-18

Tailscale in Docker Compose senza porta pubblica

L'esempio Compose di Tailscale pubblica una porta sull'host. Usa un sidecar Tailscale: l'app resta senza porte esposte ed è raggiungibile solo con il nome della tailnet.

Eseguire Tailscale in uno stack Docker Compose

Eseguire Tailscale in Docker Compose richiede due servizi. Uno è il container tailscale/tailscale, che si connette alla tailnet. L'altro è il container dell'applicazione, che condivide lo spazio dei nomi di rete del primo container invece di pubblicare una porta sull'host. Il risultato è un servizio che il laptop può aprire per nome e che non è raggiungibile da Internet.

Una tailnet è la rete privata che Tailscale crea tra i dispositivi con cui hai effettuato l'accesso. Se il termine è nuovo, leggi prima che cos'è Tailscale e come connette due macchine. Questa guida presuppone che Docker Engine e il plugin Compose v2 siano già operativi, come descritto in creare il primo stack Docker Compose su un VPS.

L’esempio del fornitore e la porta che lascia aperta

La guida Compose ufficiale di Tailscale pubblica uno stack molto simile a questo.

services:
  tailscale:
    image: tailscale/tailscale:latest
    container_name: tailscale
    hostname: tailscale-nginx
    environment:
      - TS_AUTHKEY=tskey-auth-REPLACE-ME
      - TS_STATE_DIR=/var/lib/tailscale
    volumes:
      - ./tailscale-state:/var/lib/tailscale
    cap_add:
      - net_admin
      - net_raw
    restart: unless-stopped

  nginx:
    image: nginx:latest
    container_name: nginx_server
    ports:
      - "8080:80"
    depends_on:
      - tailscale
    restart: unless-stopped

Ogni riga del servizio tailscale è corretta. TS_AUTHKEY autentica il nodo. TS_STATE_DIR indica a tailscaled dove scrivere il proprio stato e il bind mount mantiene tale stato sul disco. Il secondo servizio è il problema.

I due container si trovano sulla rete bridge predefinita di Compose, ciascuno con il proprio indirizzo. Questo è il comportamento normale descritto in come le reti Compose collegano i container usando il nome del servizio. Il container tailscale si è unito al tailnet per conto proprio e non inoltra alcun traffico al container nginx. Di conseguenza, il tailnet non ha alcun percorso verso nginx e l’unico punto di accesso dall’esterno del VPS è la porta 8080 dell’host.

Una porta pubblicata viene associata a 0.0.0.0, a meno che non si specifichi un indirizzo davanti alla porta oppure si imposti un indirizzo di binding predefinito diverso sulla rete. Su un VPS, quindi, la porta risponde sull’indirizzo IP pubblico. L’applicazione appartiene al tailnet solo nominalmente, mentre di fatto è esposta su Internet. Neppure un firewall sull’host risolve il problema, perché Docker inserisce le proprie regole di inoltro prima della catena di ufw. Questa è la trappola descritta in perché una regola deny di ufw non chiude una porta Docker pubblicata.

C’è un altro dettaglio da chiarire. L’esempio concede net_admin e net_raw, ma non mappa mai /dev/net/tun. TS_USERSPACE è impostato su true per impostazione predefinita, quindi il container esegue lo stack di rete in userspace e quelle due capability non hanno alcun effetto.

Sidecar: uno spazio dei nomi condiviso, nessuna porta pubblicata

Inserire l'applicazione nello spazio dei nomi di rete del container Tailscale con network_mode: service:tailscale. Entrambi i processi vedono quindi lo stesso loopback e lo stesso indirizzo tailnet, anche se vengono eseguiti in container separati.

services:
  tailscale:
    image: tailscale/tailscale:v1.102.5
    container_name: ts-nginx
    hostname: nginx-demo
    environment:
      - TS_AUTHKEY=${TS_AUTHKEY}
      - TS_HOSTNAME=nginx-demo
      - TS_STATE_DIR=/var/lib/tailscale
    volumes:
      - ./ts-state:/var/lib/tailscale
    restart: unless-stopped

  nginx:
    image: nginx:1.30.4-alpine
    network_mode: service:tailscale
    depends_on:
      - tailscale
    restart: unless-stopped

La chiave va inserita in un file .env accanto al file Compose, non nel file YAML. In questo modo il file sottoposto al commit non contiene secret. Una sola riga: TS_AUTHKEY=tskey-auth-.... Mantenere i secret fuori da un file Compose sottoposto al commit descrive il resto di questo approccio.

Avviare lo stack e verificare entrambe le parti.

docker compose up -d
docker compose exec tailscale tailscale status
docker compose logs --tail 20 tailscale

tailscale status dovrebbe visualizzare una riga per questo nodo con un indirizzo 100.x, seguita dagli altri computer nella tailnet. Da un laptop autenticato nella stessa tailnet, curl http://nginx-demo/ restituisce la pagina di benvenuto di nginx. Sul VPS stesso, sudo ss -lntp | grep 8080 non restituisce nulla perché nessuna porta è stata pubblicata.

Perché la porta 80 e non 8080: in modalità userspace, tailscaled inoltra una connessione in ingresso dal tunnel alla stessa porta su localhost. nginx è in ascolto sulla porta 80 nello spazio dei nomi condiviso, quindi la tailnet lo raggiunge sulla porta 80. Se si cambia la porta su cui l'applicazione è in ascolto, cambia anche la porta della tailnet. Questa tecnica di condivisione dello spazio dei nomi non è specifica di Tailscale. Le stesse domande sull'accesso all'host e al resto dello stack si presentano anche in un container Gluetun che gestisce la rete dei container adiacenti.

Quale chiave di autenticazione richiede il container?

Il tipo di chiave determina cosa accade al secondo avvio, quindi sceglietelo prima del deployment. Generatene una nella pagina Keys della console di amministrazione. La finestra di dialogo la mostra una sola volta.

  • Le chiavi monouso autenticano un singolo dispositivo. Uno stack ricreato senza la relativa directory di stato non tornerà operativo.
  • Le chiavi riutilizzabili autenticano un numero qualsiasi di dispositivi. È il tipo normalmente richiesto da uno stack Compose.
  • Le chiavi effimere contrassegnano il nodo per la rimozione automatica. Tailscale rimuove un dispositivo effimero da 30 a 60 minuti dopo la sua ultima attività.
  • Le chiavi pre-approvate saltano l'approvazione manuale del dispositivo. Questo è rilevante solo se l'approvazione dei dispositivi è attivata per il vostro tailnet.
  • Le chiavi con tag applicano un tag ACL come tag:container durante l'autenticazione. Il dispositivo non appartiene più a una persona e la scadenza della chiave è disabilitata per impostazione predefinita.

L'ultima riga è quella rilevante dal punto di vista operativo. Per impostazione predefinita, una chiave del nodo scade dopo 180 giorni. Un nodo scaduto viene rimosso dal tailnet finché una persona non lo autentica di nuovo oppure un amministratore non disabilita la scadenza della relativa chiave. Una chiave con tag elimina questa scadenza automatica. Per questo i tag esistono per server e container.

La scadenza di una chiave di autenticazione è un evento distinto e spesso viene confusa con quella della chiave del nodo. Le chiavi di autenticazione durano da 1 a 90 giorni, con 90 giorni come valore predefinito. Quando una chiave raggiunge la data di scadenza, non disconnette i dispositivi che ha già autenticato. Impedisce soltanto di aggiungerne di nuovi. Per un servizio di lunga durata, utilizzate una chiave riutilizzabile con tag e non effimera. Per uno stack che eliminate e ricreate spesso, ad esempio un ambiente di anteprima, una chiave effimera mantiene pulita la console di amministrazione senza richiedere eliminazioni manuali.

Perché il container torna a essere una macchina nuova?

Perché tailscaled conservava il proprio stato in una posizione non persistente. Se TS_STATE_DIR non è impostato, l'immagine conserva lo stato soltanto in memoria. Se è impostato ma il percorso non è montato, lo stato finisce nel layer scrivibile del container e docker compose down elimina il container.

L'identità del nodo risiede nella directory dello stato. Se la rendi persistente, il container mantiene il proprio nome e il proprio indirizzo 100.x tra i riavvii, insieme alla configurazione di servizio. Se la perdi, l'avvio successivo è un primo avvio: il container esegue di nuovo l'autenticazione con la stessa chiave e la console di amministrazione aggiunge una seconda macchina. Entrambe dichiarano l'hostname nginx-demo, quindi MagicDNS assegna alla più recente un suffisso numerico e ogni collegamento salvato punta al nodo non più attivo.

Devono essere vere due condizioni. TS_STATE_DIR=/var/lib/tailscale deve essere impostato, perché al di fuori di Kubernetes non ha un valore predefinito. Inoltre, quel percorso deve essere montato, tramite il bind mount precedente o un volume denominato; la scelta è descritta in bind mount e volumi denominati. Impostare una sola delle due opzioni è l'errore più comune. Un mount senza TS_STATE_DIR perde l'identità a ogni riavvio, mentre un TS_STATE_DIR senza mount fallisce senza messaggi evidenti: lo stack funziona correttamente fino al primo down.

Verificalo invece di dare per scontato che sia corretto.

docker compose down
ls -l ./ts-state
docker compose up -d
docker compose exec tailscale tailscale status

./ts-state dovrebbe già contenere tailscaled.state prima del secondo up e il nodo dovrebbe tornare a usare l'indirizzo precedente. Un indirizzo diverso indica che il mount non sta funzionando.

Blocca l’immagine e indica il tag utilizzato

tailscale/tailscale:latest segue la build stabile più recente. Un docker compose pull tra sei mesi scaricherà un tailscaled diverso e il successivo docker compose up -d ricreerà il container con codice che non hai mai scelto. Un semplice riavvio mantiene l’immagine precedente, quindi la modifica viene applicata quando il container viene ricreato, non al momento del pull. Lo stack precedente blocca v1.102.5, l’immagine a cui puntavano i tag stable e latest di Docker Hub il 10 October 2026. Il changelog di Tailscale elenca già il client 1.104.1, rilasciato il 7 October 2026. Prima di modificare il blocco, verifica quindi su Docker Hub la presenza di un tag immagine corrispondente. Docker Hub pubblica anche v1.102 per la linea di patch e un tag unstable, che non devi usare su un server.

Esegui l’aggiornamento intenzionalmente.

docker compose pull tailscale
docker compose up -d
docker compose exec tailscale tailscale version

Modificare il tag ed eseguire up -d ricrea il container. Non equivale a riavviarlo. Vale la pena leggere La differenza tra restart, up e rebuild prima di eseguire il debug di una modifica di versione che non è stata applicata.

Networking nello userspace e relativi costi

TS_USERSPACE è impostato su true per impostazione predefinita. Il container esegue quindi uno stack TCP/IP nello userspace e non accede mai a /dev/net/tun. Questo consente il funzionamento anche su host che non assegnano al container il dispositivo TUN. Il traffico in ingresso continua a funzionare perché le connessioni in ingresso dal tunnel vengono inoltrate alla stessa porta su localhost. Per questo il sidecar precedente non richiede né un dispositivo né capabilities.

Il costo riguarda il traffico in uscita. In modalità userspace l'applicazione non può semplicemente aprire un socket verso un altro nodo della tailnet. tailscaled fornisce invece un proxy SOCKS5 e un proxy HTTP. È quindi necessario impostare TS_SOCKS5_SERVER=localhost:1055 sul servizio tailscale e ALL_PROXY=socks5://localhost:1055 sull'applicazione, che deve rispettare questa configurazione. Qualsiasi applicazione che ignora le variabili d'ambiente del proxy non potrà raggiungere la tailnet.

Lo stack presenta alcuni limiti da conoscere. Vengono trasportati solo TCP e UDP, quindi gli altri protocolli IP, ad esempio SCTP, non passano. ICMP è limitato al ping, che il daemon ricostruisce; questo aggiunge una lieve latenza apparente. Le connessioni vengono terminate sul nodo e ristabilite verso la destinazione, quindi non sono end-to-end. Dalla versione 1.20 un nodo userspace può raggiungere un exit node o una route di subnet pubblicizzata da un altro nodo, ma solo tramite questi proxy; inoltre può pubblicizzare direttamente queste route.

Quando è necessario un traffico in uscita trasparente, passare al networking del kernel aggiungendo tre elementi al servizio tailscale.

    environment:
      - TS_USERSPACE=false
    devices:
      - /dev/net/tun:/dev/net/tun
    cap_add:
      - net_admin

Verificare prima che l'host possa fornirlo con test -c /dev/net/tun && echo ok. Su KVM il dispositivo è disponibile. Nella virtualizzazione basata su container che condivide il kernel dell'host, il dispositivo potrebbe non essere disponibile; in questo caso userspace è l'unica soluzione. Assegnare il dispositivo TUN al container se deve essere un router di subnet che pubblicizza un intervallo privato oppure un exit node per gli altri dispositivi, perché sono i ruoli che risentono maggiormente dei limiti della modalità userspace.

Raggiungere il servizio: pubblicarlo oppure usare il semplice nome MagicDNS

Il percorso più semplice è il nome MagicDNS. Da qualsiasi dispositivo della tailnet, http://nginx-demo/ funziona, così come il nome completo http://nginx-demo.your-tailnet.ts.net/. In questo caso HTTP non trasmette dati in chiaro sulla rete, perché WireGuard cifra il traffico tra i due nodi; cosa può e non può vedere il server di coordinamento definisce il limite di questa garanzia. Non essendoci un certificato, il browser contrassegna l’origine come non sicura e qualsiasi funzionalità web che richiede un contesto sicuro non viene eseguita.

L’altra opzione è Tailscale Serve, eseguito all’interno del container.

docker compose exec tailscale tailscale serve --bg localhost:80
docker compose exec tailscale tailscale serve status

Il servizio viene pubblicato su https://nginx-demo.your-tailnet.ts.net con un certificato fornito da Tailscale. Sia MagicDNS sia i certificati HTTPS devono essere abilitati nella pagina DNS della console di amministrazione; in caso contrario non esiste alcun nome da associare a un certificato. --bg scrive la configurazione nello stato di tailscaled che hai reso persistente, quindi la configurazione viene ripristinata insieme al container; tailscale serve reset la rimuove. TS_SERVE_CONFIG indica un file JSON, se preferisci mantenere questa configurazione nel repository anziché in un comando shell. Serve rimane all’interno della tailnet. Funnel è il comando distinto che espone lo stesso servizio su Internet, quindi leggi la differenza tra Serve e Funnel prima di eseguire uno dei due comandi.

Modalità di errore e messaggi visualizzati

Error response from daemon: conflicting options: port publishing and the container type network mode. Hai lasciato un blocco ports: sul servizio sidecar. Solo il container proprietario del namespace può pubblicare porte, mentre un'app accessibile soltanto tramite tailnet non deve pubblicarne nessuna. Elimina il blocco.

Il container dell'app è in esecuzione, ma non gli arriva nulla. Hai ricreato il servizio Tailscale separatamente. Il namespace che possedeva è stato eliminato insieme al servizio e l'app è collegata a una risorsa che non esiste più. Ricrea la coppia insieme usando docker compose up -d --force-recreate.

Nella console di amministrazione non compare alcun nodo. Leggi docker compose logs tailscale. Qui viene segnalata una chiave rifiutata. Una chiave monouso già utilizzata e una chiave scaduta impediscono entrambe al nodo di raggiungere la tailnet.

Il nodo è attivo, tailscale status è corretto e curl http://nginx-demo/ resta in attesa. L'app non è in ascolto dove pensi. Verificalo dall'interno del namespace condiviso usando docker compose exec nginx wget -qO- http://localhost/. Se anche questo controllo fallisce, il problema riguarda l'app, non Tailscale. Se invece ha esito positivo, l'app è associata a una sola interfaccia anziché a tutte.

La macchina è scomparsa dalla console un'ora dopo l'arresto dello stack. La chiave era effimera. La rimozione avviene da 30 a 60 minuti dopo l'ultima attività: è il comportamento previsto.

Dalla versione 1.78, l'immagine può esporre un endpoint /healthz senza autenticazione: imposta TS_ENABLE_HEALTH_CHECK=true, che resta in ascolto sulla porta TS_LOCAL_ADDR_PORT, il cui valore predefinito è [::]:9002. Configura un healthcheck Compose su questo endpoint, in modo che un nodo che non riesce ad autenticarsi venga segnalato come non integro invece di sembrare operativo. Se preferisci non dipendere affatto dai server di coordinamento Tailscale, lo stesso file Compose può comunicare con il tuo control plane tramite TS_EXTRA_ARGS=--login-server=https://headscale.example.com. Questo è il punto di partenza per eseguire Headscale come control server personale.

FAQ

Perché gli altri dispositivi della tailnet non riescono a raggiungere il mio container applicativo?

Perché il container Tailscale si è unito alla tailnet solo per se stesso. Se l'applicazione è in esecuzione sulla rete bridge predefinita di Compose con un proprio indirizzo, il nodo Tailscale non inoltra il traffico verso di essa. Dall'esterno dell'host, l'unico accesso possibile è tramite la porta pubblicata dell'host. Assegna all'applicazione network_mode: service:tailscale, in modo che condivida lo spazio dei nomi di rete del container Tailscale, quindi rimuovi il relativo blocco ports:. L'applicazione sarà raggiungibile sulla tailnet tramite la porta su cui è in ascolto.

È necessario usare /dev/net/tun per eseguire Tailscale in Docker Compose?

Non per gli accessi in ingresso. TS_USERSPACE è impostato su true per impostazione predefinita e, in questa modalità, tailscaled usa il proprio stack di rete e inoltra le connessioni in ingresso del tunnel alla stessa porta su localhost. Un sidecar funziona quindi senza dispositivo e senza capability aggiuntive. Sono necessari /dev/net/tun, TS_USERSPACE=false e net_admin quando il container deve aprire in modo trasparente connessioni in uscita verso la tailnet oppure agire come subnet router o exit node.

Devo usare una chiave di autenticazione effimera o riutilizzabile per uno stack Compose?

Usa una chiave riutilizzabile, con un tag e non effimera, per tutto ciò che deve restare in esecuzione a lungo. Per un dispositivo autenticato con una chiave dotata di tag, la scadenza della node key è disabilitata per impostazione predefinita. Il container non viene quindi rimosso dalla tailnet dopo 180 giorni in attesa che qualcuno esegua nuovamente l'accesso. L'aggiunta di un tag a un nodo già autenticato non modifica la scadenza finché il nodo non esegue nuovamente l'autenticazione. Scegli una chiave effimera solo per gli stack che elimini spesso, ad esempio gli ambienti di anteprima, perché Tailscale rimuove un dispositivo effimero da 30 a 60 minuti dopo la sua ultima attività e la console di amministrazione rimane pulita.

Perché il mio container appare come una nuova macchina a ogni riavvio?

La directory dello stato non viene mantenuta, quindi tailscaled parte senza un'identità e si autentica come un nodo completamente nuovo. Imposta TS_STATE_DIR=/var/lib/tailscale, che non ha un valore predefinito al di fuori di Kubernetes, e monta quel percorso su un bind mount o su un volume denominato. Senza TS_STATE_DIR, lo stato rimane in memoria e viene perso a ogni riavvio. Se lo imposti senza un mount, tutto sembra funzionare fino al primo docker compose down. Verifica che tailscaled.state esista nella directory montata quando lo stack è arrestato.