Runner self-hosted do GitHub Actions em VPS
Configure um runner self-hosted no Ubuntu 24.04 com usuário dedicado, checksum, config.sh e systemd. Entenda também o risco de pull requests de forks.
O que faz um runner self-hosted do GitHub Actions
Um runner self-hosted do GitHub Actions é um programa que instala no seu próprio VPS. O programa consulta o GitHub em busca de jobs e executa-os no seu hardware. Regista-o num repositório, instala-o como um serviço systemd e ele volta a iniciar depois de cada reboot. O GitHub agenda o job. O seu servidor executa o trabalho.
Usar CI (integração contínua) numa máquina que controla é útil por dois motivos. Os minutos de compilação deixam de ser contabilizados, e um job pode aceder a recursos que só a sua máquina tem, como uma cache de compilação aquecida ou uma rede privada. O custo é a segurança. O runner executa tudo o que o ficheiro de workflow indicar, com o utilizador que lhe atribuiu. Por isso, um ficheiro de workflow equivale, por definição, à execução remota de código. Num repositório privado, isto é aceitável, porque apenas pessoas de confiança podem adicionar um workflow. Num repositório público, existe um risco real. A secção sobre pull requests de forks explica o mecanismo.
Tudo o que se segue usa Ubuntu 24.04 e a versão 2.338.0 do runner, a release atual em 12 October 2026.
O que precisa de ter antes de começar
Comece com um VPS que tenha uma conta de administração normal e sudo, no estado descrito em os primeiros dez minutos num VPS novo. Não precisa de abrir nenhuma porta de entrada. O runner abre uma ligação HTTPS (hypertext transfer protocol secure) de saída para o GitHub e mantém-na aberta enquanto aguarda trabalho. Por isso, o GitHub nunca se liga ao seu servidor. A firewall pode continuar fechada para o exterior e os jobs continuarão a chegar.
Também precisa de direitos de administração no repositório, porque o token de registo é apresentado nas definições do repositório.
Crie um utilizador dedicado para o runner
Nunca execute o runner como root nem como o seu próprio utilizador de administração. Cada job herda as permissões do utilizador do runner, pelo que um workflow que chama sudo é executado com sucesso se o utilizador do runner puder usar sudo. Crie um utilizador sem privilégios que não seja proprietário de nada além do seu próprio diretório pessoal. Contas de utilizador com privilégios mínimos num VPS explica o padrão geral. Este é o caso específico.
sudo useradd -m -s /bin/bash gharunner
sudo passwd -l gharunner
sudo chmod 750 /home/gharunner
sudo install -d -m 700 -o gharunner -g gharunner /home/gharunner/actions-runnerpasswd -l bloqueia a palavra-passe, pelo que ninguém pode iniciar sessão como gharunner usando uma palavra-passe. O modo 700 no diretório do runner é importante porque o runner armazena aí as suas credenciais em texto simples, e um checkout pode conter código-fonte privado.
Confirme ambas as propriedades antes de continuar:
sudo passwd -S gharunner
sudo -l -U gharunnerpasswd -S imprime uma linha que começa por gharunner L, onde L significa que a palavra-passe está bloqueada. sudo -l -U gharunner deve responder com is not allowed to run sudo. Se imprimir uma lista de comandos permitidos, a conta pertence a um grupo sudo e o isolamento que acabou de criar deixou de existir.
Baixe o runner e verifique o tarball
A partir daqui, trabalhe como o utilizador runner.
sudo -iu gharunner
cd ~/actions-runner
RUNNER_VERSION=2.338.0
curl -fL -o actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz \
"https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz"Execute uname -m primeiro se não tiver a certeza da arquitetura. x86_64 usa o ficheiro linux-x64 acima. aarch64 usa actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz.
Agora verifique o que foi descarregado. O SHA256 (algoritmo de hash seguro, 256 bits) abaixo corresponde ao tarball x64 da versão 2.338.0. O GitHub apresenta o valor da versão atual na página da release e no ecrã New self-hosted runner. O valor muda a cada versão. Por isso, copie-o dessas páginas quando instalar outra versão.
echo "af4b794c1bc41d73d40535e3fe092a39f9679cd8d965954c2aca25a05ca41d32 actions-runner-linux-x64-2.338.0.tar.gz" | sha256sum -cUm download correto apresenta uma linha:
actions-runner-linux-x64-2.338.0.tar.gz: OKUm ficheiro truncado ou alterado apresenta a falha e um aviso:
actions-runner-linux-x64-2.338.0.tar.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT matchNão ignore esta verificação e deixe tar detetar o problema mais tarde. Um arquivo escrito apenas parcialmente falha com gzip: stdin: unexpected end of file e tar: Unexpected EOF in archive. Isto indica que o ficheiro está danificado, mas não se foi truncado ou substituído.
tar xzf ./actions-runner-linux-x64-2.338.0.tar.gz
lsO que o tarball contém e o que não contém
Depois da extração, o diretório contém config.sh, run.sh, env.sh, safe_sleep.sh, bin/ e externals/. bin/ contém os binários do runner e bin/installdependencies.sh. externals/ contém o runtime Node incluído, que executa as actions JavaScript.
Ainda não existe svc.sh. A documentação do GitHub descreve-o como o script «que é criado depois de adicionar o runner com sucesso», porque é escrito a partir de um template que inclui o repositório e o nome do runner no nome do serviço. Por isso, sudo ./svc.sh install antes de ./config.sh falha com sudo: ./svc.sh: command not found. Registe primeiro o runner e instale depois o serviço.
Instale as dependências do runner
O runner é uma aplicação .NET, por isso precisa de algumas bibliotecas partilhadas. Mantenha a shell do utilizador do runner e instale-as com sudo, porque o script escreve na base de dados de pacotes do sistema.
exit
cd /home/gharunner/actions-runner
sudo ./bin/installdependencies.shNo Ubuntu 24.04, esse comando instala libkrb5-3, zlib1g, liblttng-ust1t64, libssl3t64 e libicu74. O script tenta vários nomes de versão para cada biblioteca e mantém o nome disponibilizado pela sua release. Por isso, o mesmo script funciona em versões mais antigas do Ubuntu e no Debian.
Se ignorar este passo e faltar uma dessas bibliotecas, ./config.sh para antes de executar qualquer operação:
Dependencies is missing for Dotnet Core 6.0
Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies.A ausência de libicu apresenta a mesma recomendação, mas com uma primeira linha diferente: Libicu's dependencies is missing for Dotnet Core 6.0. Ambas as mensagens têm a mesma origem: antes de começar, config.sh verifica as bibliotecas incluídas com ldd e libicu com ldconfig. Assim, uma ligação não resolvida faz o script parar em vez de provocar uma falha confusa mais tarde.
Registe o runner no seu repositório
Obtenha um token no repositório. Abra Settings, depois Actions, depois Runners e, por fim, New self-hosted runner. A página mostra um token de registo. O token expira uma hora depois de ser criado, por isso gere-o quando estiver pronto para o colar.
Registe o runner como o utilizador do runner. O config.sh recusa-se a ser executado com sudo.
sudo -iu gharunner
cd ~/actions-runner
./config.sh --url https://github.com/YOUR-USER/YOUR-REPO \
--token PASTE_REGISTRATION_TOKEN_HERE \
--name vps-runner-1 \
--labels vps \
--work _work \
--unattended \
--replaceO que fazem estas opções. --name define o nome com que o runner aparece no repositório, por isso escolha um nome que continue a reconhecer daqui a seis meses. --labels adiciona as suas próprias labels; o runner já inclui self-hosted, Linux e X64 automaticamente. --work define o diretório onde os checkouts são colocados, dentro do diretório do runner. --unattended responde às perguntas interativas com os valores predefinidos, que é o comportamento pretendido quando o comando está num script. --replace assume o registo existente com o mesmo nome em vez de falhar, que é o comportamento pretendido quando reconstrói o servidor.
Uma execução bem-sucedida termina com estas linhas:
√ Runner successfully added
√ Runner connection is good
√ Settings Saved.O registo fica agora guardado no diretório do runner como .runner, .credentials e .credentials_rsaparams. Os dois últimos identificam este runner perante o GitHub, por isso qualquer pessoa que consiga lê-los pode fazer-se passar por ele. Por esse motivo, o diretório tem o modo 700 e o utilizador não tem sudo.
Instalar o runner como serviço systemd
./run.sh num terminal é suficiente para um teste, mas termina quando a sessão SSH termina. Instale o serviço para que o runner arranque no boot. serviços e timers do systemd numa VPS explica os próprios ficheiros de unidade. Aqui, svc.sh cria um ficheiro de unidade por si.
exit
cd /home/gharunner/actions-runner
sudo ./svc.sh install gharunner
sudo ./svc.sh start
sudo ./svc.sh statussvc.sh requer root porque escreve uma unidade em /etc/systemd/system e ativa-a. O argumento depois de install é o utilizador com que o serviço é executado. Passe gharunner explicitamente. Sem argumento, o script usa $SUDO_USER, que é a sua conta de administrador, e cada job é então executado por um utilizador que pode usar sudo.
A unidade recebe o nome do repositório e do runner, no formato actions.runner.YOUR-USER-YOUR-REPO.vps-runner-1.service. Nunca precisa de escrever esse nome manualmente:
systemctl list-units 'actions.runner.*'
sudo journalctl -u 'actions.runner.*' -n 20 --no-pagerUm runner em funcionamento regista √ Connected to GitHub e, em seguida, uma linha terminada em Listening for Jobs. A página Runners do repositório mostra-o como Idle. Um runner apresentado como Offline não está em execução ou não consegue alcançar o GitHub na porta 443.
Enviar um job para o runner
runs-on seleciona um runner pela label. Peça self-hosted e a sua própria label, para impedir que o job seja executado num runner diferente do pretendido.
name: build
on:
push:
branches: [main]
jobs:
build:
runs-on: [self-hosted, linux, vps]
steps:
- uses: actions/checkout@v5
- run: uname -aSe o job ficar à espera em Waiting for a runner to pick up this job, as labels não correspondem. Todas as labels em runs-on têm de existir no runner. Uma palavra adicional deixa o job na fila sem gerar qualquer erro. Compare a lista com as labels apresentadas junto ao runner nas definições do repositório.
Por que self-hosted runners e repositórios públicos não combinam
Esta é a parte que muitas pessoas ignoram. A orientação do GitHub é clara: os self-hosted runners "quase nunca devem ser usados em repositórios públicos" e "não oferecem garantias de execução em máquinas virtuais efémeras e limpas, podendo ser comprometidos de forma persistente por código não confiável num workflow".
O mecanismo é simples. Um pull request de um fork inclui a sua própria cópia do ficheiro de workflow. Se o seu repositório público executar workflows de pull request no seu runner, qualquer pessoa que consiga criar um fork do repositório pode propor um workflow que execute os seus comandos no seu VPS. Não precisa de acesso de escrita, porque aquilo que está a propor é precisamente o que será executado.
As definições de aprovação reduzem este risco, mas não o eliminam. A política predefinida de um repositório público pede a um maintainer que aprove o workflow de um fork criado por um contribuidor pela primeira vez. O GitHub considera alguém um contribuidor pela primeira vez apenas até que qualquer commit ou pull request dessa pessoa seja integrado no repositório. Depois disso, os pull requests seguintes são executados sem pedir aprovação, e o próprio GitHub alerta que uma correção trivial integrada é suficiente para chegar a esse ponto. Assim, o controlo consiste numa pessoa ler um diff, e é fácil não detetar um payload escondido três níveis abaixo num script de build.
Um pull request de um fork não recebe os seus secrets, e o seu GITHUB_TOKEN é apenas de leitura. Isto limita os danos dentro do GitHub. Não protege o seu servidor. O atacante obtém uma shell como gharunner, podendo ler todos os ficheiros que esse utilizador consegue ler, aceder a tudo o que o VPS consegue alcançar na sua rede privada e deixar algo em ~/.bashrc ou numa unidade systemd do utilizador que seja executada durante o job seguinte.
Registar com --ephemeral faz com que o runner aceite um job e depois seja removido do registo, impedindo que um job leia o workspace do job seguinte. Isto só ajuda se algum processo reconstruir a máquina ou o container para cada job, porque um backdoor escrito no diretório pessoal do utilizador do runner permanece depois de um novo registo.
As regras seguintes são curtas. Use self-hosted runners em repositórios privados. Se tiver de associar um a um repositório público, não execute pull requests de forks nesse runner, não mantenha mais nada nesse servidor e trate a máquina como descartável.
Jobs Docker e o grupo que equivale a root
Jobs de contentores, contentores de serviços e qualquer etapa do workflow que invoque docker build precisam de um daemon Docker no host do runner. Instale o Docker da forma habitual, conforme descrito em Docker e Docker Compose numa VPS, e depois adicione o utilizador do runner ao grupo docker.
Compreenda o compromisso antes de fazer isso. A associação ao grupo docker equivale a acesso root, porque um contentor pode montar / como bind mount e ser executado como root dentro dele. Assim, um workflow que consiga comunicar com o socket do Docker pode ler e escrever em todos os ficheiros da VPS, incluindo /etc/shadow. Num repositório privado com contribuidores de confiança, este pode ser um custo aceitável. Noutros contextos, isso elimina a finalidade de utilizar um utilizador sem privilégios. O Docker rootless mantém as compilações dos contentores dentro das permissões do próprio utilizador do runner, mas implica um driver de armazenamento mais lento e não permite contentores privilegiados.
Atualizações e remoção limpa do runner
Um self-hosted runner atualiza-se por predefinição. Deteta uma nova release, substitui os próprios ficheiros e reinicia o serviço. Normalmente, não precisa de fazer nada. Para desativar a atualização automática, passe --disableupdate a ./config.sh quando registar o runner. Num runner já registado, isso significa registá-lo novamente com essa flag. Isto não permite permanecer indefinidamente numa única versão. A documentação do GitHub é explícita: um runner configurado com --disableupdate continua a ter de ser atualizado manualmente no prazo de 30 dias após uma nova release. Caso contrário, o GitHub deixa de colocar jobs em fila para esse runner.
Uma atualização manual mantém o registo, porque .runner e .credentials não estão no tarball. Pare o serviço, descarregue o novo tarball e calcule o checksum como gharunner, extraia-o sobre o mesmo diretório com tar xzf e inicie novamente o serviço:
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh startPara remover o runner, desinstale primeiro o serviço e, em seguida, remova o registo. O token de remoção está na mesma página Runners, no botão Remove do próprio runner.
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh uninstall
sudo -iu gharunner
cd ~/actions-runner
./config.sh remove --token PASTE_REMOVAL_TOKEN_HEREEliminar o diretório sem remover o registo deixa o runner listado como Offline no repositório. Ele permanece nessa lista até um administrador clicar em Force remove this runner ou até o GitHub o remover automaticamente após 14 dias sem ligação.
Modos de falha e mensagens apresentadas
Must not run with sudo. config.sh apresenta esta mensagem e termina quando é executado como root. A verificação é intencional, porque ficheiros pertencentes a root em _work fazem falhar todos os trabalhos posteriores executados com a conta de serviço. Execute ./config.sh como gharunner. A variável RUNNER_ALLOW_RUNASROOT ignora a verificação, mas apenas adia a falha.
sudo: ./svc.sh: command not found. Está no diretório correto. svc.sh ainda não existe, porque config.sh ainda não concluiu um registo. Registe o runner e instale o serviço.
Http response code: NotFound from 'POST https://api.github.com/actions/runner-registration'. Normalmente, o token não é um token de registo válido. Pode ter expirado, porque dura apenas uma hora, ou pode ter sido colado um personal access token no lugar do token de registo da página Runners. Gere um token novo e cole-o novamente. Confirme também que --url contém o URL completo do repositório.
Dependencies is missing for Dotnet Core 6.0. Execute sudo ./bin/installdependencies.sh a partir do diretório do runner como root e faça o registo novamente.
Runner Offline depois de um reboot. Execute systemctl is-enabled 'actions.runner.*'. Se não for apresentada nenhuma entrada, ./svc.sh install nunca foi executado. Nesse caso, o runner só existiu dentro da sua sessão de terminal. Se a unidade estiver ativa e o runner continuar Offline, consulte journalctl -u 'actions.runner.*' e verifique o HTTPS de saída.
O disco fica cheio. Os checkouts, as caches de compilação e as imagens Docker acumulam-se em _work e no diretório home do utilizador do runner. Nada os remove automaticamente. Monitorize du -sh /home/gharunner/actions-runner/_work e adicione uma limpeza agendada antes de o disco ficar cheio.
FAQ
Por que sudo ./svc.sh install informa que o comando não foi encontrado?
Porque svc.sh não está no tarball do runner. Ele é gerado no diretório do runner quando ./config.sh termina o registo, usando o nome do repositório e do runner para criar o nome do serviço. Execute primeiro ./config.sh como o utilizador do runner. Depois disso, sudo ./svc.sh install gharunner encontra o script e grava uma unidade chamada actions.runner.OWNER-REPO.RUNNER-NAME.service em /etc/systemd/system.
Preciso de abrir uma porta na firewall para um runner self-hosted?
Não. O runner abre uma ligação HTTPS de saída para o GitHub e mantém-na aberta enquanto aguarda jobs. Assim, o GitHub nunca inicia uma ligação para o seu VPS. Permita a saída na porta 443 e mantenha fechadas as regras de entrada. Se o runner aparecer como Offline enquanto o serviço está em execução, verifique a filtragem de saída e o DNS, e não as regras de entrada.
Posso usar um runner self-hosted num repositório público?
Pode, mas o GitHub desaconselha essa configuração. Um pull request de um fork inclui o seu próprio ficheiro de workflow. Assim, qualquer pessoa que possa criar um fork do seu repositório pode propor comandos para executar na sua máquina. O pedido de aprovação predefinido abrange apenas contribuidores que nunca tiveram um commit ou pull request integrado no repositório. Se associar um runner a um repositório público, desative os workflows de pull request de forks nesse runner, não mantenha mais nada nesse servidor e reinstale a máquina regularmente.
Por que o registo falha com Http response code: NotFound?
A chamada de registo responde NotFound quando a credencial está errada, e não apenas quando o URL está errado. Isso torna a mensagem enganadora. Os tokens de registo expiram uma hora depois de serem apresentados, e um personal access token não é aceite nesta chamada. Abra novamente Settings, Actions, Runners, New self-hosted runner, copie o token atualizado e confirme que o valor --url aponta para um repositório no qual tem direitos de administrador.