← Blog

Hospede o Healthchecks.io para monitorar cron jobs

Railyard

19 de julho de 2026 · 11 min de leitura

Siga isso no seu próprio servidor. Comece grátis, sem cartão

Um cron job que para de rodar em silêncio não tem como te avisar que parou. O script simplesmente não executa numa noite, nada quebra, nada loga um erro, e três semanas depois você descobre, do jeito mais difícil, que os backups não rodam mais desde que o kernel do servidor foi atualizado e o daemon do cron nunca voltou. O Healthchecks.io é um dead man’s switch feito exatamente para isso: cada um dos seus jobs agendados dá um ping numa URL única quando termina, e se um ping não aparecer dentro do intervalo que você disse ao Healthchecks para esperar, ele te avisa por e-mail, Slack, webhook ou qualquer uma das outras integrações que suporta. Este guia monta uma instância self-hosted no seu próprio servidor com Postgres, HTTPS via Caddy e e-mail de alerta de verdade, e depois mostra o caminho de um clique se você preferir pular a configuração manual.

O que você vai precisar

  • Um servidor. Um Hetzner CX23 (2 vCPUs, 4 GB de memória, 40 GB NVMe) por €5,99/mês sem VAT é suficiente para o Healthchecks, seu próprio Postgres e um proxy reverso rodando juntos numa única máquina, para um uso pessoal ou a carga de cron jobs de uma equipe pequena.
  • Um domínio (ou um subdomínio de um que você já tenha), com acesso às configurações de DNS dele.
  • Docker e o plugin Docker Compose instalados no servidor (coberto na Etapa 2).
  • Uma conta ou relay de SMTP pelo qual você possa enviar e-mail, já que os e-mails de alerta são todo o sentido de rodar isso.
  • Um terminal e acesso SSH ao servidor.

Etapa 1: Crie o servidor e aponte um domínio para ele

Suba um servidor Ubuntu 24.04 no plano CX23 da Hetzner (2 vCPUs, 4 GB de memória, 40 GB NVMe, €5,99/mês sem VAT). Anote o IP público dele, depois adicione sua chave SSH durante a criação ou logo depois, para não depender de senha.

Nas configurações de DNS do seu domínio, adicione um registro A apontando um subdomínio, por exemplo checks.yourdomain.com, para esse IP:

Type: A
Name: checks
Value: <IP do seu servidor>
TTL: 300

Confirme que está resolvendo antes de continuar:

dig checks.yourdomain.com +short

Isso deve imprimir o IP do seu servidor, não um antigo e não nada. Enquanto está nisso, restrinja o firewall só ao que essa stack precisa:

ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

Etapa 2: Instale o Docker

Conecte via SSH ao servidor e instale o Docker Engine mais o plugin Compose:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER

Saia e entre de novo para a mudança de grupo ter efeito, depois confirme que as duas peças estão presentes:

docker --version
docker compose version

Etapa 3: Organize o projeto e seus segredos

Crie um diretório para a stack:

mkdir -p ~/healthchecks && cd ~/healthchecks

Gere uma chave secreta aleatória e uma senha de banco de dados. O Healthchecks usa SECRET_KEY para assinar sessões, então um valor fraco ou reaproveitado é um problema real, não uma formalidade:

openssl rand -base64 32
openssl rand -base64 24

Crie o .env com as próprias chaves de configuração do projeto, preenchidas para o seu domínio:

# .env
DOMAIN=checks.yourdomain.com
DB=postgres
DB_HOST=db
DB_PORT=5432
DB_NAME=healthchecks
DB_USER=postgres
DB_PASSWORD=<a senha que você gerou>
ADMINS=you@yourdomain.com
DEFAULT_FROM_EMAIL=healthchecks@yourdomain.com
SITE_ROOT=https://checks.yourdomain.com
SITE_NAME=Healthchecks
PING_ENDPOINT=https://checks.yourdomain.com/ping/
REGISTRATION_OPEN=False
SECRET_KEY=<a chave que você gerou>
GUNICORN_WORKERS=4
GUNICORN_PORT=8000

