Resumo
Esta pipeline automatiza a publicação da aplicação de forma simples: o GitHub valida o projeto, gera o pacote de produção e envia tudo pronto para o servidor. No ambiente de produção, a aplicação é publicada diretamente na pasta www e atualizada pelo PM2 dentro da estrutura padrão da cloud, usando o script start:production definido no próprio projeto. O processo cria um backup temporário de www durante a troca, limpa arquivos temporários e mantém o servidor sem histórico permanente de releases acumuladas.
Vantagens:
- o deploy fica previsível e reproduzível
- o build roda fora do servidor, poupando recursos da hospedagem
- o ambiente segue o padrão nativo da cloud, sem alterar a estrutura interna
- a publicação da aplicação fica simples no dia a dia com
git pushouRun workflow - o servidor fica mais enxuto, sem manter releases antigas como backup permanente
- se a validação falhar, o workflow tenta restaurar automaticamente a cópia temporária anterior de
www
Use este pipeline se sua aplicação precisa ficar rodando no servidor, como projetos Node.js, Astro SSR, Next.js standalone, Express, Fastify, NestJS ou APIs HTTP. Se o projeto só gera arquivos HTML, CSS e JS, use o pipeline estático. Se a hospedagem confirmar Docker disponível e liberado para o usuário da aplicação, o pipeline Docker também pode ser usado.
Antes de começar, tenha em mãos:
- acesso ao repositório no GitHub
- permissão para criar secrets no GitHub
- acesso ao painel da hospedagem
- usuário SSH da aplicação
- domínio ou subdomínio definido
- porta da aplicação definida
- variáveis de ambiente da aplicação
- branch principal confirmada, normalmente
main
Pipeline
git push na main
> job CI instala Node 22 no runner
> runner restaura cache do npm
> runner instala dependências com npm ci
> runner executa lint (se existir)
> runner executa testes (se existirem)
> job CD é liberado apenas se o job CI passar
> job CD instala Node 22 no runner
> runner restaura cache do npm
> runner instala dependências com npm ci
> runner valida e busca o ~/.env do servidor para obter variáveis de build
> runner executa o build da aplicação
> runner valida o script start:production
> runner compacta o pacote de produção do projeto
> runner envia o pacote por SCP
> servidor resolve o diretório *.configr.cloud da aplicação
> servidor executa source <app-root>/activate
> servidor extrai o pacote em um diretório temporário
> servidor instala dependências de runtime no diretório temporário
> servidor cria uma cópia temporária da pasta www atual
> servidor publica o conteúdo final na pasta www
> servidor limpa o estado antigo do PM2 daquele usuário e recria a aplicação em www com npm run start:production
> servidor valida a saúde da aplicação
> se a validação falhar, servidor restaura a cópia temporária anterior de www
> servidor remove o diretório temporário dist e o backup temporário
> o arquivo compactado enviado é removido ao fim da execução
> o servidor remove artefatos antigos de releases, se existirem
> o servidor mostra o status final do PM2
Workflow do GitHub Actions
No projeto local:
mkdir -p .github/workflows
nano .github/workflows/ci-cd-pipeline.yml
Cole o conteúdo abaixo:
name: CI/CD Pipeline
on:
push:
branches:
- main
workflow_dispatch:
concurrency:
group: deploy-production
cancel-in-progress: true
jobs:
ci:
name: CI - Quality Checks
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run lint
run: npm run lint --if-present
- name: Run tests
run: npm run test --if-present
cd:
name: CD - Build and Deploy Application
needs: ci
runs-on: ubuntu-latest
env:
APP_NAME: ${{ github.event.repository.name }}
REMOTE_ARCHIVE_PATH: /home/${{ secrets.SERVER_USER }}/dist.tar.gz
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Configure SSH
env:
SSH_KEY: ${{ secrets.SERVER_SSH_KEY }}
run: |
if [ -z "$SSH_KEY" ]; then
echo "SERVER_SSH_KEY is empty or was not configured in GitHub Secrets."
exit 1
fi
mkdir -p ~/.ssh
printf '%s\n' "$SSH_KEY" > ~/.ssh/server_key
chmod 600 ~/.ssh/server_key
if ! ssh-keyscan -H "${{ secrets.SERVER_HOST }}" >> ~/.ssh/known_hosts; then
echo "Could not add SERVER_HOST to known_hosts. Check SERVER_HOST in GitHub Secrets."
exit 1
fi
- name: Validate SSH connection
run: |
if ! ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "echo 'SSH connection validated.'"; then
echo "Could not connect to the server using SSH. Check SERVER_HOST, SERVER_USER, SERVER_SSH_KEY, and the authorized public key."
exit 1
fi
- name: Validate remote env file
run: |
if ! ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "test -f ~/.env"; then
echo "Remote ~/.env was not found. Create it on the server before running this pipeline."
exit 1
fi
- name: Fetch build environment from server
run: |
if ! ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "cat ~/.env" > .server.env; then
echo "Could not read remote ~/.env from the server."
exit 1
fi
if [ ! -s .server.env ]; then
echo "Remote ~/.env is empty. Add the production variables before deploying."
exit 1
fi
- name: Build application
run: |
set -a
. ./.server.env
set +a
if ! npm run build; then
echo "Application build failed. Check the build script and required build-time variables."
exit 1
fi
- name: Validate production scripts
run: |
if ! node -e "const fs = require('node:fs'); const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); if (!pkg.scripts || !pkg.scripts['start:production']) { throw new Error('Missing required script: start:production'); }"; then
echo "package.json must define scripts.start:production."
exit 1
fi
- name: Create deployment archive
run: |
ARCHIVE_PATH="$(mktemp /tmp/dist.XXXXXX.tar.gz)"
tar \
--exclude=.git \
--exclude=.github \
--exclude=node_modules \
--exclude=.server.env \
--exclude=.env \
--exclude=.env.* \
-czf "$ARCHIVE_PATH" .
mv "$ARCHIVE_PATH" dist.tar.gz
- name: Upload deployment archive to server via SCP
run: |
if ! scp -i ~/.ssh/server_key dist.tar.gz "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}:${REMOTE_ARCHIVE_PATH}"; then
echo "Could not upload dist.tar.gz to the server. Check SSH credentials and available disk space."
exit 1
fi
- name: Resolve application root on server
id: resolve_app_root
run: |
ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "
set -e
APP_ROOT=\"\$(find \"\$HOME\" -maxdepth 1 \\( -type l -o -type d \\) -name '*.configr.cloud' 2>/dev/null | sort | head -n 1)\"
if [ -z \"\$APP_ROOT\" ]; then
echo 'Could not find a *.configr.cloud application directory on the server.'
echo 'Create a Node.js application in the hosting panel before running this workflow.'
exit 1
fi
ACTIVATE_PATH=\"\$APP_ROOT/activate\"
if [ ! -f \"\$ACTIVATE_PATH\" ]; then
echo 'Could not find the activate script for the application.'
echo 'Check whether the hosting Node.js application was created correctly.'
exit 1
fi
printf '%s\n%s' \"\$APP_ROOT\" \"\$ACTIVATE_PATH\"
" > .app-root-data
test -s .app-root-data
APP_ROOT="$(sed -n '1p' .app-root-data)"
ACTIVATE_PATH="$(sed -n '2p' .app-root-data)"
printf 'app_root=%s\n' "$APP_ROOT" >> "$GITHUB_OUTPUT"
printf 'activate_path=%s\n' "$ACTIVATE_PATH" >> "$GITHUB_OUTPUT"
shell: bash
- name: Prepare deployment directories on server
id: prepare_deployment
run: |
APP_ROOT="${{ steps.resolve_app_root.outputs.app_root }}"
DIST_PATH="$APP_ROOT/dist-tmp"
WWW_PATH="$APP_ROOT/www"
BACKUP_PATH="$APP_ROOT/www-backup-tmp"
ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "
set -e
rm -rf -- \"$DIST_PATH\" \"$BACKUP_PATH\"
mkdir -p \"$DIST_PATH\"
mkdir -p \"$WWW_PATH\"
"
printf 'dist_path=%s\n' "$DIST_PATH" >> "$GITHUB_OUTPUT"
printf 'www_path=%s\n' "$WWW_PATH" >> "$GITHUB_OUTPUT"
printf 'backup_path=%s\n' "$BACKUP_PATH" >> "$GITHUB_OUTPUT"
shell: bash
- name: Validate deployment archive on server
run: |
ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "
if [ ! -f \"${REMOTE_ARCHIVE_PATH}\" ]; then
echo 'Deployment archive was not found on the server after upload.'
exit 1
fi
"
- name: Extract deployment archive on server
run: |
DIST_PATH="${{ steps.prepare_deployment.outputs.dist_path }}"
ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "
set -e
if ! tar -xzf \"${REMOTE_ARCHIVE_PATH}\" -C \"$DIST_PATH\"; then
echo 'Could not extract the deployment archive on the server.'
exit 1
fi
rm -f \"${REMOTE_ARCHIVE_PATH}\"
"
shell: bash
- name: Activate environment and install runtime dependencies
run: |
DIST_PATH="${{ steps.prepare_deployment.outputs.dist_path }}"
ACTIVATE_PATH="${{ steps.resolve_app_root.outputs.activate_path }}"
ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "
set -e
ENV_PATH=\"\$HOME/.env\"
set +e
. \"$ACTIVATE_PATH\"
ACTIVATE_STATUS=\$?
set -e
if [ ! -f \"\$ENV_PATH\" ]; then
echo 'Remote ~/.env was not found during runtime dependency installation.'
exit 1
fi
set -a
. \"\$ENV_PATH\"
set +a
if ! command -v node >/dev/null 2>&1; then
echo \"Activate exit status: \$ACTIVATE_STATUS\"
echo 'Node.js is not available after sourcing activate.'
exit 1
fi
if ! command -v npm >/dev/null 2>&1; then
echo \"Activate exit status: \$ACTIVATE_STATUS\"
echo 'npm is not available after sourcing activate.'
exit 1
fi
cd \"$DIST_PATH\"
if ! npm ci --omit=dev; then
echo 'Runtime dependency installation failed on the server.'
exit 1
fi
"
shell: bash
- name: Publish to www and restart PM2
run: |
APP_ROOT="${{ steps.resolve_app_root.outputs.app_root }}"
ACTIVATE_PATH="${{ steps.resolve_app_root.outputs.activate_path }}"
DIST_PATH="${{ steps.prepare_deployment.outputs.dist_path }}"
WWW_PATH="${{ steps.prepare_deployment.outputs.www_path }}"
BACKUP_PATH="${{ steps.prepare_deployment.outputs.backup_path }}"
ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "
set -e
ENV_PATH=\"\$HOME/.env\"
APP_NAME=\"${APP_NAME}\"
set +e
. \"$ACTIVATE_PATH\"
ACTIVATE_STATUS=\$?
set -e
if [ ! -f \"\$ENV_PATH\" ]; then
echo 'Remote ~/.env was not found before restarting PM2.'
exit 1
fi
set -a
. \"\$ENV_PATH\"
set +a
if [ -z \"\${PORT:-}\" ]; then
echo 'PORT is not defined in ~/.env.'
echo 'Set PORT in the server ~/.env before restarting the application.'
exit 1
fi
restore_previous() {
if [ -d \"$BACKUP_PATH\" ]; then
find \"$WWW_PATH\" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
cp -a \"$BACKUP_PATH/.\" \"$WWW_PATH/\" 2>/dev/null || true
fi
}
if [ ! -d \"$DIST_PATH\" ]; then
echo 'Temporary dist directory was not created correctly.'
exit 1
fi
if [ ! -f \"$DIST_PATH/package.json\" ]; then
echo 'package.json was not found in the extracted deployment.'
exit 1
fi
PM2_BIN=\"\$(command -v pm2)\"
NPM_BIN=\"\$(command -v npm)\"
if [ -z \"\$PM2_BIN\" ]; then
echo 'pm2 is not available after sourcing activate.'
exit 1
fi
if [ -z \"\$NPM_BIN\" ]; then
echo 'npm is not available after sourcing activate.'
exit 1
fi
echo \"Using pm2: \$PM2_BIN\"
echo \"Using npm: \$NPM_BIN\"
echo \"Using start script: npm run start:production\"
echo \"Publishing application to: $WWW_PATH\"
rm -rf -- \"$BACKUP_PATH\"
mkdir -p \"$BACKUP_PATH\"
if [ -d \"$WWW_PATH\" ]; then
cp -a \"$WWW_PATH/.\" \"$BACKUP_PATH/\" 2>/dev/null || true
fi
if ! find \"$WWW_PATH\" -mindepth 1 -maxdepth 1 -exec rm -rf {} +; then
echo 'Could not clean the www directory before publishing.'
restore_previous
exit 1
fi
if ! cp -a \"$DIST_PATH/.\" \"$WWW_PATH/\"; then
echo 'Could not copy the new deployment into www.'
restore_previous
exit 1
fi
pm2 delete \"$APP_NAME\" >/dev/null 2>&1 || true
pm2 kill >/dev/null 2>&1 || true
if ! pm2 start \"\$NPM_BIN\" --name \"$APP_NAME\" --cwd \"$WWW_PATH\" -- run start:production; then
echo 'PM2 failed to start the application using npm run start:production.'
restore_previous
pm2 start \"\$NPM_BIN\" --name \"$APP_NAME\" --cwd \"$WWW_PATH\" -- run start:production >/dev/null 2>&1 || true
pm2 save >/dev/null 2>&1 || true
exit 1
fi
if ! pm2 save; then
echo 'PM2 failed to save the current process list.'
exit 1
fi
"
shell: bash
- name: Verify application health
run: |
ACTIVATE_PATH="${{ steps.resolve_app_root.outputs.activate_path }}"
APP_ROOT="${{ steps.resolve_app_root.outputs.app_root }}"
ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "
set -e
ENV_PATH=\"\$HOME/.env\"
APP_NAME=\"${APP_NAME}\"
set +e
. \"$ACTIVATE_PATH\"
ACTIVATE_STATUS=\$?
set -e
if [ ! -f \"\$ENV_PATH\" ]; then
echo 'Remote ~/.env was not found during health check.'
exit 1
fi
set -a
. \"\$ENV_PATH\"
set +a
ATTEMPTS=15
SLEEP_SECONDS=2
SUCCESS=''
for i in \$(seq 1 \$ATTEMPTS); do
if curl -fsS --connect-timeout 2 --max-time 5 \"http://127.0.0.1:\${PORT}/\" >/dev/null; then
SUCCESS=1
break
fi
echo \"Waiting for application response... attempt \$i/\$ATTEMPTS\"
sleep \$SLEEP_SECONDS
done
if [ -z \"\$SUCCESS\" ]; then
echo 'Application health check failed after all attempts.'
pm2 logs \"$APP_NAME\" --lines 200 --nostream || true
exit 1
fi
"
shell: bash
- name: Restore previous www on failure
if: failure()
run: |
ACTIVATE_PATH="${{ steps.resolve_app_root.outputs.activate_path }}"
WWW_PATH="${{ steps.prepare_deployment.outputs.www_path }}"
BACKUP_PATH="${{ steps.prepare_deployment.outputs.backup_path }}"
if [ -z "$WWW_PATH" ] || [ -z "$BACKUP_PATH" ] || [ ! -f ~/.ssh/server_key ]; then
echo "WWW path, backup path, or SSH key is not available. Skipping rollback."
exit 0
fi
ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "
set -e
APP_NAME=\"${APP_NAME}\"
if [ ! -d \"$BACKUP_PATH\" ]; then
echo 'No previous www backup was found. Skipping rollback.'
exit 0
fi
set +e
. \"$ACTIVATE_PATH\"
set -e
find \"$WWW_PATH\" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
cp -a \"$BACKUP_PATH/.\" \"$WWW_PATH/\" 2>/dev/null || true
NPM_BIN=\"\$(command -v npm)\"
if [ -n \"\$NPM_BIN\" ] && [ -f \"$WWW_PATH/package.json\" ]; then
pm2 delete \"\$APP_NAME\" >/dev/null 2>&1 || true
pm2 start \"\$NPM_BIN\" --name \"\$APP_NAME\" --cwd \"$WWW_PATH\" -- run start:production >/dev/null 2>&1 || true
pm2 save >/dev/null 2>&1 || true
fi
echo 'Previous www backup was restored after deployment failure.'
"
shell: bash
- name: Remove temporary dist directory
if: always()
run: |
DIST_PATH="${{ steps.prepare_deployment.outputs.dist_path }}"
if [ -z "$DIST_PATH" ] || [ ! -f ~/.ssh/server_key ]; then
echo "Dist path or SSH key is not available. Skipping temporary directory cleanup."
exit 0
fi
ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "
rm -rf -- \"$DIST_PATH\"
"
shell: bash
- name: Remove temporary www backup
if: always()
run: |
BACKUP_PATH="${{ steps.prepare_deployment.outputs.backup_path }}"
if [ -z "$BACKUP_PATH" ] || [ ! -f ~/.ssh/server_key ]; then
echo "Backup path or SSH key is not available. Skipping backup cleanup."
exit 0
fi
ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "
rm -rf -- \"$BACKUP_PATH\"
"
shell: bash
- name: Remove legacy release artifacts
if: always()
run: |
APP_ROOT="${{ steps.resolve_app_root.outputs.app_root }}"
if [ -z "$APP_ROOT" ] || [ ! -f ~/.ssh/server_key ]; then
echo "Application root or SSH key is not available. Skipping legacy artifact cleanup."
exit 0
fi
ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "
rm -rf -- \"$APP_ROOT/releases\" \"$APP_ROOT/current\"
"
shell: bash
- name: Remove uploaded archive
if: always()
run: |
if [ ! -f ~/.ssh/server_key ]; then
echo "SSH key was not configured. Skipping remote archive cleanup."
exit 0
fi
ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "
rm -f -- \"${REMOTE_ARCHIVE_PATH}\"
"
shell: bash
- name: Show PM2 status
run: |
ACTIVATE_PATH="${{ steps.resolve_app_root.outputs.activate_path }}"
APP_ROOT="${{ steps.resolve_app_root.outputs.app_root }}"
ssh -i ~/.ssh/server_key "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "
set +e
. \"$ACTIVATE_PATH\"
ACTIVATE_STATUS=\$?
set -e
pm2 list
"
shell: bash
No nano, salve e saia com: Ctrl + O, Enter, Ctrl + X.
Depois:
git add .github/workflows/ci-cd-pipeline.yml
git commit -m "Adiciona fluxo de CI/CD nativo com PM2"
git push
Passo a passo do primeiro deploy
0. Criar a aplicação e preparar o acesso SSH
Na hospedagem, crie uma aplicação do tipo Node.js.
Antes de configurar as chaves do GitHub Actions, defina também uma senha de acesso manual ao servidor. Isso permite entrar por SSH para testar conexão, criar o ~/.env, validar o PM2 e executar os comandos operacionais do processo.
Na hospedagem, acesse:
Painel da aplicação > Acessos > Senha SFTP/SSH > Alterar Senha
Nesta etapa:
- defina a senha do usuário da aplicação para o acesso manual via
SSH - confirme que
Usar SSHestá ativado
Depois, teste o acesso manual pela sua máquina local:
ssh <usuario>@<host>
1. Padronizar o script de produção no projeto
Antes do primeiro deploy, o projeto precisa expor um script start:production no package.json. Esse é o contrato que o pipeline usa para subir a aplicação no PM2.
O script deve ficar dentro da chave "scripts" do package.json. O exemplo abaixo é apenas um mapa visual para mostrar onde alterar:
{
"scripts": {
... ↓
"build": "astro build",
"start:production": "node dist/server/entry.mjs",
... ↑
}
}
No package.json real, adicione/altere apenas os comandos de build e start:production conforme o framework:
Astro SSR
"build": "astro build",
"start:production": "node dist/server/entry.mjs"
Next.js standalone
"build": "next build",
"start:production": "node .next/standalone/server.js"
NestJS
"build": "nest build",
"start:production": "node dist/main.js"
Express ou Fastify com TypeScript
"build": "tsc",
"start:production": "node dist/server.js"
Express ou Fastify sem build
"build": "echo \"No build step\"",
"start:production": "node server.js"
O importante é manter os nomes build e start:production, mesmo que os comandos internos mudem de framework para framework.
Na sua máquina local, dentro da pasta do projeto, faça um teste mínimo:
npm ci
npm run build
npm run start:production
O comando start:production precisa iniciar um servidor HTTP escutando em process.env.PORT. Se ele não subir localmente, ajuste o script antes de seguir para o deploy.
Sessions no Astro
Se o projeto utilizar sessions no Astro e não houver outro storage configurado, defina explicitamente o driver abaixo no astro.config:
session: {
driver: "fs-lite",
options: {
base: "./.astro/sessions",
},
},
Esse ajuste faz com que as sessões sejam salvas em uma pasta local da própria aplicação em execução. Isso é importante principalmente quando o build acontece fora do servidor, como no GitHub Actions, porque evita que o runtime tente usar caminhos absolutos do ambiente de build.
Use essa configuração quando o projeto tiver sessions ativas e não estiver utilizando outro storage, como Redis.
2. Gerar a chave SSH do GitHub Actions
Na sua máquina local ou em uma máquina segura:
ssh-keygen -t ed25519 -C "github-actions-deploy" -f github_actions_deploy
Quando o terminal pedir a passphrase, deixe em branco e pressione Enter nas duas perguntas. Essa chave deve ser criada sem passphrase para que o GitHub Actions consiga usá-la sem prompt interativo.
Esse comando vai gerar:
github_actions_deploy: chave privadagithub_actions_deploy.pub: chave pública
Copie e cole sempre o conteúdo diretamente do arquivo original, sem alterar quebras de linha, sem adicionar aspas e sem editar manualmente o texto da chave.
Se quiser visualizar e copiar os conteúdos logo em seguida:
cat github_actions_deploy
cat github_actions_deploy.pub
Exemplo visual do que significa copiar a chave completa:
Chave privada:
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
Chave pública:
ssh-ed25519 ... github-actions-deploy
A cópia deve incluir tudo, inclusive as linhas de início e fim da chave privada.
3. Configurar as chaves e secrets
Copie o conteúdo completo de github_actions_deploy.pub para o servidor ou para a interface da hospedagem. Esta é a chave pública.
Opção 1:
Na hospedagem, acesse:
Painel da aplicação > Acessos > Senha SFTP/SSH > Alterar Senha > Chaves Autorizadas
Cole o conteúdo completo do arquivo github_actions_deploy.pub no campo Chaves Autorizadas, do início ao fim e sem modificar a estrutura da chave. Mantenha Usar SSH ativado e salve em Atualizar.
Opção 2:
No servidor:
mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys
Cole dentro desse arquivo o conteúdo completo de github_actions_deploy.pub, do início ao fim, exatamente como ele foi gerado.
No nano, salve e saia com: Ctrl + O, Enter, Ctrl + X.
Em seguida:
chmod 600 ~/.ssh/authorized_keys
No GitHub
Copie o conteúdo completo de github_actions_deploy para o secret SERVER_SSH_KEY no GitHub. Esta é a chave privada.
Depois, no repositório, acesse Settings > Secrets and variables > Actions > New repository secret.
Criar:
SERVER_HOSTValor:<ip-ou-host-do-servidor>SERVER_USERValor:<usuario>SERVER_SSH_KEYValor: cole a chave privada completa do arquivogithub_actions_deploy, do início ao fim e sem modificar a estrutura da chave
4. Criar o arquivo ~/.env no servidor
Esse arquivo é criado uma única vez e vira a fonte de verdade do ambiente de produção.
Conteúdo obrigatório: este pipeline exige ~/.env no servidor; a linha PORT=<porta-da-aplicacao> é obrigatória.
Ele é usado em dois momentos:
- no build da aplicação, caso a aplicação precise de variáveis já durante o processo de build
- no runtime da aplicação, quando o workflow carrega o
~/.envno shell remoto antes de iniciar o processo noPM2
No servidor:
ssh <usuario>@<ip-ou-host-do-servidor>
nano ~/.env
Cole a base abaixo e ajuste os valores do seu projeto. Se a aplicação exigir mais variáveis, adicione abaixo no mesmo arquivo.
NODE_ENV=production
HOST=0.0.0.0
PORT=<porta-da-aplicacao>
No nano, salve e saia com: Ctrl + O, Enter, Ctrl + X.
Sempre que você alterar o ~/.env do servidor, basta disparar um novo deploy para rebuildar o pacote de produção e reiniciar a aplicação com as novas variáveis.
Escolha uma porta livre e fixa para cada aplicação. Em hospedagem compartilhada, evite reutilizar a mesma porta em mais de um projeto. O domínio só passa a abrir publicamente depois que o proxy reverso da hospedagem apontar para essa porta.
5. Ativar o PM2 no boot
Depois do primeiro deploy bem-sucedido, faça essa ativação uma única vez para que a aplicação volte sozinha após reinicializações do servidor.
No servidor:
source ~/*.configr.cloud/activate
cd ~/*.configr.cloud/www
pm2 startup
Depois disso, o pm2 startup vai imprimir um novo comando com privilégios elevados.
- copie esse comando gerado
- envie esse comando ao suporte usando o modelo da etapa seguinte
- depois que o suporte confirmar a execução, volte ao servidor e rode:
pm2 save
Esse pm2 save grava o estado atual para que o PM2 saiba qual processo deve restaurar automaticamente no boot.
6. Enviar a mensagem para o suporte da Cloud
Depois de concluir a configuração e obter o comando gerado pelo pm2 startup, envie esta mensagem ao suporte:
Título do ticket: Configuração de Proxy Reverso HTTPS e PM2 Startup
Olá, tudo bem?
Poderiam configurar o proxy reverso do domínio <projeto.seudominio.com.br> apontando para a porta <porta-da-aplicacao>, com HTTPS ativo?
Além disso, poderiam executar o comando abaixo com sudo, gerado pelo `pm2 startup`, para registrar a inicialização automática do PM2 do usuário da aplicação?
<comando-gerado-pelo-pm2-startup>
Depois disso, eu mesmo executarei o `pm2 save` para salvar o estado atual da aplicação.
Etapas de apoio
▸ Checklist antes de rodar o workflow
Antes de fazer o primeiro git push ou clicar em Run workflow, confira:
- workflow criado em
.github/workflows/ci-cd-pipeline.yml - aplicação criada como
Node.jsna hospedagem - acesso manual por
SSHtestado - chave pública autorizada no servidor
- secrets
SERVER_HOST,SERVER_USEReSERVER_SSH_KEYcriados no GitHub - arquivo
~/.envcriado no servidor - variável
PORTdefinida no~/.env - script
start:productioncriado nopackage.json npm run buildfuncionando localmente- branch principal confirmada, normalmente
main
▸ Como o deploy funciona no dia a dia
Deploy automático
O deploy automático acontece sempre que você envia código para a branch main.
Use normalmente:
git add .
git commit -m "Atualizar projeto"
git push
As etapas de lint e test rodam com --if-present, então o workflow continua compatível com projetos que ainda não possuem esses scripts.
Deploy manual
Se quiser rodar o mesmo processo sem fazer um novo commit, use o disparo manual do workflow no GitHub:
GitHub > Actions > CI/CD Pipeline > Run workflow
Isso executa exatamente a mesma esteira do deploy automático, reaproveitando o commit atual da main.
▸ Comandos úteis de monitoramento
Para validar a aplicação no servidor:
source ~/*.configr.cloud/activate
cd ~/*.configr.cloud/www
pm2 list
pm2 logs <nome-da-aplicacao>
pm2 monit
curl http://127.0.0.1:<porta-da-aplicacao>/
Se quiser inspecionar os arquivos publicados:
ls -la ~/*.configr.cloud/www
▸ Como saber que deu certo
Use os comandos da etapa anterior e confira estes resultados:
pm2 list: a aplicação aparece com statusonlinepm2 logs <nome-da-aplicacao>: não mostra erro de inicialização ou loop de restartcurl http://127.0.0.1:<porta-da-aplicacao>/: responde sem erro no servidorls -la ~/*.configr.cloud/www: mostra os arquivos atuais da aplicação- GitHub Actions: o workflow terminou verde
- navegador: o domínio abre depois do proxy reverso configurado
▸ Erros comuns
Use a mensagem do log do GitHub Actions como ponto de partida:
SERVER_SSH_KEY is empty or was not configured in GitHub Secrets.: o secretSERVER_SSH_KEYnão foi criado ou está vazioCould not add SERVER_HOST to known_hosts.:SERVER_HOSTestá vazio, incorreto ou inacessível pelo runnerCould not connect to the server using SSH.: reviseSERVER_HOST,SERVER_USER,SERVER_SSH_KEYe a chave pública autorizada no servidorLoad key ... error in libcrypto: chave privada colada de forma incompleta ou com estrutura alterada no secretSERVER_SSH_KEYPermission denied (publickey,password): chave pública não autorizada no servidor ouSERVER_USER/SERVER_HOSTincorretoRemote ~/.env was not found.: o arquivo~/.envainda não foi criado no servidorRemote ~/.env is empty.: o arquivo existe, mas não tem variáveis salvasApplication build failed.:npm run buildfalhou; confira dependências, scripts e variáveis usadas no buildpackage.json must define scripts.start:production.: faltou o scriptstart:productionnopackage.jsonCould not upload dist.tar.gz to the server.: falha de SSH, permissão ou espaço em disco no servidorCould not find a *.configr.cloud application directory: a aplicação Node.js não foi criada corretamente na hospedagemCould not find the activate script: a aplicação Node.js da hospedagem não possui o arquivoactivateesperadoDeployment archive was not found on the server after upload.: o pacote não chegou ao servidor no caminho esperadoCould not extract the deployment archive on the server.: o pacote enviado está incompleto, corrompido ou sem permissão de leituraRuntime dependency installation failed on the server.:npm ci --omit=devfalhou no servidorPORT is not defined in ~/.env.: faltouPORTno~/.envdo servidorTemporary dist directory was not created correctly.: o diretório temporáriodist-tmpnão pôde ser preparado no servidorPrevious www backup was restored after deployment failure.: o workflow restaurou automaticamente a cópia temporária anterior dewwwpm2 is not available after sourcing activate.: oPM2não está disponível no ambiente ativado da aplicaçãoPM2 failed to start the application using npm run start:production.: o script de produção falhou ou aponta para um arquivo inexistenteApplication health check failed after all attempts.: a aplicação não respondeu emhttp://127.0.0.1:<porta>/; confiraPORT, logs doPM2e proxy reverso