SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-28

Alojar Planka con Docker Compose en un VPS

Despliega 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é obtiene al alojar Planka por su cuenta

Alojar Planka por su cuenta proporciona a su equipo un tablero Kanban con el modelo de tarjetas, listas y etiquetas que los usuarios ya conocen de Trello, ejecutándose en un VPS que usted controla. No hay límites de usuarios 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 carguen los usuarios.

El lector al que va dirigida esta guía forma parte de un equipo de dos a cinco personas que deja el nivel gratuito de Trello. Si todavía está decidiendo qué tablero utilizar, lea primero la comparación de alternativas de Trello autoalojadas. Esta guía presupone que la decisión ya está tomada y cubre únicamente la implementación.

Necesita un VPS con Docker Engine y el complemento Compose, además de un registro DNS A que apunte a él. También necesita una instancia de Traefik que ya termine TLS (seguridad de la capa de transporte) en ese servidor. Si Traefik todavía no está configurado, prepare primero un proxy inverso de Traefik delante de varias aplicaciones Compose y consulte los conceptos básicos de Docker Compose para un VPS si el archivo siguiente no le resulta familiar.

¿Cuántos recursos de VPS necesita Planka?

El proyecto no publica unos requisitos mínimos de hardware, así que considere cualquier cifra que encuentre 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 holgado del proveedor, no un requisito medido por el proyecto. Es más que suficiente para un tablero que utilizan cinco personas.

En realidad, la carga es pequeña: un proceso de Node.js sirve la API y el frontend compilado, y un proceso de Postgres almacena los datos. Dentro del contenedor de Planka también se ejecuta un tercer proceso proxy pequeño para filtrar sus solicitudes salientes. Un plan con 1 vCPU y 2 GB admite un tablero utilizado por entre dos y cinco personas, y la mayor parte de la memoria libre termina como caché de Postgres. Un tablero genera poca carga, así que, si el mismo VPS también debe alojar los documentos del equipo, dimensione el servidor para esa aplicación primero: ejecutar AFFiNE como espacio de trabajo al estilo de Notion necesita un par de gigabytes por sí solo antes de que Planka requiera recursos adicionales.

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 -v

El primer comando muestra la memoria y la CPU actuales de cada contenedor. El segundo indica cuánto espacio ocupa cada volumen. Tome ambas lecturas después de una semana normal de trabajo, no el día de la instalación, porque un tablero inactivo no dice nada sobre el uso de su equipo.

Escriba el archivo de Compose

Cree el directorio y cambie su propietario para no tener que editar estos archivos mediante sudo.

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

Genere los secretos en un archivo .env junto al archivo de 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 .env

openssl rand -hex es intencionado. Una cadena hexadecimal sólo contiene dígitos y las letras de la a a la f, por lo que no puede romper la cadena de conexión de DATABASE_URL en la que se inserta. Una contraseña en base64 que contenga una barra o un signo arroba provoca un error de conexión que parece indicar que el nombre de host es incorrecto, y puede hacerle perder una hora. El patrón general se explica en mantener los secretos fuera del archivo de 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 más se suelen cambiar y lamentar después.

  • No hay ningún bloque ports: en el servicio de Planka. Traefik llega al contenedor a través de la red proxy, 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=1337 indica el puerto dentro del contenedor. Planka escucha en 1337, y el ejemplo anterior sólo llega a él en 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_healthy se combina con la comprobación de estado 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. Los detalles se explican en comprobaciones de estado y orden de inicio en Compose.
  • El servicio de base de datos se llama postgres de forma intencionada. Planka 2 dirige sus propias solicitudes salientes mediante un filtro interno cuya lista de bloqueo predeterminada es localhost,postgres. Si cambia el nombre del servicio, eliminará 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 de .env ya sustituidos. Un valor vacío indica que Compose no está leyendo el archivo .env, normalmente porque ejecuta el comando desde otro directorio.

Qué hacen realmente las variables de inicialización 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.

