Deploy de Repositorio Local

Este tutorial mostra como deployar uma aplicacao a partir de codigo que ja esta no servidor — sem depender de um repositorio GitHub remoto. Util para:

  • Servidores sem acesso a internet
  • Projetos internos sem repositorio externo
  • Testes rapidos e prototipagem
  • Migracoes de outros sistemas de deploy

Cenario

Voce tem o codigo da aplicacao em /home/deploy/meu-app/ no servidor e quer deployar via Runner sem configurar GitHub.

Pre-requisitos

  • Runner instalado (runner init executado)
  • Docker e Traefik rodando no servidor
  • Codigo da aplicacao no servidor

Requer runner v2.155.0 ou mais novo. Em versões anteriores, o deploy local falhava com no_repo_configured, porque o Runner exigia um repositório mesmo com source: { type: local }.

Desde a v2.163.0 o cron (runner fetch --deploy) detecta mudança no código local e faz o deploy sozinho, e a origem pode ficar fora de pull/ (path:). Ver Detecção de mudança abaixo. Para buscar o código de um servidor HTTPS ou por rsync, veja Sources de arquivo.

Passo 1: Preparar o Diretorio

O código da app vai em pull/, dentro do diretório da app em /data/apps/ (ou no apps_dir configurado no config.yml). É de lá que o Runner lê o código a cada deploy, sem git:

mkdir -p /data/apps/meu-app/pull
cp -r /home/deploy/meu-app/. /data/apps/meu-app/pull/

Estrutura necessaria

/data/apps/meu-app/
├── .deploy.yml          # Obrigatorio (cópia canônica do manifesto)
└── pull/                # Código da app
    ├── .deploy.yml      # O mesmo manifesto
    ├── Dockerfile       # Se usar build mode
    ├── src/             # Codigo fonte (ou dist/)
    ├── requirements.txt # Ou package.json, etc
    └── version.json     # Versão da app (ver abaixo)

O .deploy.yml fica nos dois lugares, com o mesmo conteúdo: na raiz é a cópia que o Runner lê; em pull/ é a que acompanha o código. O runner preflight acusa quando os dois divergem.

Se a pasta pull/ não existir, o deploy para com source_local_sem_codigo, dizendo onde colocar o código.

Passo 2: Criar .deploy.yml

# /data/apps/meu-app/.deploy.yml (e a cópia em pull/.deploy.yml)
project: meu-app
system: sys
port: 8000

# Modo BUILD (com Dockerfile local)
build:
  context: .
  dockerfile: Dockerfile

# Sem secao github: — deploy local nao precisa
# (Nota: o bloco `github:` foi DEPRECATED em v2.16.0 mesmo para deploys remotos)

version:
  source: file              # opcional desde v2.18.2
  file: version.json
  field: version
# Desde v2.18.2, generator default emite `source: git-commit` (deploys
# auto-versionados via SHA, sem precisar `version` field em arquivo).

networks:
  - public

healthcheck:
  path: /health           # endpoint a ser implementado pela app (HTTP 2xx)
  start_period: 30s
  interval: 10s
  timeout: 5s
  retries: 3

instances:
  production:
    domain: app.meusite.com.br
    source:
      type: local      # Usa codigo local, nao git
    keep_versions: 3

Nota: sem a secao github:, o Runner nao tenta fazer git fetch. O deploy usa o codigo que ja esta em /data/apps/meu-app/.

Criar version.json

{
  "version": "1.0.0"
}

Criar Dockerfile

FROM python:3.12-slim
WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY src/ ./src/

EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "main:app"]

Passo 3: Registrar Manualmente

Para deploys locais, em vez de runner add --repo, criamos o state file manualmente:

# Criar state file
cat > /opt/runner/state/meu-app.yml << 'EOF'
app_path: /data/apps/meu-app
project: meu-app
system: sys
app_type: sys
dist_branch: ""
current_version: null
status: registered
instance: production
EOF

# Verificar
runner list

Nota: app_type e obrigatorio no state (use o mesmo type: do manifesto, ou o valor de system:); sem ele o Runner considera o state corrompido. instance fixa a instancia do registro (ver Permissão de escrita nas pastas montadas e o aviso de instância não fixada).

Nota: dist_branch e o campo interno que armazena a branch de deploy. Vazio significa deploy local (sem git fetch). Para apps com remote, o valor e preenchido automaticamente pelo runner add --branch X.

Nota: system: aceita qualquer string descritiva (sys/api/front/worker/scanner/etc.) — desde v2.x não há mais enum fechado. O modo de deploy é decidido pelo .deploy.yml (presença de image: vs build:), não pelo valor de system.

Passo 4: Deploy

runner deploy meu-app -V

Output esperado:

[1/7] FETCH     Baixando codigo fonte...
  (skipped — local source, no git remote)
[2/7] PREPARE   Preparando versao v1.0.0...
[3/7] SECURITY  Verificando seguranca...
[4/7] BUILD     Building image meu-app:v1.0.0...
[5/7] CONTAINER Iniciando container...
[6/7] HEALTH    Verificando saude...
[7/7] ROUTING   Configurando Traefik...

Passo 5: Verificar

# Health check
curl -s https://app.meusite.com.br/health

# Container
docker ps | grep meu-app

# Logs
docker logs meu-app_production_v1-0-0 --tail 20

Atualizando o Deploy

Para atualizar o codigo local e redeployar:

# 1. Atualizar codigo
cp -r /home/deploy/meu-app-novo/. /data/apps/meu-app/pull/

