Saudações.
Esse tutorial é um guia rápido de instalação e uso do Plane.
O Plane é um gestor de projetos. Seu objetivo é ajudar as empresas e pessoas a organizarem tarefas, projetos, metas, anotações e interações entre colaboradores.
Seu diferencial é a organização por WorkSpace. Cada workspace isola totalmente os projetos para atender várias empresas ou departamentos com isolamento completo de acesso.
Pré-requisitos (constam em outros artigos aqui do blog):
- Instalação do Linux (Debian);
- Internet no servidor (sua VPS ou host);
- Docker CE instalado;
- Domínio DNS;
- Serviços: PostgreSQL, Redis, AMQP (RabbitMQ) e S3 (Minio).
1 – Preparando o Docker
Vamos preparar o ambiente docker com a rede (network_public) e usando o Traefik como proxy reverso.
1.1 – Docker Network com endereçamento automático
# Rede "network_public" na configuração padrão.
docker network create \
-d bridge \
network_public;
2 – Proxy-Reverso
Vou usar a execução padrão do container Traefik. O container do Traefik se chamará “traefik-app“.
Personalize a variável EMAIL para que o LetsEncrypt funcione corretamente. Vou salvar o email de contato no arquivo /etc/email do host:
# Email de registro no letsencrypt
EMAIL="voce@seudominio.com.br";
# Gravar no arquivo para consulta
echo "$EMAIL" > /etc/email;
Rodando o Traefik:
#!/bin/sh
# Variaveis
# - Nome do container
NAME=traefik-app;
# - Imagem do software traefik
IMAGE=traefik:latest;
# - Email de registro no letsencrypt
EMAIL=$(head -1 /etc/email);
# - Diretorio do volume (dados persistentes)
DATADIR=/storage/$NAME;
# Preparar volume:
mkdir -p $DATADIR/letsencrypt;
mkdir -p $DATADIR/logs;
mkdir -p $DATADIR/config;
# Obter imagem atualizada:
docker pull $IMAGE;
# Remover instância atual:
docker rm -f $NAME 2>/dev/null;
# Renovar/criar/rodar:
docker run \
-d --restart=unless-stopped --name $NAME -h $NAME.intranet.br \
\
--network network_public \
\
-p 80:80 \
-p 443:443 \
\
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v $DATADIR/letsencrypt:/etc/letsencrypt \
-v $DATADIR/config:/etc/traefik \
-v $DATADIR/logs:/logs \
\
--tmpfs /run:rw,noexec,nosuid,size=16m \
--tmpfs /tmp:rw,noexec,nosuid,size=16m \
--read-only \
\
$IMAGE \
\
--global.checkNewVersion=false \
--global.sendAnonymousUsage=false \
\
--api.insecure=true \
\
--log.level=INFO \
--log.filePath=/logs/error.log \
\
--accessLog.filePath=/logs/access.log \
\
--entrypoints.web.address=:80 \
--entrypoints.web.http.redirections.entryPoint.to=websecure \
--entrypoints.web.http.redirections.entryPoint.scheme=https \
--entrypoints.web.http.redirections.entryPoint.permanent=true \
\
--entrypoints.websecure.address=:443 \
\
--providers.docker=true \
--providers.file.directory=/etc/traefik \
\
--certificatesresolvers.letsencrypt.acme.email=$EMAIL \
--certificatesresolvers.letsencrypt.acme.storage=/etc/letsencrypt/acme.json \
--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web;
3 – Serviços externos para o Plane
O Plane depende de alguns serviços que você deverá rodar externamente antes de iniciar a execução do Plane.
Esses serviços podem ser containers vizinhos ou emserviços terceirizados na nuvem. Continue apenas quando todas as credenciais deles para o Plane estiverem prontas para uso.
São 4 serviços:
- PostgreSQL: banco de dados SQL;
- Endereço e Porta do PG;
- Usuário, senha e banco dados (privilégio de controle total);
- REDIS: cache e chaves temporárias;
- Endereço e porta do servidor Redis;
- RabbitMQ: para troca de mensagens entre os serviços do Plane;
- Endereço para acesso (IP ou nome de DNS);
- Porta do protocolo AMQP;
- VHOST exclusivo para o Plane;
- Usuário e senha com poderes para criar a estrutura usar a mensageria;
- Minio: para storage de objetos e arquivos;
- Endereço da API S3;
- Chave de Acesso (AK) e Secret Key (SK);
- Nome do Bucket (balde);

