← All heartbeat examples · What Heartbeat is
Heartbeat from a Docker container
A ping that leaves a container proves three things at once: the host is up, the Docker daemon started the
container, and the container can reach the internet. Where you put the ping decides what else it proves.
Three forms, from the simplest to the most telling. Every one of them needs only the slot's ping URL from
Settings → Heartbeats, kept in a variable called
HEARTBEAT_URL.
ENV line: an image pushed to a registry carries it
to everyone who can pull. Pass it at run time from a .env
file or an --env-file, and list that file in
.gitignore and
.dockerignore.
1. "The stack is up": a Compose sidecar
A throwaway container that pings once a minute for as long as the Compose project runs. If the host,
the daemon or the project goes down, so does the ping. Compose reads
.env next to the file on its own, and the
doubled $$ stops Compose from expanding the
variable before the shell inside the container sees it.
# .env (chmod 600, never committed)
HEARTBEAT_URL=https://freshjots.com/hb/<token>
# compose.yaml
services:
app:
image: ghcr.io/you/app:latest
heartbeat:
image: curlimages/curl:latest
restart: unless-stopped
environment:
HEARTBEAT_URL: ${HEARTBEAT_URL}
command: >
sh -c 'while true; do curl -fsS --max-time 10 "$$HEARTBEAT_URL" >/dev/null; sleep 60; done'
What it does not prove: the app container can be
crash-looping while the sidecar pings happily. When you need the ping to mean "the app is healthy", move it
into the healthcheck.
2. "The app is healthy": ping from the HEALTHCHECK
Docker already runs a health command inside the container on an interval. Put the ping after the local
check, so it fires only when the app answered, and end it with
|| true so a network blip on the ping can never
mark the container unhealthy. The healthcheck's own interval does the scheduling: 30 seconds is twice a
minute, well inside the limits. The command runs with the container's environment, so
HEARTBEAT_URL only has to be passed at run time.
# Dockerfile (localhost:3000/up is your app's own health URL and port)
HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --retries=3 \
CMD curl -fsS http://localhost:3000/up || exit 1; \
curl -fsS --max-time 5 "$HEARTBEAT_URL" >/dev/null 2>&1 || true
The same thing without rebuilding the image, from Compose:
services:
app:
image: ghcr.io/you/app:latest
env_file: .env
healthcheck:
test:
- CMD-SHELL
- curl -fsS http://localhost:3000/up || exit 1; curl -fsS --max-time 5 "$$HEARTBEAT_URL" >/dev/null 2>&1 || true
interval: 30s
timeout: 10s
start_period: 20s
This closes a gap Docker leaves open: a plain docker run
never restarts an unhealthy container, it only marks it, and nobody is watching the mark. The missing ping
turns the mark into an email. Images without curl
(Alpine, BusyBox) have wget -qO- -T 5 "$HEARTBEAT_URL"
instead, and a distroless image can ping from
inside the application.
3. "The host is alive": one docker run
No Compose file, no cron: a single detached container with a restart policy pings for as long as the
daemon runs, and comes back after a reboot on its own. The URL comes from a root-only env file so it never
lands in shell history or ps output.
docker run -d --name freshjots-heartbeat --restart unless-stopped \
--env-file /etc/heartbeat.env \
curlimages/curl:latest \
sh -c 'while true; do curl -fsS --max-time 10 "$HEARTBEAT_URL" >/dev/null; sleep 60; done'
A reboot that takes under two minutes writes nothing into the record; a longer one is an
OFF / ON
pair with the minutes between them.
4. A worker container
The three forms above prove the container. A worker that is connected but not consuming (a stuck job,
a dropped queue connection, a loop that exited while the process lingered) looks fine to all three. For a
worker the ping belongs inside the loop, after each unit of work: see
background workers and job queues.
Kubernetes has its own form, a one-minute CronJob that
proves the scheduler and egress, on the
main examples page.
5. Gotchas
- All containers on a host share its IP. The limits are 120 hits a
minute per token and 60 a minute per IP. Once a minute per container is nothing; ten containers each
pinging every second is a
429, andcurl -fwill tell you so in the container's logs. - One slot per container. Two containers on one slot hide each other's death. Give each its own URL and put the service name in the slot's label.
docker compose configprints the resolved URL. Useful to check the variable arrived, dangerous in a pasted log or a screenshot. If it leaks, Rotate URL on the slot page kills the old one at once.- A 404 from the ping means the slot is switched off, the URL was rotated, or the token was mangled on the way in (a stray quote in the env file is the usual cause). Nothing is recorded on the Fresh Jots side.
- Staging gets its own slot. A staging stack pinging the production slot masks a dead production stack.