Cómo instalar Planka con Docker Compose en un VPS
Instala Planka con Docker Compose, Postgres y Traefik. Configura las variables de administrador y evita el error de inicio de sesión causado por BASE_URL.
Qué se obtiene al alojar Planka por cuenta propia
Alojar Planka por cuenta propia proporciona a tu equipo un tablero Kanban con el modelo de tarjetas, listas y etiquetas que ya conoce de Trello, ejecutándose en un VPS bajo tu control. No hay límites de puestos ni facturación por usuario, porque el único coste es el servidor. Esta guía lo implementa con Docker Compose detrás de Traefik, usa Postgres para los datos y un volumen con nombre para cada archivo que suban los usuarios.
Está pensada para un equipo de dos a cinco personas que deja el nivel gratuito de Trello. Si todavía estás decidiendo qué tablero usar, lee primero la comparación de alternativas autoalojadas a Trello. Esta guía da por tomada esa decisión y cubre sólo la implementación.
Necesitas un VPS con Docker Engine y el plugin de Compose, además de un registro DNS A que apunte a él. También necesitas una instancia de Traefik que ya termine TLS (seguridad de la capa de transporte) en ese servidor. Si Traefik todavía no está instalado, configura primero un proxy inverso de Traefik delante de varias aplicaciones Compose y lee los conceptos básicos de Docker Compose para un VPS si el archivo siguiente no te resulta familiar.
¿Qué recursos de VPS necesita Planka?
El proyecto no publica unos requisitos mínimos de hardware, así que considere cualquier cifra que lea como un punto de partida, no como una medición. La cifra de 2 vCPU y 4 GB que repiten las páginas de hosting es un valor predeterminado cómodo del proveedor, no un requisito medido por el proyecto. Es generosa para un tablero que utilizan cinco personas.
El funcionamiento real es sencillo: un proceso de Node.js sirve la API y el frontend compilado, y un proceso de Postgres almacena los datos. Un tercer proceso proxy pequeño se ejecuta dentro del contenedor de Planka para filtrar sus solicitudes salientes. Un plan con 1 vCPU y 2 GB es suficiente para un tablero utilizado por entre dos y cinco personas, y la mayor parte de la memoria disponible termina como caché de Postgres.
Dimensione el disco antes que la memoria, porque los archivos adjuntos son lo que más crece. Mida su propia instancia en lugar de confiar en este párrafo:
docker stats --no-stream
docker system df -vEl primer comando muestra la memoria y la CPU utilizadas en tiempo real por cada contenedor. El segundo muestra cuánto espacio ocupa cada volumen. Tome ambas mediciones después de una semana normal de trabajo, no el día de la instalación, porque un tablero inactivo no dice nada sobre su equipo.
Escriba el archivo Compose
Cree el directorio y asígnele la propiedad correspondiente. Así nunca tendrá que editar estos archivos mediante sudo.
sudo mkdir -p /opt/planka
sudo chown "$USER":"$USER" /opt/planka
cd /opt/plankaGenere los secretos en un archivo .env junto al archivo Compose. Compose lee ese archivo automáticamente y sustituye los valores.
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 es intencionado. Una cadena hexadecimal sólo contiene dígitos y las letras a a f, por lo que no puede romper la cadena de conexión DATABASE_URL en la que se inserta. Una contraseña en base64 que contenga una barra o un signo arroba produce un error de conexión que parece indicar un nombre de host incorrecto. Esto puede hacerle perder una hora. El patrón general se explica en mantener los secretos fuera del archivo Compose.
Ahora docker-compose.yml. Sustituya kanban.example.com por su propio nombre de host en los dos lugares donde aparece.
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:Conviene explicar cuatro decisiones de ese archivo. Son las que los usuarios suelen cambiar y lamentar después.
- No hay ningún bloque
ports:en el servicio Planka. Traefik llega al contenedor a través de la redproxy, por lo que el puerto 1337 nunca se publica en el host. Publicarlo permitiría a cualquiera eludir el proxy y el certificado. loadbalancer.server.port=1337indica el puerto dentro del contenedor. Planka escucha en 1337, y el ejemplo original sólo llega a él mediante 3000 porque asigna el puerto al host. Aquí no hay ninguna asignación al host, por lo que debe indicar a Traefik el puerto del contenedor.condition: service_healthyse combina con el healthcheck de Postgres. Sin esta opción, Planka se inicia antes de que la base de datos acepte conexiones, falla en su primera consulta y termina. Esto parece un bucle de reinicios por fallo. Los detalles se explican en healthchecks de Compose y orden de inicio.- El servicio de base de datos se llama
postgresde forma intencionada. Planka 2 enruta sus propias solicitudes salientes mediante un filtro interno cuya lista de bloqueo predeterminada eslocalhost,postgres. Si cambia el nombre del servicio, elimina silenciosamente la base de datos de esa lista.
Compruebe que Compose puede ver los secretos antes de iniciar nada:
docker compose config | grep -E 'image:|BASE_URL|POSTGRES_USER'Esto muestra el archivo con los valores .env ya sustituidos. Un valor vacío significa que Compose no está leyendo el archivo .env, normalmente porque está ejecutando el comando desde otro directorio.
Qué hacen realmente las variables de arranque del administrador
Desde Planka 1.13 no se crea ningún administrador automáticamente, por lo que una base de datos nueva no tiene ningún usuario que pueda iniciar sesión. El grupo DEFAULT_ADMIN_* es una de las dos formas de solucionarlo.
Durante el arranque, Planka busca un usuario que coincida con DEFAULT_ADMIN_EMAIL. Si no existe, crea uno con la contraseña, el nombre visible y el nombre de usuario definidos junto a esa variable. Esto ocurre durante el primer arranque con una base de datos vacía, por lo que estas variables sirven para crear una cuenta inicial, no para administrarla.
DEFAULT_ADMIN_EMAIL tiene otra función que suele causar problemas. Mientras la variable esté definida, nadie puede editar ni eliminar desde la interfaz la cuenta que indica. Es una protección contra el bloqueo y también explica por qué no puedes cambiar el nombre de esa cuenta ni su dirección de correo en la interfaz. Elimina la variable y reinicia. La cuenta se convertirá en una cuenta de administrador normal que podrás editar como cualquier otra.
La línea de la contraseña requiere especial cuidado. Cualquier valor incluido en environment: puede ser leído por quien pueda ejecutar docker inspect en el contenedor, por lo que DEFAULT_ADMIN_PASSWORD no debería permanecer allí. Inicia sesión, cambia la contraseña en la interfaz, elimina esa línea y vuelve a ejecutar docker compose up -d.
La opción más limpia omite por completo las variables. Comenta todo el grupo DEFAULT_ADMIN_* y crea la cuenta de forma interactiva:
docker compose run --rm planka npm run db:create-admin-userEl comando solicita el correo electrónico, la contraseña, el nombre visible y un nombre de usuario opcional, y escribe el usuario directamente en la base de datos. La contraseña nunca pasa por el archivo Compose ni por el entorno del contenedor. Usa esta opción si más de una persona tiene acceso shell al VPS. El comando inicia primero Postgres debido a depends_on, por lo que funciona aunque la pila nunca se haya iniciado.
Cualquiera de las dos opciones implica administrar manualmente las contraseñas de Planka. Si esas son ya las cuartas credenciales que ha tenido que recopilar tu equipo, Planka puede delegar los inicios de sesión en un proveedor OIDC como Authentik ejecutándose como tu propio servidor de inicio de sesión único, mientras conservas el administrador inicial como cuenta de emergencia para el caso de que el proveedor no esté disponible.
Por qué BASE_URL rompe los inicios de sesión cuando no coincide con el nombre de host
BASE_URL es la dirección exacta que los usuarios escriben en el navegador, con el esquema y sin una barra final. Para esta pila, es https://kanban.example.com. Planka genera sus propios enlaces y su conexión WebSocket a partir de ese valor. Por eso, un BASE_URL incorrecto no muestra un error claro. Muestra una página que se carga y luego nunca termina de cargar.
El caso habitual es el siguiente: copia el ejemplo del proyecto original, deja BASE_URL=http://localhost:3000 sin modificar y accede al sitio mediante HTTPS en su dominio real. El formulario de inicio de sesión se envía y las credenciales se aceptan. El tablero nunca aparece. Abra la consola de desarrollador del navegador y verá que fallan las solicitudes a /socket.io/, porque se indicó al cliente que abriera su conexión en tiempo real en localhost:3000 y, en su equipo portátil, esa dirección no existe.
TRUST_PROXY=true es la otra parte del mismo problema. Planka se ejecuta detrás de Traefik, por lo que cada solicitud llega desde la dirección del proxy mediante HTTP sin cifrar dentro de la red Docker. Sin TRUST_PROXY, la aplicación ignora las cabeceras X-Forwarded-Proto y X-Forwarded-For que establece Traefik. Por tanto, considera que la conexión no es segura y trata a todos los clientes como una única dirección IP compartida. Al establecerlo, la aplicación lee esas cabeceras y coincide con el navegador sobre el esquema utilizado.
Traefik gestiona WebSockets sin configuración adicional, que es una de las razones para preferirlo en este caso. En nginx, socket.io necesita su propio bloque location con proxy_set_header Upgrade $http_upgrade y proxy_set_header Connection "upgrade". De lo contrario, verá el mismo indicador de carga detenido, pero por una causa diferente.
Si más adelante mueve el tablero a un nombre de host nuevo, debe cambiar dos elementos conjuntamente: el valor BASE_URL y la regla Host() de Traefik. Si cambia uno y olvida el otro, volverá a aparecer el indicador de carga detenido. Servir Planka desde una subruta como https://example.com/planka funciona a partir de la versión 2.1.0, publicada en marzo de 2026. En las etiquetas anteriores, asígnele un subdominio propio.
Dónde guarda Planka los archivos adjuntos y los avatares
Planka 2 guarda todo lo que sube un usuario en una única ruta dentro del contenedor: /app/data. Los archivos adjuntos, los avatares de usuario y las imágenes de fondo de los tableros se almacenan allí. La versión 1 usaba tres directorios independientes, por lo que un archivo de Compose copiado de una guía antigua monta rutas que ya no existen y deja sin montar el directorio de datos real.
Ese único montaje determina si un tablero sobrevive a una actualización o si se pierde el trabajo realizado. Si /app/data no está en un volumen, los archivos subidos se almacenan en la capa escribible del contenedor. Esa capa se elimina cuando se vuelve a crear el contenedor, y el contenedor se vuelve a crear cada vez que cambia la etiqueta de la imagen. El tablero vuelve a mostrarse correctamente, todas las tarjetas siguen allí y todos los enlaces a los archivos adjuntos dejan de funcionar, porque las filas de la base de datos siguen apuntando a archivos que ya no existen.
El volumen con nombre del archivo de Compose anterior evita este problema. Un bind mount también funciona y facilita las copias de seguridad de los archivos con herramientas convencionales, pero requiere un paso adicional. El proceso de Node dentro del contenedor se ejecuta con el UID 1000, por lo que un directorio del host propiedad de root provoca un error de permisos al cargar el primer archivo:
sudo chown -R 1000:1000 /opt/planka/dataLa diferencia entre ambas opciones se explica en bind mounts frente a volúmenes con nombre.
Si los archivos adjuntos superan el espacio disponible en su plan, Planka puede escribirlos en almacenamiento compatible con S3 mediante S3_ENDPOINT, S3_BUCKET y las variables de clave correspondientes. Puede apuntar a un bucket alojado o a un almacén de objetos MinIO autohospedado en otro equipo. Decida esta configuración antes de que el equipo llene el tablero, porque se aplica a los archivos que se suban a partir de ese momento.
Inicie la pila y compruebe que funciona
docker compose pull
docker compose up -d
docker compose psdocker compose ps debe mostrar postgres como healthy y planka como running. Si Planka se reinicia en bucle, lo primero que debe comprobar es la conexión con la base de datos, no la aplicación.
docker compose logs -f plankaUn primer arranque correcto ejecuta las migraciones de la base de datos y después indica que el servidor está escuchando en el puerto 1337. Confirme que el esquema se haya creado realmente consultando Postgres directamente, en lugar de confiar en el registro:
docker compose exec postgres psql -U planka -d planka -c '\dt'Una lista de tablas que incluya board y card indica que las migraciones se ejecutaron. "Did not find any relations" significa que Planka nunca se conectó. Compare DATABASE_URL con los valores POSTGRES_USER y POSTGRES_PASSWORD de su .env.
Después, compruebe la ruta desde su propio equipo, no desde el VPS:
curl -I https://kanban.example.comHTTP/2 200 significa que Traefik tiene un certificado y puede acceder al contenedor. Un 404 servido por Traefik significa que las etiquetas del router no coinciden. La causa más habitual es que el contenedor no esté conectado a la red proxy. Ahora abra el sitio e inicie sesión con la cuenta de administrador.
Haz un pg_dump antes de cada actualización de versión
Dos almacenes independientes contienen tu tablero, por lo que la copia de seguridad debe cubrir ambos: la base de datos de Postgres y el volumen planka-data. Haz el volcado de la base de datos mientras el stack está en ejecución.
docker compose exec -T postgres pg_dump -U planka -d planka > "planka-db-$(date +%F).sql"-T no es opcional. Sin él, Compose asigna una pseudo-terminal y la capa de terminal reescribe los finales de línea en el flujo. Como resultado, obtienes un archivo de volcado que falla a mitad de una restauración. El fallo aparece semanas después, que es el peor momento posible.
Después, haz una copia de los archivos cargados. Primero busca el nombre real del volumen, porque Compose le antepone el nombre del directorio del proyecto.
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 .El proyecto también incluye docker-backup.sh y docker-restore.sh en su repositorio, y la documentación oficial indica que deben ejecutarse mediante una tarea cron nocturna. Cualquiera de los dos métodos es válido. Lo que no es válido es tener una copia de seguridad que nunca hayas restaurado. Restaura una en un VPS temporal y confirma que puedes iniciar sesión y abrir un archivo adjunto.
Ejecuta el volcado inmediatamente antes de cada cambio de versión. Una copia de seguridad de anoche no equivale a una copia de seguridad creada antes de la migración que estás a punto de ejecutar.
Fije las etiquetas y lea las notas de la versión
Las dos etiquetas de imagen de ese archivo están fijadas de forma intencionada.
ghcr.io/plankanban/planka:2.1.1 corresponde a una versión específica, vigente en agosto de 2026. latest cambia cada vez que el proyecto upstream publica una versión, por lo que un docker compose pull rutinario puede incorporar una migración de esquema en un momento que usted no ha elegido. Lea las notas de la versión antes de cambiar ese número. Ahí se describen los cambios incompatibles y las correcciones de seguridad. La versión 2.0.3 se publicó como una versión de seguridad. Es exactamente el tipo de cambio que debe leer, no incorporar por accidente.
postgres:16-alpine está fijada a una versión principal por un motivo más importante. Postgres escribe su directorio de datos en un formato vinculado a la versión principal. El servidor se niega a abrir un directorio escrito por otra versión. Escriba postgres:latest, deje que la etiqueta avance hasta 17 y el contenedor no se iniciará:
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.No se pierde nada y reiniciar tampoco lo soluciona. Pasar a una nueva versión principal de Postgres requiere crear un volcado con la versión antigua y restaurarlo en un directorio de datos nuevo con la versión nueva. Es una tarea planificada, con el stack detenido, no un efecto secundario de descargar una imagen.
Si va a migrar una instalación existente de Planka 1.x en lugar de empezar desde cero, esa actualización tiene su propio procedimiento documentado en la documentación del proyecto. No hay forma de volver a la versión 1 sin una copia de seguridad creada previamente.
Modos de fallo y cadenas que verá
Planka se reinicia en bucle y el registro menciona la base de datos. Las credenciales de DATABASE_URL no coinciden con el entorno de Postgres. Tenga en cuenta que POSTGRES_PASSWORD sólo se aplica cuando el directorio de datos se inicializa por primera vez, por lo que corregir la variable después de un primer arranque incorrecto no cambia nada. Debe eliminar el volumen de db-data y volver a iniciar.
El inicio de sesión funciona, pero el tablero nunca carga. BASE_URL no coincide con la dirección de la barra del navegador, o falta TRUST_PROXY. La consola del navegador muestra solicitudes fallidas a /socket.io/.
Las cargas de archivos fallan, pero todo lo demás funciona. Hay un bind mount cuyo propietario es root. Ejecute sudo chown -R 1000:1000 en el directorio del host y reinicie el contenedor.
Los archivos adjuntos desaparecieron después de una actualización. /app/data no estaba en un volumen, por lo que los archivos permanecían en la capa del contenedor que la actualización reemplazó. Restaure los archivos desde una copia de seguridad y añada el volumen antes de volver a modificar la etiqueta de la imagen.
Traefik devuelve 404. El contenedor no está en la red proxy, o la regla Host() no coincide con el registro DNS. docker compose config muestra las etiquetas después de la sustitución; ahí se hacen visibles los errores tipográficos.
Las notificaciones o los webhooks nunca llegan. Planka 2 envía sus solicitudes HTTP salientes a través de un filtro interno, y la lista de bloqueo predeterminada incluye localhost y postgres. Un webhook dirigido a otro contenedor en el mismo host puede bloquearse de forma intencionada. Ajuste OUTGOING_ALLOWED_HOSTS en lugar de eliminar el filtro.
Una vez que está en ejecución, la carga operativa es reducida. Revise las notas de la versión y haga un volcado de la base de datos antes de cada actualización. Un reinicio vuelve a iniciar la pila automáticamente debido a restart: unless-stopped, siempre que el servicio de Docker esté habilitado durante el arranque. Pilas de Compose que vuelven a iniciarse después de un reinicio cubre los casos en los que no sucede.
FAQ
¿Por qué Planka carga indefinidamente después de iniciar sesión?
Las credenciales se aceptaron, pero la conexión en tiempo real no se estableció. Planka construye la URL de WebSocket a partir de BASE_URL. Si esa variable todavía contiene http://localhost:3000 mientras accede al sitio mediante https://kanban.example.com, el navegador intenta abrir un socket hacia una dirección que no existe en su máquina. La consola del desarrollador muestra solicitudes fallidas a /socket.io/. Establezca BASE_URL con la dirección pública exacta, sin barra final, añada TRUST_PROXY=true para que la aplicación respete la cabecera X-Forwarded-Proto del reverse proxy y ejecute docker compose up -d.
¿Cómo creo el primer usuario administrador de Planka?
Desde la versión 1.13 no se crea ningún administrador automáticamente. Puede establecer DEFAULT_ADMIN_EMAIL con las variables correspondientes de contraseña, nombre y nombre de usuario e iniciar el stack, o ejecutar docker compose run --rm planka npm run db:create-admin-user y responder a las indicaciones. El comando interactivo es más seguro en un servidor compartido, porque la contraseña nunca entra en el entorno del contenedor, donde docker inspect puede leerla. Mantener DEFAULT_ADMIN_EMAIL establecido después bloquea esa cuenta e impide modificarla o eliminarla desde la interfaz.
¿Dónde almacena Planka los archivos adjuntos y los avatares?
En Planka 2, todos los archivos subidos se almacenan en /app/data dentro del contenedor, incluidos los archivos adjuntos, los avatares de usuario y los fondos de los tableros. Monte esa ruta en un volumen con nombre. Si no está montado, los archivos permanecen en la capa escribible del contenedor y se destruyen la próxima vez que se vuelva a crear el contenedor, algo que ocurre con cada actualización de la imagen. También puede usar un bind mount, pero el proceso de Node se ejecuta como UID 1000. Por tanto, ejecute sudo chown -R 1000:1000 en el directorio del host o las cargas fallarán por un error de permisos.
¿Cuánta RAM necesita una instancia de Planka autoalojada?
El proyecto no publica un requisito mínimo de hardware. La cifra de 2 vCPU y 4 GB que se repite en las páginas de alojamiento es el valor predeterminado de un proveedor, no una medición, y resulta generosa para un tablero pequeño. La carga completa consta de un proceso de Node y uno de Postgres, por lo que un plan con 1 vCPU y 2 GB admite un equipo de dos a cinco personas. Ejecute docker stats --no-stream después de una semana normal y dimensione el sistema a partir de sus propios datos. Vigile el disco más que la memoria, porque lo que crece son los archivos adjuntos.
¿Cómo actualizo Planka sin perder datos?
Vuelque la base de datos y archive el volumen de cargas inmediatamente antes de la actualización, no según la programación de la noche anterior. Use docker compose exec -T postgres pg_dump -U planka -d planka > planka-db.sql y conserve -T para que el pseudo-terminal no corrompa la salida redirigida. Lea las notas de la versión de todas las versiones que omita, cambie la etiqueta de la imagen por una versión específica en lugar de latest, después ejecute docker compose pull y docker compose up -d y supervise el registro para comprobar la migración. Mantenga fijada la etiqueta de Postgres en su versión mayor, porque el servidor se niega a abrir un directorio de datos escrito por una versión mayor diferente.