4 – Preparando ambiente para o Plane
Você pode reunir todos os scripts abaixo em um único script principal. Vou separar em partes para poder explicar melhor.
O Plane precisa de endereço de DNS próprio (variável $FQDN):
- plane.seudominio.com.br: endereço do portal Plane;
Variáveis vitais para o Plane – personalize e não erre:
# Variaveis vitais
# - Nome da aplicacao (nomes unicos para os containers e volumes)
APPNAME="tmsoft";
# - Nome de DNS
FQDN="plane.seudominio.com.br";
EMAIL="voce@seudominio.com.br";
# - Secret de seguranca (gerar com: openssl rand -hex 25)
SECRET_KEY="b75b2889d0dd3bc4cc34acfcb8cde843c506697d15ccb19803";
# - Secret do app Live (gerar com: openssl rand -hex 16)
LIVE_SERVER_SECRET_KEY="068213ae627b576852b5b36518113721";
# - Limite de arquivo para upload unitario
FILE_SIZE_LIMIT=5242880;
Variáveis para controle dos containers no Docker:
# Variaveis para o Docker
# - Rede docker de containers do plane
NETWORK="network_public";
# - Diretorio base de todos os volumes
DATADIR="/storage/$APPNAME-plane";
# - Nome dos containers
NAME_MIGRATOR="$APPNAME-plane-migrator";
NAME_API="$APPNAME-plane-api";
NAME_WORKER="$APPNAME-plane-worker";
NAME_BEAT="$APPNAME-plane-beat-worker";
NAME_SPACE="$APPNAME-plane-space";
NAME_ADMIN="$APPNAME-plane-admin";
NAME_LIVE="$APPNAME-plane-live";
NAME_WEB="$APPNAME-plane-web";
# - Imagens
# Versao do Plane:
PLANE_VERSION="v1.2.1";
# Imagem dos containers:
IMAGE_MIGRATOR="artifacts.plane.so/makeplane/plane-backend:$PLANE_VERSION";
IMAGE_API="artifacts.plane.so/makeplane/plane-backend:$PLANE_VERSION";
IMAGE_SPACE="artifacts.plane.so/makeplane/plane-space:$PLANE_VERSION";
IMAGE_ADMIN="artifacts.plane.so/makeplane/plane-admin:$PLANE_VERSION";
IMAGE_LIVE="artifacts.plane.so/makeplane/plane-live:$PLANE_VERSION";
IMAGE_WEB="artifacts.plane.so/makeplane/plane-frontend:$PLANE_VERSION";
# Imagens para o worker e beat:
IMAGE_BACKEND="artifacts.plane.so/makeplane/plane-backend:$PLANE_VERSION";
Variáveis com acessos a serviços externos – preencha todas corretamente:
# Dados do Minio
USE_MINIO="1";
MINIO_ENDPOINT_SSL="0";
MINIO_ROOT_USER="admin";
MINIO_ROOT_PASSWORD="SenhaAdminMinio";
# Dados para cliente Minio/S3
AWS_REGION="";
AWS_ACCESS_KEY_ID="ak_app_plane";
AWS_SECRET_ACCESS_KEY="95c4b832c7ef1d39fd4e3911d110232f8d08d9fa";
AWS_S3_ENDPOINT_URL="http://minio:9000"; #< container do minio ou URL externa
AWS_S3_BUCKET_NAME="bucket-name"; #< usar nome do bucket para o Plane
# Dados do container RabbitMQ (se for dedicado ao Plane)
RABBITMQ_HOST="rabbitmq"; #< container do RabbitMQ
RABBITMQ_PORT="5672"; #< porta AMQP
RABBITMQ_DEFAULT_USER="tmsoft_plane"; #< se o RabbitMQ for dedicado
RABBITMQ_DEFAULT_PASS="Tmsoft_Plane7789"; #< se o RabbitMQ for dedicado
RABBITMQ_DEFAULT_VHOST="tmsoft_plane"; #< se o RabbitMQ for dedicado
RABBITMQ_VHOST="tmsoft_plane"; #< vhost exclusivo do Plane
# Dados para cliente RabbitMQ
# amqp:// + user:pass + @ + ip-ou-dns + :5672 + / + vhost-exclusivo
AMQP_URL="amqp://tmsoft_plane:Tmsoft_Plane7789@rabbitmq:5672/tmsoft_plane";
# Dados para acesso ao Redis
REDIS_HOST="redis-db"; #< nome do container ou endereco
REDIS_PORT="6379"; #< porta do redis
REDIS_URL="redis://$REDIS_HOST:$REDIS_PORT/20"; #<
# Dados para acesso ao Postgres
PGHOST="pgvector-main";
PGDATABASE="tmsoft_plane";
POSTGRES_USER="tmsoft_plane";
POSTGRES_PASSWORD="tulipasql";
POSTGRES_DB="tmsoft_plane";
POSTGRES_PORT="5432";
PGDATA="/var/lib/postgresql/data";
# Dados para cliente Postgres
DATABASE_URL="postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$PGHOST/$POSTGRES_DB";
Com todas as variáveis de acesso definidas acima, vamos criar um arquivo “.env” dentro da pasta raiz dos volumes para facilitar a leitura delas pelos containers (via –env-file):
# - Criar diretorio raiz de todos os volumes
mkdir -p $DATADIR;
# Criar arquivo com todas as variaveis de ambiente
#==============================================================================
(
echo;
echo "# ── Aplicação";
echo "WEB_URL=https://$FQDN";
echo "DEBUG=0";
echo "CORS_ALLOWED_ORIGINS=";
echo "GUNICORN_WORKERS=1";
echo "USE_MINIO=$USE_MINIO";
echo "SECRET_KEY=$SECRET_KEY";
echo "API_KEY_RATE_LIMIT=60/minute";
echo "MINIO_ENDPOINT_SSL=$MINIO_ENDPOINT_SSL";
echo "LIVE_SERVER_SECRET_KEY=$LIVE_SERVER_SECRET_KEY";
echo;
echo "# ── Banco de Dados";
echo "PGHOST=$PGHOST";
echo "PGDATABASE=$PGDATABASE";
echo "POSTGRES_USER=$POSTGRES_USER";
echo "POSTGRES_PASSWORD=$POSTGRES_PASSWORD";
echo "POSTGRES_DB=$POSTGRES_DB";
echo "POSTGRES_PORT=$POSTGRES_PORT";
echo "PGDATA=$PGDATA";
echo "DATABASE_URL=$DATABASE_URL";
echo;
echo "# ── Redis";
echo "REDIS_HOST=$REDIS_HOST";
echo "REDIS_PORT=$REDIS_PORT";
echo "REDIS_URL=$REDIS_URL";
echo;
echo "# ── MinIO / S3";
echo "MINIO_ROOT_USER=$MINIO_ROOT_USER";
echo "MINIO_ROOT_PASSWORD=$MINIO_ROOT_PASSWORD";
echo "AWS_REGION=$AWS_REGION";
echo "AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID";
echo "AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY";
echo "AWS_S3_ENDPOINT_URL=$AWS_S3_ENDPOINT_URL";
echo "AWS_S3_BUCKET_NAME=$AWS_S3_BUCKET_NAME";
echo;
echo "# ── RabbitMQ";
echo "RABBITMQ_HOST=$RABBITMQ_HOST";
echo "RABBITMQ_PORT=$RABBITMQ_PORT";
echo "RABBITMQ_DEFAULT_USER=$RABBITMQ_DEFAULT_USER";
echo "RABBITMQ_DEFAULT_PASS=$RABBITMQ_DEFAULT_PASS";
echo "RABBITMQ_DEFAULT_VHOST=$RABBITMQ_DEFAULT_VHOST";
echo "RABBITMQ_VHOST=$RABBITMQ_VHOST";
echo "AMQP_URL=$AMQP_URL";
echo;
echo "# ── Domínio / Proxy";
echo "APP_DOMAIN=$FQDN";
echo "FILE_SIZE_LIMIT=$FILE_SIZE_LIMIT";
echo "CERT_EMAIL=$EMAIL";
echo "BUCKET_NAME=$AWS_S3_BUCKET_NAME";
echo "SITE_ADDRESS=:80";
echo;
echo "# ── Live";
echo "API_BASE_URL=http://$NAME_API:8000";
echo;
) > $DATADIR/plane.env;
Agora é hora de preparar o banco de dados no Postgres, criando tabelas e recursos. Esse processo MIGRATOR deve ser executado somente na instalação e quando você fizer upgrade para uma versão mais nova:
# Rodar container temporario do migrator - preparar banco de dados:
docker run -d --rm \
--name $NAME_MIGRATOR \
--hostname $NAME_MIGRATOR.intranet.br \
--network $NETWORK \
\
--env-file $DATADIR/plane.env \
-v $DATADIR/logs_migrator:/code/plane/logs \
\
$IMAGE_MIGRATOR ./bin/docker-entrypoint-migrator.sh;
Nota: o container MIGRATOR é temporário, ele deve rodar
5 – Deploy do Plane
Agora é a hora de colocar os containers para rodar e fazer uso tranquilo do Plane.
# API
#----------------------------------------------------------------------------------
docker rm -f $NAME_API 2>/dev/null;
docker run -d \
--name $NAME_API \
--hostname $NAME_API.intranet.br \
--network $NETWORK \
--restart always \
\
--env-file $DATADIR/plane.env \
\
-v $DATADIR/logs_api:/code/plane/logs \
\
--label "traefik.enable=true" \
--label "traefik.http.routers.$APPNAME-api.rule=Host(\`$FQDN\`) && (PathPrefix(\`/api\`) || PathPrefix(\`/auth\`))" \
--label "traefik.http.routers.$APPNAME-api.entrypoints=websecure" \
--label "traefik.http.routers.$APPNAME-api.tls=true" \
--label "traefik.http.routers.$APPNAME-api.tls.certresolver=letsencrypt" \
--label "traefik.http.routers.$APPNAME-api.priority=30" \
--label "traefik.http.services.$APPNAME-api.loadbalancer.server.port=8000" \
\
$IMAGE_API ./bin/docker-entrypoint-api.sh
# WORKER
#----------------------------------------------------------------------------------
docker rm -f $NAME_WORKER 2>/dev/null;
docker run -d \
--name $NAME_WORKER \
--hostname $NAME_WORKER.intranet.br \
--network $NETWORK \
--restart always \
\
--env-file $DATADIR/plane.env \
\
-v $DATADIR/logs_worker:/code/plane/logs \
\
$IMAGE_BACKEND ./bin/docker-entrypoint-worker.sh;
# BEAT-WORKER
#----------------------------------------------------------------------------------
docker rm -f $NAME_BEAT 2>/dev/null;
docker run -d \
--name $NAME_BEAT \
--hostname $NAME_BEAT.intranet.br \
--network $NETWORK \
--restart always \
\
--env-file $DATADIR/plane.env \
\
-v $DATADIR/logs_beat-worker:/code/plane/logs \
\
$IMAGE_BACKEND ./bin/docker-entrypoint-beat.sh;
# SPACE
#----------------------------------------------------------------------------------
# Nome de DNS ajustado para expressao regular
FQDN_ESCAPE=$(echo $FQDN | sed 's#\.#\\.#g');
docker rm -f $NAME_SPACE 2>/dev/null;
docker run -d \
--name $NAME_SPACE \
--hostname $NAME_SPACE.intranet.br \
--network $NETWORK \
--restart always \
\
--env-file $DATADIR/plane.env \
\
--label "traefik.enable=true" \
--label "traefik.http.middlewares.$APPNAME-pss.redirectregex.regex=^https://$FQDN_ESCAPE/spaces$" \
--label "traefik.http.middlewares.$APPNAME-pss.redirectregex.replacement=https://$FQDN/spaces/" \
--label "traefik.http.middlewares.$APPNAME-pss.redirectregex.permanent=true" \
--label "traefik.http.routers.$APPNAME-ps.rule=Host(\`$FQDN\`) && PathPrefix(\`/spaces\`)" \
--label "traefik.http.routers.$APPNAME-ps.entrypoints=websecure" \
--label "traefik.http.routers.$APPNAME-ps.tls=true" \
--label "traefik.http.routers.$APPNAME-ps.tls.certresolver=letsencrypt" \
--label "traefik.http.routers.$APPNAME-ps.priority=20" \
--label "traefik.http.routers.$APPNAME-ps.middlewares=$APPNAME-pss" \
--label "traefik.http.services.$APPNAME-ps.loadbalancer.server.port=3000" \
\
$IMAGE_SPACE;
# ADMIN
#----------------------------------------------------------------------------------
# Nome de DNS ajustado para expressao regular
FQDN_ESCAPE=$(echo $FQDN | sed 's#\.#\\.#g');
docker rm -f $NAME_ADMIN 2>/dev/null;
docker run -d \
--name $NAME_ADMIN \
--hostname $NAME_ADMIN.intranet.br \
--network $NETWORK \
--restart always \
\
--env-file $DATADIR/plane.env \
\
--label "traefik.enable=true" \
--label "traefik.http.middlewares.$APPNAME-pgs.redirectregex.regex=^https://$FQDN_ESCAPE/god-mode$" \
--label "traefik.http.middlewares.$APPNAME-pgs.redirectregex.replacement=https://$FQDN/god-mode/" \
--label "traefik.http.middlewares.$APPNAME-pgs.redirectregex.permanent=true" \
--label "traefik.http.routers.$APPNAME-paa.rule=Host(\`$FQDN\`) && PathPrefix(\`/god-mode\`)" \
--label "traefik.http.routers.$APPNAME-paa.entrypoints=websecure" \
--label "traefik.http.routers.$APPNAME-paa.tls=true" \
--label "traefik.http.routers.$APPNAME-paa.tls.certresolver=letsencrypt" \
--label "traefik.http.routers.$APPNAME-paa.priority=20" \
--label "traefik.http.routers.$APPNAME-paa.middlewares=$APPNAME-pgs" \
--label "traefik.http.services.$APPNAME-paa.loadbalancer.server.port=3000" \
\
$IMAGE_ADMIN;
# LIVE
#----------------------------------------------------------------------------------
docker rm -f $NAME_LIVE 2>/dev/null;
docker run -d \
--name $NAME_LIVE \
--hostname $NAME_LIVE.intranet.br \
--network $NETWORK \
--restart always \
\
--env-file $DATADIR/plane.env \
\
--label "traefik.enable=true" \
--label "traefik.http.routers.$APPNAME-plive.rule=Host(\`$FQDN\`) && PathPrefix(\`/live\`)" \
--label "traefik.http.routers.$APPNAME-plive.entrypoints=websecure" \
--label "traefik.http.routers.$APPNAME-plive.tls=true" \
--label "traefik.http.routers.$APPNAME-plive.tls.certresolver=letsencrypt" \
--label "traefik.http.routers.$APPNAME-plive.priority=20" \
--label "traefik.http.services.$APPNAME-plive.loadbalancer.server.port=3000" \
\
$IMAGE_LIVE;
# WEB FRONTEND
#----------------------------------------------------------------------------------
docker rm -f $NAME_WEB 2>/dev/null;
docker run -d \
--name $NAME_WEB \
--hostname $NAME_WEB.intranet.br \
--network $NETWORK \
--restart always \
\
--env-file $DATADIR/plane.env \
\
--label "traefik.enable=true" \
\
--label "traefik.http.middlewares.$APPNAME-phr.redirectscheme.scheme=https" \
--label "traefik.http.middlewares.$APPNAME-phr.redirectscheme.permanent=true" \
\
--label "traefik.http.routers.$APPNAME-pwh.rule=Host(\`$FQDN\`)" \
--label "traefik.http.routers.$APPNAME-pwh.entrypoints=web" \
--label "traefik.http.routers.$APPNAME-pwh.middlewares=$APPNAME-phr" \
--label "traefik.http.routers.$APPNAME-pwh.priority=1" \
\
--label "traefik.http.routers.$APPNAME-pw.rule=Host(\`$FQDN\`)" \
--label "traefik.http.routers.$APPNAME-pw.entrypoints=websecure" \
--label "traefik.http.routers.$APPNAME-pw.tls=true" \
--label "traefik.http.routers.$APPNAME-pw.tls.certresolver=letsencrypt" \
--label "traefik.http.routers.$APPNAME-pw.service=$APPNAME-pw-svc" \
--label "traefik.http.routers.$APPNAME-pw.priority=1" \
--label "traefik.http.services.$APPNAME-pw-svc.loadbalancer.server.port=3000" \
\
--label "traefik.http.routers.$APPNAME-ps3.rule=Host(\`$FQDN\`) && PathPrefix(\`/$AWS_S3_BUCKET_NAME\`)" \
--label "traefik.http.routers.$APPNAME-ps3.entrypoints=websecure" \
--label "traefik.http.routers.$APPNAME-ps3.tls=true" \
--label "traefik.http.routers.$APPNAME-ps3.tls.certresolver=letsencrypt" \
--label "traefik.http.routers.$APPNAME-ps3.service=$APPNAME-s3redir" \
--label "traefik.http.routers.$APPNAME-ps3.priority=25" \
--label "traefik.http.services.$APPNAME-s3redir.loadbalancer.server.url=$AWS_S3_ENDPOINT_URL" \
\
$IMAGE_WEB;
Recomendo executar um por um e acompanhar no comando “docker ps -a” se eles estão rodando normalmente.
Acesse o endereço de DNS criado para ele no navegador e faça bom uso! Principais urls:
- plane.seudominio.com.br: endereço do portal para trabalhar nos projetos;
- plane.seudominio.com.br/god-mode/: endereço do painel de administrador;
Guia de URLs:
| Path | Serviço | Descrição |
|---|---|---|
plane.seudominio.com.br/ | web:3000 | Frontend principal — workspaces e projetos |
plane. | space:3000 | Área pública de issues (embed externo) |
plane. | admin:3000 | Painel administrativo da instância |
plane. | live:3000 | Servidor de colaboração em tempo real |
plane. | api:8000 | API REST |
plane. | api:8000 | Autenticação |
plane. | minio:9000 | Arquivos e uploads. O path “/bucket-name” deve ser substituido pelo valor da variável AWS_S3_BUCKET_NAME |
“As únicas coisas que evoluem por vontade
própria em uma organização são a
desordem, o atrito e o mau desempenho.“
Peter Drucker
Terminamos por hoje!
Patrick Brandão, patrickbrandao@gmail.com