Algumas dessas configurações importam mais do que parecem:

  • SITE_ROOT é a URL HTTPS completa da sua instância, não só o domínio puro. O Healthchecks usa isso para construir todo link que mostra para você ou te envia por e-mail, incluindo as próprias URLs de ping.
  • PING_ENDPOINT deve corresponder a SITE_ROOT com /ping/ no final. É uma configuração separada porque algumas instalações servem o tráfego de ping de um caminho diferente, mais leve, do que a UI principal; nesta configuração de container único, os dois compartilham o mesmo domínio.
  • REGISTRATION_OPEN=False fecha o cadastro público na sua instância. O padrão é aberto, que é a única configuração neste arquivo que você não quer esquecer, já que uma página de cadastro aberta num domínio público é um convite para estranhos criarem contas no seu servidor.
  • DOMAIN é uma segunda forma, com o domínio puro, do mesmo valor, usada pelo Caddyfile na Etapa 5. O próprio Healthchecks nunca lê DOMAIN; ele lê SITE_ROOT.

Você também vai precisar das configurações de SMTP para o Healthchecks de fato conseguir te enviar e-mails de alerta, já que uma ferramenta de monitoramento que não consegue te avisar por e-mail quando um job fica quieto não está cumprindo o próprio trabalho. Adicione EMAIL_HOST, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD, EMAIL_PORT e EMAIL_USE_TLS no mesmo arquivo .env, apontados para qualquer provedor ou relay de SMTP que você já use. A própria documentação de configuração self-hosted do projeto cobre em detalhe cada variável relacionada a e-mail, se o seu provedor precisar de algo além do básico.

Defina as permissões para que só o seu usuário possa ler o arquivo:

chmod 600 .env

Etapa 4: Escreva o arquivo Docker Compose

O próprio docker/docker-compose.yml do Healthchecks, do repositório do projeto no GitHub, é escrito para construir a imagem localmente a partir de docker/Dockerfile e publicar a porta 8000 direto no host, o que é a forma certa de rodar na sua própria máquina, mas não para um servidor exposto à internet. Esta configuração adapta isso de três formas: usa a imagem pré-construída healthchecks/healthchecks em vez de construir a partir do código-fonte, fixa uma tag de versão exata em vez de latest para que um restart sem supervisão não te mude silenciosamente para uma nova versão, e não publica a porta 8000 no host de forma alguma, já que o Caddy fica na frente.

Crie docker-compose.yml:

services:
  db:
    image: postgres:17-alpine
    restart: unless-stopped
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=${DB_NAME}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 5s
      timeout: 5s
      retries: 10
    networks:
      - healthchecks-net

  web:
    image: healthchecks/healthchecks:v5.0.1
    restart: unless-stopped
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy
    networks:
      - healthchecks-net

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    environment:
      - DOMAIN=${DOMAIN}
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - web
    networks:
      - healthchecks-net

networks:
  healthchecks-net:

volumes:
  db-data:
  caddy_data:
  caddy_config:

postgres:17-alpine substitui o postgres:16 simples que o próprio exemplo do projeto usa; os dois funcionam bem, 17 é só a versão mais atual no momento em que isso foi escrito. POSTGRES_USER é deixado sem definir de propósito, o que faz o Postgres usar o padrão postgres como usuário, batendo com DB_USER=postgres no .env sem repetir o valor. Só o Caddy publica portas no host; db e web só são acessíveis pela rede interna do Docker.

Etapa 5: Escreva o Caddyfile e inicie tudo

Crie o Caddyfile no mesmo diretório:

{$DOMAIN} {
	reverse_proxy web:8000
}

Inicie a stack:

docker compose up -d

Observe o Caddy obter um certificado:

docker compose logs -f caddy

Você deve ver uma linha sobre um certificado obtido para o seu domínio, e então https://checks.yourdomain.com deve carregar com um cadeado válido e sem avisos.

Etapa 6: Configuração inicial e fechamento do cadastro

Visitar o seu domínio pela primeira vez mostra a própria página de cadastro do Healthchecks. Não existe uma etapa separada via CLI para criar uma conta de admin aqui; a primeira conta que você registrar é a conta que você vai usar para gerenciar checks, projetos e integrações a partir daí. Registre agora, faça login e crie seu primeiro check para confirmar que os pings chegam até ele.

Depois que essa conta existir, confirme que REGISTRATION_OPEN=False está definido no .env (já está, desde a Etapa 3) e refaça o deploy do serviço web para que a configuração tenha efeito, caso você tenha mudado algo:

docker compose up -d web

