Plane – Guia de instalação

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

Bash
# 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:

Bash
# Email de registro no letsencrypt
EMAIL="voce@seudominio.com.br";

# Gravar no arquivo para consulta
echo "$EMAIL" > /etc/email;

Rodando o Traefik:

Bash
#!/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:

Bash
# 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:

Bash
# 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:

Bash
# 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):

Bash
# - 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:

Bash
# 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.

Bash

# 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:

PathServiçoDescrição
plane.seudominio.com.br/web:3000Frontend principal — workspaces e projetos
plane.seudominio.com.br/spaces/space:3000Área pública de issues (embed externo)
plane.seudominio.com.br/god-mode/admin:3000Painel administrativo da instância
plane.seudominio.com.br/live/live:3000Servidor de colaboração em tempo real
plane.seudominio.com.br/api/api:8000API REST
plane.seudominio.com.br/auth/api:8000Autenticação
plane.seudominio.com.br/bucket-name/minio:9000Arquivos 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