# 2. Atualizar versao
echo '{"version": "1.1.0"}' > /data/apps/meu-app/pull/version.json

# 3. Redeployar
runner deploy meu-app

A versão nova (1.1.0) dispara o deploy. Sem mudar a versão, o Runner responde JÁ IMPLANTADO; aí use runner deploy meu-app --force.

Desde a v2.163.0, o cron (runner fetch --deploy) faz isso sozinho: quando a árvore de pull/ (ou do path:) muda, ele pede o deploy. Antes, a app aparecia como sem git e o deploy era sempre comandado.

Detecção de mudança (`detect`)

instances:
  production:
    source:
      type: local
      path: /srv/entregas/meu-app   # opcional; sem path = /data/apps/meu-app/pull
      detect: tree                  # tree (padrão) | control_file | manual
      exclude: [".git", "node_modules"]   # só no modo tree; padrão: [".git"]
detect O cron compara Quando usar
tree (padrão) o sha256 da árvore inteira (caminho, permissão e conteúdo de cada arquivo, sem o exclude) o código é copiado inteiro a cada entrega
control_file só o conteúdo do control_file (relativo ao path), ex.: VERSION quem entrega grava um arquivo de versão por último; árvores grandes
manual nada: o cron não implanta o deploy é sempre comandado (runner deploy)
    source:
      type: local
      path: /srv/entregas/meu-app
      detect: control_file
      control_file: VERSION         # obrigatório com detect: control_file

Como funciona:

  • Sem path, o Runner usa pull/ como está e não grava nada nele (nem o bundle de secrets): gravar mudaria a árvore e o cron redeployaria em loop. Se pull/ for um checkout git seu, o runner fetch --force também não faz git pull nele.
  • Com path, cada deploy copia a origem para pull/ (pasta nova e troca atômica). Link simbólico que aponta para fora do path não é seguido. O path não pode ficar dentro do diretório da app nem conter ele.
  • No modo tree, o cron lê a árvore duas vezes, com 5 s de intervalo. Se ela mudou no meio (a entrega ainda está sendo copiada), a app aparece como origem em mudança e o cron tenta de novo no próximo ciclo. Isso não conta como erro para o auto-pause.
  • A revisão que o cron compara é tree:<sha256> ou ctl:<sha256>, e é ela que o runner status mostra em revision:. A comparação é da string inteira.
  • A versão da pasta src/<instância>/<versão> segue o version: do manifesto, lido só de pull/. Sem bloco version:, ou com version.source: git-commit/git-tag e sem git em pull/, a versão é <tipo>-<12 hex> da revisão (ex.: tree-3f9a1c2b7d4e).

Modo IMAGE (sem Dockerfile)

Se voce nao quer buildar uma imagem e prefere usar uma imagem pronta com o codigo montado via volume:

# .deploy.yml
project: meu-app
port: 80
image: caddy:2-alpine    # Imagem pronta

instances:
  production:
    domain: app.meusite.com.br
    source:
      type: local

Nesse caso o código de pull/ é copiado para a versão (src/<instancia>/<versao>/) e montado em /app dentro do container.

Exemplo: Frontend Estatico (Caddy)

# Estrutura
/data/apps/meu-front/
├── .deploy.yml
└── pull/
    ├── .deploy.yml
    ├── dist/
    │   ├── index.html
    │   ├── assets/
    │   └── ...
    └── Caddyfile
# .deploy.yml
project: meu-front
port: 80
image: caddy:2-alpine

instances:
  production:
    domain: meusite.com.br
    source:
      type: local
runner deploy meu-front
# Caddy serve os arquivos estaticos de /app/dist/

Exemplo: Worker Python (sem dominio)

Servicos que nao precisam de Traefik (workers, cron jobs):

# .deploy.yml
project: meu-worker
port: 0                   # Sem porta HTTP

build:
  context: .
  dockerfile: Dockerfile

healthcheck:
  path: ""                # Sem healthcheck HTTP

# Sem instances.production.domain — o container roda sem Traefik
# Usa ports: para bind direto se necessario
instances:
  production:
    source:
      type: local
runner deploy meu-worker
# Container roda sem Traefik, sem healthcheck HTTP
# Runner aceita "running" como sucesso

Migrar de Local para GitHub

Quando quiser migrar de deploy local para deploy via GitHub:

# 1. Criar repo no GitHub
gh repo create devborlot/meu-app --private

# 2. Push do codigo
cd /data/apps/meu-app
git init && git add . && git commit -m "initial"
git remote add origin https://github.com/devborlot/meu-app.git
git push -u origin main

# 3. Re-registrar a app no Runner com o repo (v2.16.0+: o repo fica
#    no state file, não mais em `github:` no .deploy.yml — esse bloco
#    foi DEPRECATED).
runner unregister meu-app
runner add --repo devborlot/meu-app --branch main --token ghp_xxxYYYzzz

A partir dai o Runner detecta pushes na branch configurada (--branch main neste exemplo) automaticamente.

Troubleshooting

"No .deploy.yml found"

O arquivo precisa estar em /data/apps/meu-app/.deploy.yml (raiz do app dir).

"Already deployed" (sem --force)

Para redeploy local sem mudar versao:

runner deploy meu-app --force

Build falha

Testar localmente primeiro:

cd /data/apps/meu-app
docker build -t test .
docker run --rm -p 8000:8000 test
curl http://localhost:8000/health

Container sobe mas health falha

Ver guia de healthcheck. Para deploy local sem HTTP, use healthcheck.path: "" ou healthcheck.tcp: <porta>.

By Borlot.com.br on 22/10/2026