A partir daqui, visitar a página de cadastro na sua instância mostra uma mensagem de cadastro fechado em vez de um formulário funcionando, então mais ninguém pode criar uma conta no seu servidor.

Etapa 7: Faça backup do banco de dados e da chave secreta

O banco de dados guarda todo projeto, check, integração e histórico de ping que você tem. Um dump diário do Postgres, mantido fora do servidor:

docker compose exec -T db pg_dump -U postgres healthchecks > "healthchecks-backup-$(date +%F).sql"

Coloque isso num cron job (ironicamente, um que você poderia fazer o próprio Healthchecks monitorar) e copie o resultado para algum lugar fora deste servidor, já que um backup que só vive no servidor que ele está protegendo não sobrevive ao tipo de falha que de fato destrói um servidor.

Faça backup do .env também, guardado em algum lugar separado e com acesso controlado. O SECRET_KEY dele assina toda sessão que o Healthchecks emite; perder ele só significa que todo mundo é deslogado e precisa entrar de novo, o que é um incômodo pequeno, mas uma cópia vazada dele é um jeito de alguém forjar uma sessão, então trate com o mesmo cuidado que a senha do banco de dados.

Etapa 8: Faça upgrade com segurança

Confira as release notes no repositório do projeto no GitHub entre a tag que você está usando e a que você vai migrar, depois mude a versão em docker-compose.yml:

image: healthchecks/healthchecks:<new-version>

Baixe a imagem e reinicie só esse serviço:

docker compose pull web
docker compose up -d web

Confira os logs para ver uma inicialização limpa e uma etapa de migração bem-sucedida:

docker compose logs web

A imagem pré-construída roda automaticamente qualquer migração de banco de dados pendente como parte da própria inicialização, então um conjunto de logs limpo sem erro de migração é o que você está procurando; se algo falhar, o backup do Postgres da Etapa 7 é o caminho de volta para a versão anterior.

O caminho curto: faça com o Railyard

Tudo isso acima, uma instância do Healthchecks com Postgres, chave secreta gerada e HTTPS já ligados, também é um template de um clique:

https://app.railyard.run/deploy?template=healthchecks

O Railyard constrói direto do docker/Dockerfile no próprio repositório do projeto, provisiona um banco de dados Postgres gerenciado automaticamente e roda o próprio comando de migração do projeto como uma etapa de release antes de cada deploy, o mesmo python manage.py migrate que você rodaria à mão de outra forma. Ele também define ALLOWED_HOSTS=* e gera uma SECRET_KEY de verdade para você, então não há segredo nenhum para você inventar e colar.

Se você preferir começar pelo Railyard de forma geral antes de decidir pelo Healthchecks especificamente, o quickstart cobre o básico, deploy button explica o formato do link ?template= se você quiser incorporar um em outro lugar, e databases and backups cobre como os backups funcionam entre templates em geral, não só este. O plano Free do Railyard cobre um servidor mais um servidor de banco de dados e três apps sem exigir cartão, o Pro custa US$19/mês por equipe, e o Team, US$49/mês por equipe.

Solução de problemas

Os e-mails de alerta nunca chegam. Isso quase sempre é configuração de SMTP faltando ou errada, não um bug do Healthchecks. Confira se EMAIL_HOST, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD, EMAIL_PORT e EMAIL_USE_TLS estão todos definidos no .env e se a conta que eles descrevem consegue de fato enviar e-mail; muitos provedores bloqueiam o primeiro envio de um servidor ou IP novo até você verificar. Uma ferramenta de monitoramento com o próprio caminho de alerta quebrado vai falhar em silêncio exatamente na única tarefa para a qual ela existe, então envie para você mesmo uma falha de teste e confirme que o e-mail realmente chega antes de confiar nisso para algo real.

As URLs de ping mostradas na UI apontam para o domínio errado. SITE_ROOT não corresponde ao endereço HTTPS real de onde você está servindo o app. Corrija no .env e refaça o deploy do web; toda URL de ping que o Healthchecks já te mostrou ou colocou num e-mail foi construída a partir do valor antigo e precisa ser copiada de novo na UI depois do ajuste.

Bad Request (400) ao carregar o site. ALLOWED_HOSTS não inclui o domínio que você está visitando. A configuração do compose aqui define como * pelo próprio tratamento padrão do projeto, então isso costuma aparecer só se você mudou esse comportamento; confira se corresponde ao seu domínio real, se for o caso.

