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 initexecutado) - 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 comsource: { 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 depull/(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: 3Nota: 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 listNota:
app_typee obrigatorio no state (use o mesmotype:do manifesto, ou o valor desystem:); sem ele o Runner considera o state corrompido.instancefixa a instancia do registro (ver Permissão de escrita nas pastas montadas e o aviso de instância não fixada).
Nota:
dist_branche 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 pelorunner 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 deimage:vsbuild:), não pelo valor desystem.
Passo 4: Deploy
runner deploy meu-app -VOutput 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 20Atualizando 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-appA 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_fileComo funciona:
- Sem
path, o Runner usapull/como está e não grava nada nele (nem o bundle de secrets): gravar mudaria a árvore e o cron redeployaria em loop. Sepull/for um checkout git seu, orunner fetch --forcetambém não fazgit pullnele. - Com
path, cada deploy copia a origem parapull/(pasta nova e troca atômica). Link simbólico que aponta para fora dopathnão é seguido. Opathnã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 comoorigem em mudançae 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>ouctl:<sha256>, e é ela que orunner statusmostra emrevision:. A comparação é da string inteira. - A versão da pasta
src/<instância>/<versão>segue oversion:do manifesto, lido só depull/. Sem blocoversion:, ou comversion.source: git-commit/git-tage sem git empull/, 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: localNesse 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: localrunner 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: localrunner deploy meu-worker
# Container roda sem Traefik, sem healthcheck HTTP
# Runner aceita "running" como sucessoMigrar 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_xxxYYYzzzA 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 --forceBuild falha
Testar localmente primeiro:
cd /data/apps/meu-app
docker build -t test .
docker run --rm -p 8000:8000 test
curl http://localhost:8000/healthContainer sobe mas health falha
Ver guia de healthcheck. Para deploy local sem
HTTP, use healthcheck.path: "" ou healthcheck.tcp: <porta>.