Al iniciar, Planka busca un usuario que coincida con DEFAULT_ADMIN_EMAIL. Si no existe, crea uno con la contraseña, el nombre mostrado 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 inicializan una cuenta, pero no la administran.

DEFAULT_ADMIN_EMAIL tiene una segunda función que suele causar problemas. Mientras la variable permanezca definida, nadie puede editar ni eliminar desde la interfaz la cuenta que indica. Es una protección contra el bloqueo de acceso y también explica por qué no puede cambiar el nombre de esa cuenta ni su dirección de correo electrónico desde la interfaz. Elimine la variable y reinicie. La cuenta se convertirá en un administrador normal que podrá editar como cualquier otra.

La línea de la contraseña requiere especial cuidado. Cualquier valor definido en environment: puede leerlo cualquiera que pueda ejecutar docker inspect en el contenedor, por lo que DEFAULT_ADMIN_PASSWORD no debería permanecer allí. Inicie sesión, cambie la contraseña desde la interfaz, elimine esa línea y vuelva a ejecutar docker compose up -d.

La opción más limpia no usa las variables. Comente todo el grupo DEFAULT_ADMIN_* y cree la cuenta de forma interactiva:

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

El comando solicita el correo electrónico, la contraseña, el nombre mostrado y un nombre de usuario opcional. Después escribe el usuario directamente en la base de datos. La contraseña nunca pasa por el archivo Compose ni por el entorno del contenedor. Use esta opción si más de una persona tiene acceso de 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 este es el cuarto conjunto de credenciales que ha acumulado su equipo, Planka puede delegar los inicios de sesión en un proveedor OIDC, como Authentik ejecutándose como su propio servidor de inicio de sesión único, y mantener 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. En 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 produce un error claro. La página carga, pero nunca termina de cargarse.

El caso habitual es este: copia el ejemplo del proyecto original, deja BASE_URL=http://localhost:3000 sin cambiar y accede al sitio mediante HTTPS en tu dominio real. El formulario de inicio de sesión se envía y las credenciales se aceptan. El tablero nunca aparece. Abre la consola de desarrollo del navegador y verás que fallan las solicitudes a /socket.io/, porque se indicó al cliente que abriera su conexión en tiempo real con localhost:3000, y en tu portátil esa dirección no existe.

TRUST_PROXY=true es la otra parte del mismo problema. Planka está detrás de Traefik, por lo que cada solicitud llega desde la dirección del proxy mediante HTTP sin cifrar dentro de la red de 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 sola dirección IP compartida. Cuando se configura, la aplicación lee esas cabeceras y coincide con el navegador respecto al esquema.

Traefik gestiona los 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, aparece el mismo indicador de carga detenido, pero por otra causa.

Si más adelante mueves el tablero a un nombre de host nuevo, debes cambiar dos elementos juntos: el valor BASE_URL y la regla Host() de Traefik. Si cambias uno y olvidas el otro, volverás a ver el indicador 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ígnale 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 separados, 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 provoca problemas. Si /app/data no está en un volumen, las cargas se guardan 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 se cambia la etiqueta de la imagen. El tablero vuelve a aparecer correctamente, todas las tarjetas siguen allí y todos los enlaces a los archivos adjuntos están rotos, 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. También puede usar un bind mount, que facilita las copias de seguridad de los archivos con herramientas habituales, 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 produce un error de permisos en la primera carga:

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

La diferencia entre ambas opciones se explica en bind mounts frente a volúmenes con nombre.

Si los archivos adjuntos superan el espacio de disco incluido en su plan, Planka puede escribirlos en un 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 autogestionado en otro equipo. Decida esto antes de que el equipo llene el tablero, porque la configuración sólo se aplica a las cargas nuevas.

Inicie la pila y compruebe que funciona

docker compose pull
docker compose up -d
docker compose ps

docker 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 planka

Un 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 significa 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.com

HTTP/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