Conexão recusada no Postgres, ou o container web fica reiniciando. Confira docker compose logs db primeiro para ver o erro real. Uma senha que não bate entre DB_PASSWORD no .env e o que o Postgres foi inicializado com é a causa mais comum, geralmente por editar o .env depois que o volume db-data já existe; o Postgres só lê POSTGRES_PASSWORD na primeiríssima inicialização com um diretório de dados vazio, então uma mudança posterior precisa de um volume novo ou de uma troca de senha dentro do próprio banco para corresponder.

O HTTPS nunca é emitido, ou o navegador mostra um aviso de certificado. Rode dig checks.yourdomain.com +short e confirme que retorna o IP deste servidor. O Let’s Encrypt também precisa que a porta 80 esteja acessível pela internet para a etapa de validação dele, então confira se ufw status mostra as portas 80 e 443 abertas, não só a 443.

Perguntas frequentes

Eu preciso de Postgres, ou SQLite funciona? O Healthchecks suporta SQLite para instalações pequenas e de baixo tráfego, mas o Postgres é a escolha melhor no momento em que mais de uma ou duas pessoas dependem dessa instância continuar no ar, já que ele lida melhor com escritas simultâneas e históricos de ping maiores. Este guia usa Postgres desde o início por esse motivo.

Como eu de fato aponto um cron job para isso depois que está no ar? Adicione uma chamada curl no final do próprio job, depois do comando que você está monitorando ter sucesso:

your-backup-script.sh && curl -fsS -m 10 --retry 5 https://checks.yourdomain.com/ping/<uuid>

Cada check na UI te dá sua própria URL de ping única para copiar no job. Se o script falhar antes de chegar nessa linha, nenhum ping chega, e é exatamente esse o sinal que o Healthchecks está esperando.

Qual é a diferença real entre isso e o healthchecks.io hospedado? Na prática, muito pouca; é a mesma aplicação open-source nos dois casos. A versão hospedada é mantida para você e cobrada por mês depois que você passa do plano gratuito dela; uma instância self-hosted custa só o servidor em que roda, sem limite no número de checks, mas você é quem cuida de atualizar e fazer backup.

O REGISTRATION_OPEN realmente precisa ficar fechado? Sim, depois que a sua própria conta existir. Não há motivo para uma instância de monitoramento self-hosted no seu próprio domínio aceitar cadastros de qualquer um que encontre a URL por acaso, e deixar aberto é a única configuração neste guia inteiro que é de fato um erro pular.

Quanto custa rodar isso? O servidor custa €5,99/mês sem VAT. O Healthchecks em si é software livre e gratuito, sem cobrança por check ou por integração; você está pagando pela máquina em que ele roda, não pelo que roda nela.

Um cron job que falha fazendo barulho é fácil de notar. O que falha em silêncio, o backup que parou três semanas atrás, a renovação de certificado que parou de disparar sem ninguém perceber, é exatamente para isso que essa ferramenta serve, e agora você tem sua própria instância de olho justamente nisso.

Por que escolhem o Railyard para isso

  • Sua própria conta de cloud. Faça deploy no DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, ou em qualquer servidor Linux via SSH, cobrado direto pelo seu provedor, sem markup.
  • Um preço fixo por equipe. O Free é grátis para sempre: um servidor mais um servidor de banco de dados, três apps, sem cartão. O Pro custa US$19 por mês para até cinco servidores e apps ilimitados. O Team custa US$49 por mês com servidores ilimitados. Sem cobrança por app, por deploy, por assento ou por gigabyte.
  • Templates de um clique. Faça deploy de apps open source, como bancos de dados, analytics e ferramentas de dev, no seu próprio servidor em um clique.
  • Bancos de dados gerenciados. Postgres, MySQL e Redis com backups programados, verificados por restauração, e recuperação point-in-time.
  • Push-to-deploy. Faça deploy a partir do GitHub, GitLab ou da CLI, com HTTPS automático e releases sem downtime.

Pule a configuração do servidor

Suba o template verificado do Healthchecks no Railyard com um clique e já tenha uma instância rodando com Postgres e HTTPS prontos.

Plano grátis, sem cartão, e 14 dias de Pro para cada equipe nova. Um preço fixo por equipe.