.NET no Raspberry Pi: publicar para ARM e manter o app de pé como serviço
Descubra a arquitetura do Pi, publique com o RID certo, escolha entre runtime instalado ou app autocontido e rode como serviço systemd que volta depois do reboot.
Você publica a API do sensor no notebook, copia a pasta para o Raspberry Pi, roda ./SensorApi e recebe cannot execute binary file: Exec format error. Não é permissão, não é cartão corrompido. É arquitetura. A resposta curta: descubra se o sistema do Pi é ARM de 64 ou 32 bits, publique com o RID correspondente (linux-arm64 ou linux-arm), copie a pasta inteira e rode o app como serviço do systemd, numa porta alta, com log que não enche o cartão.
O executável gerado no PC foi compilado para x64, a língua do processador do notebook. O Pi fala ARM. O código C# é o mesmo, mas o arquivo que inicia o processo e o runtime por baixo dele precisam ser da arquitetura do aparelho.
Por que o binário do PC não roda no Pi
Um app .NET publicado tem duas partes. Os arquivos .dll com o seu código em IL são portáveis. Mas o executável que você chama na linha de comando (o apphost) e o runtime que interpreta e compila esse IL são nativos, feitos para uma arquitetura e um sistema operacional específicos. Esse par é o que o .NET chama de RID, o identificador de runtime.
Quando você publica sem dizer o RID, ou com o RID do PC, o apphost é x64. No Pi, o kernel olha o cabeçalho do arquivo, não reconhece a arquitetura e devolve o "Exec format error". Copiar o runtime x64 também não adianta, pelo mesmo motivo.
Há ainda uma restrição de hardware: o .NET exige ARMv7 ou superior. Raspberry Pi 1 e Pi Zero da primeira geração são ARMv6 e não rodam .NET moderno. Pi 3, Pi 4, Pi 5 e Pi Zero 2 W rodam.
Descobrir a arquitetura certa no aparelho
Rode dois comandos no Pi. O primeiro mostra a arquitetura do kernel, o segundo a dos pacotes do sistema, que é a que importa para o seu binário.
$ uname -m
aarch64
$ dpkg --print-architecture
arm64
Se o segundo comando responde arm64, publique para linux-arm64. Se responde armhf, o sistema é 32 bits e o RID é linux-arm, mesmo que o uname -m diga aarch64. Isso acontece em instalações com kernel de 64 bits e sistema de 32 bits, e é a armadilha que faz alguém publicar para arm64 e tomar o mesmo erro. Em projeto novo, prefira o Raspberry Pi OS de 64 bits.
Publicar: runtime instalado ou app autocontido
Há dois caminhos, e os dois funcionam. O que não funciona é misturar: app dependente de framework publicado para Windows rodando num Linux ARM.
Autocontido: o runtime vai junto
dotnet publish -c Release -r linux-arm64 --self-contained true -o ./publish
A pasta publish leva o runtime inteiro. Fica maior, algumas dezenas de megabytes a mais, mas o Pi não precisa ter .NET instalado e a versão do runtime é a que você testou. Para um aparelho único em campo, é o caminho mais previsível.
Dependente de framework: o Pi tem o runtime
# no Pi, instala o runtime do ASP.NET Core numa pasta do sistema
curl -sSL https://dot.net/v1/dotnet-install.sh -o dotnet-install.sh
chmod +x dotnet-install.sh
sudo ./dotnet-install.sh --channel 10.0 --runtime aspnetcore --install-dir /opt/dotnet
# no PC
dotnet publish -c Release -r linux-arm64 --self-contained false -o ./publish
A publicação fica pequena e várias aplicações compartilham o mesmo runtime. Em troca, a versão maior do runtime instalado precisa atender a do projeto: um app feito para net10.0 não sobe num Pi que só tem o runtime 8, e a mensagem pede para instalar ou atualizar o .NET. Como o runtime ficou em /opt/dotnet, o processo precisa da variável DOTNET_ROOT apontando para lá, o que o serviço abaixo já faz.
Copiar e rodar como serviço
O caso de exemplo é a SensorApi, que lê um sensor de temperatura e expõe a leitura por HTTP para o painel da oficina. Ela precisa voltar sozinha depois de queda de energia, e não pode depender de um terminal aberto.
- Crie um usuário de sistema para o serviço, sem login:
sudo useradd --system --no-create-home sensor. - Copie a pasta publicada inteira:
scp -r ./publish/* pi@oficina-pi.local:/tmp/sensor-api/, depois mova para/opt/sensor-apie ajuste o dono comsudo chown -R sensor:sensor /opt/sensor-api. - Garanta a permissão de execução do apphost:
sudo chmod +x /opt/sensor-api/SensorApi. - Teste uma vez à mão:
sudo -u sensor /opt/sensor-api/SensorApi. - Crie a unidade do systemd e habilite.
# /etc/systemd/system/sensor-api.service
[Unit]
Description=API do sensor da oficina
After=network-online.target
Wants=network-online.target
[Service]
User=sensor
WorkingDirectory=/opt/sensor-api
ExecStart=/opt/sensor-api/SensorApi
Restart=always
RestartSec=5
Environment=ASPNETCORE_URLS=http://0.0.0.0:5000
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_ROOT=/opt/dotnet
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now sensor-api
systemctl status sensor-api
journalctl -u sensor-api -f
No app autocontido, a linha do DOTNET_ROOT é desnecessária, mas não atrapalha. Ao parar o serviço, o systemd manda SIGTERM, e o ASP.NET Core usa esse sinal para encerrar com calma; o porquê disso importar está em SIGTERM termina o pedido. Se quiser que o systemd saiba exatamente quando o app terminou de subir, o pacote Microsoft.Extensions.Hosting.Systemd integra o host com Type=notify.
Porta alta e proxy na frente
Portas abaixo de 1024 exigem privilégio no Linux. Rodar a API como root para escutar na 80 resolve o sintoma e cria um problema maior. Deixe a aplicação na 5000 e, se precisar da 80 ou da 443, coloque um Nginx ou Caddy na frente. O proxy cuida do TLS e a aplicação continua sem privilégio.
O cartão SD é o ponto fraco
Cartão SD aguenta mal escrita contínua e queda de energia no meio de uma gravação. Um app que registra cada leitura do sensor em log de texto, uma por segundo, enche e desgasta o cartão em meses. Três cuidados resolvem a maior parte:
- Mande o log para o journald (a saída padrão já vai para lá com systemd) e limite o tamanho em
/etc/systemd/journald.confcomSystemMaxUse=100M. - Reduza o nível de log em produção para
Warningnas categorias barulhentas, peloappsettings.Production.json. - Se o aparelho grava dados importantes, use SSD por USB ou envie os dados para fora com frequência. O cartão é o primeiro a falhar.
Se você prefere containers, dá para gerar a imagem para ARM no próprio PC com docker buildx build --platform linux/arm64. A regra é a mesma: a imagem precisa ser da arquitetura do aparelho. O começo desse caminho está em Docker no servidor: primeiro passo.
Erros comuns
- Publicar sem
-r: o apphost sai com a arquitetura do PC e o Pi responde "Exec format error". - Confiar só no
uname -m: com sistema de 32 bits sobre kernel de 64, você publica para arm64 e o binário não roda. - Copiar só o executável: as
.dlle o.runtimeconfig.jsonficam para trás e o app falha ao iniciar. - Runtime de versão maior diferente da do projeto: o app dependente de framework se recusa a subir.
- Rodar como root para usar a porta 80: qualquer falha na aplicação vira controle total do aparelho.
- App rodando num terminal SSH aberto: fecha a sessão ou falta luz, e a API não volta.
Checklist
dpkg --print-architecturefoi conferido no Pi.- O RID da publicação bate com a arquitetura do sistema.
- A pasta publicada foi copiada inteira e o apphost tem permissão de execução.
- O app roda com um usuário de sistema, não com root nem com o usuário
pi. - Existe uma unidade systemd com
Restart=alwayseenablefeito. - A aplicação escuta numa porta acima de 1024.
- O journald tem limite de tamanho configurado.
- Depois de
sudo reboot, a API respondeu sem ninguém tocar no aparelho.
Rode sudo reboot no seu Pi agora e espere dois minutos. A aplicação voltou sozinha, e o RID com que ela foi publicada é o que o dpkg --print-architecture mostra?
Referência: https://edi.wang/post/2019/9/29/setup-net-core-30-runtime-and-sdk-on-raspberry-pi-4