Tu tablero se almacena en dos ubicaciones independientes, por lo que la copia de seguridad debe cubrir ambas: la base de datos de Postgres y el volumen planka-data. Haz el volcado de la base de datos mientras la pila 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 esta opción, Compose asigna un pseudo-terminal y la capa del 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.

A continuación, haz una copia de las cargas. Primero busca el nombre real del volumen, porque Compose le añade como prefijo 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 un cron nocturno. 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 una VPS de prueba y confirma que puedes iniciar sesión y abrir un archivo adjunto. Este mismo par de almacenes aparece en todas las aplicaciones de Compose que aceptan cargas. Por eso, si más adelante instalas Chatwoot en el mismo servidor que tu mesa de ayuda, el procedimiento que construyas aquí se puede reutilizar con pocos cambios, principalmente en los nombres de los volúmenes.

Ejecuta el volcado inmediatamente antes de cada cambio de versión. Una copia de seguridad de anoche no equivale a una copia de seguridad realizada 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 es una versión específica, actualizada en agosto de 2026. latest cambia cada vez que upstream publica una versión, por lo que una docker compose pull rutinaria 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, porque 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. Fijar la versión es sencillo en este caso porque upstream publica imágenes. Cuando un proyecto no publica imágenes, debe aplicar la misma disciplina y añadir un paso más, como en openGym compilado en el equipo a partir de una etiqueta de git extraída.

postgres:16-alpine está fijado a una versión principal por un motivo más importante. Postgres escribe su directorio de datos en un formato asociado a la versión principal, y 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 ningún dato y reiniciar tampoco soluciona el problema. Pasar a una versión principal nueva de Postgres requiere hacer un volcado de la versión antigua y restaurarlo en un directorio de datos nuevo de la versión nueva. Es una tarea planificada que se realiza 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, y no hay forma de volver a la versión 1 sin una copia de seguridad creada previamente.

Modos de fallo y mensajes 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 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. Un bind mount pertenece a 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 quedaron 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 cambiar la etiqueta de la imagen.

Traefik devuelve 404. El contenedor no está en la red proxy, o la regla Host() no coincide con su 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 mediante un filtro interno, y la lista de bloqueo predeterminada incluye localhost y postgres. Un webhook dirigido a otro contenedor del mismo host puede bloquearse de forma intencionada. Ajuste OUTGOING_ALLOWED_HOSTS en lugar de eliminar el filtro.

Una vez en funcionamiento, la carga operativa es baja. Revise las notas de la versión y vuelque la base de datos antes de cada actualización. Un reinicio vuelve a levantar la pila automáticamente debido a restart: unless-stopped, siempre que el servicio Docker esté habilitado durante el arranque. Las pilas de Compose que vuelven a levantarse después de un reinicio cubre los casos en los que no lo está.

FAQ

¿Por qué Planka se queda cargando 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 y 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 junto 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 cargados se almacenan en /app/data dentro del contenedor. Esto incluye 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 recrea el contenedor. Esto ocurre con cada actualización de la imagen. También puede usar un bind mount, pero el proceso de Node se ejecuta con UID 1000. Por tanto, ejecute sudo chown -R 1000:1000 en el directorio del host. De lo contrario, las cargas fallarán con un error de permisos.

¿Cuánta RAM necesita una instalación de Planka autohospedada?

El proyecto no publica un requisito mínimo de hardware. La cifra de 2 vCPU y 4 GB que aparece repetidamente en las páginas de hosting es el valor predeterminado de un proveedor, no una medición, y resulta generosa para un tablero pequeño. Toda la carga consiste en un proceso de Node y uno de Postgres. Por eso, un plan con 1 vCPU y 2 GB puede atender a 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 tarea programada 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 pseudoterminal no corrompa la salida redirigida. Lea las notas de la versión de todas las versiones que omita, cambie la etiqueta de imagen por una versión específica en lugar de latest, ejecute docker compose pull y docker compose up -d y monitorice el registro para comprobar la migración. Mantenga fijada la etiqueta de Postgres en su versión principal, porque el servidor se niega a abrir un directorio de datos escrito por otra versión principal.