Skip to main content

Cron Job Setup for DigitalOcean Droplet

Last updated: February 26, 2026

Configures host-level crontab to run /api/cron/check-deviations every 30 seconds, checking for route deviations and low fuel alerts.


Overview

The cron job checks all active routes every 30 seconds and:

  • Detects route deviations (vehicles off-route)
  • Monitors fuel levels (alerts when <25%)
  • Auto-completes routes when vehicles reach destination
  • Creates/updates/resolves both fuel and deviation alerts
  • Sends Telegram notifications if chat ID configured

The single endpoint /api/cron/check-deviations handles both fuel and deviation logic — there should never be a separate check-fuel-levels endpoint.


Current Production Configuration

Host-Level Crontab

SSH into the droplet and run crontab -e as root. The production crontab has two entries to achieve 30-second intervals:

* * * * * CID=$(docker ps --filter label=coolify.name=wgg0oso8w00sgoco048w8k0s --format "{{.ID}}" | head -1) && [ -n "$CID" ] && IP=$(docker inspect $CID --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}") && PORT=$(docker exec $CID sh -c "echo \$PORT" 2>/dev/null || echo "80") && curl -s "http://$IP:$PORT/api/cron/check-deviations?secret=c549994f99f672ab4cf949f030a242c5" >> /var/log/cron-deviations.log 2>&1
* * * * * sleep 30 && CID=$(docker ps --filter label=coolify.name=wgg0oso8w00sgoco048w8k0s --format "{{.ID}}" | head -1) && [ -n "$CID" ] && IP=$(docker inspect $CID --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}") && PORT=$(docker exec $CID sh -c "echo \$PORT" 2>/dev/null || echo "80") && curl -s "http://$IP:$PORT/api/cron/check-deviations?secret=c549994f99f672ab4cf949f030a242c5" >> /var/log/cron-deviations.log 2>&1
0 3 * * 0 docker builder prune -af >> /var/log/docker-cleanup.log 2>&1

Design explanation:

  • Dynamic container resolution: Uses Coolify label coolify.name=wgg0oso8w00sgoco048w8k0s to resolve container ID — survives all redeploys without changes
  • Dynamic IP: Extracts container's internal Docker network IP via docker inspect (not localhost — routes through Docker bridge)
  • Dynamic PORT: Reads $PORT environment variable from inside the container via docker exec sh -c "echo \$PORT", falls back to 80 if unset
  • 30-second interval: Two cron entries: first runs at :00 of every minute, second runs at :30 (via sleep 30)
  • Log location: /var/log/cron-deviations.log — checked by monitoring systems

Why Dynamic Port is Critical

Incident: Coolify changed the container's PORT from 3000 → 80 on a deployment. The old hardcoded crontab called http://$IP:3000/... which failed silently with "Connection refused". All route deviations and fuel alerts stopped firing for ~2 hours until the crontab was fixed.

Solution: The new crontab reads $PORT from the container itself, ensuring it always targets the correct port regardless of Coolify's configuration.


Configuration Variables

VariableValueNotes
Container labelwgg0oso8w00sgoco048w8k0sFound in Coolify service settings; used to identify container by name rather than ID
Cron secretc549994f99f672ab4cf949f030a242c5Must match CRON_SECRET in app env vars; treat as sensitive
Log file/var/log/cron-deviations.logMonitored by Uptime Kuma; check for recent activity to diagnose failures
Interval30 secondsStaggered via two cron entries: :00 and :30
Telegram chat IDSet via Settings UIRequired for Telegram alerts; not an env var — check app_settings_v2.telegram_chat_id in database

Setup Instructions (Fresh Deployment)

1. SSH into your Droplet

ssh root@your-droplet-ip

2. Find the Coolify Service Label

Open coolify.getitinera.com, navigate to the Itinera service, and note the container name/label (format: alphanumeric string like wgg0oso8w00sgoco048w8k0s).

3. Add the cron entries

crontab -e

Paste the two cron entries above (replacing the container label with your actual one, and the secret with your actual CRON_SECRET).

4. Verify

crontab -l

Both lines should appear in the output.


Log Monitoring

View live logs

tail -f /var/log/cron-deviations.log

Check if cron is firing

ls -la /var/log/cron-deviations.log

Compare the modification timestamp to current time (date). If modification time is more than 60 seconds old, the cron is not running.

Typical log entry (success)

{"success":true,"activeRoutes":6,"vehiclesFound":101,"processed":6,"deviationsCreated":2,"deviationsUpdated":0,"deviationsEnded":0,"fuelAlertsCreated":1,"fuelAlertsResolved":0,"polylinesCached":0,"errors":[],"duration":243}

Telegram Setup

For Telegram alerts to fire, the Telegram chat ID must be configured in the Settings UI (/settings). Navigate to the Telegram section and paste your bot's chat ID. The app stores this in app_settings_v2.telegram_chat_id, not as an environment variable.

Without chat ID: Cron runs successfully but Telegram alerts are silently skipped (no errors).


Testing

Manual test (via droplet)

CID=$(docker ps --filter label=coolify.name=wgg0oso8w00sgoco048w8k0s --format "{{.ID}}" | head -1) && IP=$(docker inspect $CID --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}") && PORT=$(docker exec $CID sh -c "echo \$PORT" 2>/dev/null || echo "80") && curl -v "http://$IP:$PORT/api/cron/check-deviations?secret=c549994f99f672ab4cf949f030a242c5"

Expected response

{
"success": true,
"activeRoutes": 6,
"vehiclesFound": 101,
"processed": 6,
"deviationsCreated": 0,
"deviationsUpdated": 1,
"deviationsEnded": 0,
"fuelAlertsCreated": 0,
"fuelAlertsResolved": 1,
"polylinesCached": 0,
"errors": [],
"duration": 245
}

Troubleshooting

Cron stopped firing (log hasn't updated in >60s)

Check 1: Is the cron service running?

service cron status
service cron start

Check 2: Does the container exist?

docker ps --filter label=coolify.name=wgg0oso8w00sgoco048w8k0s

If no output, the Coolify service label has changed. Open Coolify UI and find the new label.

Check 3: Can you reach the container?

CID=$(docker ps --filter label=coolify.name=wgg0oso8w00sgoco048w8k0s --format "{{.ID}}" | head -1) && docker inspect $CID --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}"

Should return an IP like 172.19.0.2. If empty, Docker networking is broken.

Check 4: Can you manually invoke the endpoint?

CID=$(docker ps --filter label=coolify.name=wgg0oso8w00sgoco048w8k0s --format "{{.ID}}" | head -1) && IP=$(docker inspect $CID --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}") && PORT=$(docker exec $CID sh -c "echo \$PORT" 2>/dev/null || echo "80") && curl -v "http://$IP:$PORT/api/cron/check-deviations?secret=c549994f99f672ab4cf949f030a242c5"

This tests the entire chain: container resolution, IP extraction, port detection, and endpoint reachability.

Cron is firing but no Telegram alerts

Check 1: Is telegram_chat_id set?

# Via psql on the droplet:
docker exec itinera-db psql -U itinera -d itinera -c "SELECT telegram_chat_id FROM app_settings_v2 LIMIT 1;"

If the result is NULL or empty, set it via the Settings UI.

Check 2: Is the Telegram bot token correct?

curl "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getMe"

Replace <YOUR_BOT_TOKEN> with the actual token. Should return {"ok":true,"result":{...}}.

Check 3: Check app logs for Telegram errors

pm2 logs itinera | grep -i telegram

(Or if using Coolify: docker logs <container-id> | grep -i telegram)

"Connection refused" or port mismatch

If you see errors like Failed to connect to ... port 3000 in the cron logs, Coolify likely changed the container's PORT. The fix:

  1. Open Coolify service settings for Itinera
  2. Check the current PORT environment variable
  3. The crontab should auto-detect it via docker exec $CID sh -c "echo \$PORT" — if detection fails, it falls back to port 80
  4. If still failing, manually test: docker exec $CID sh -c "echo \$PORT" to verify the env var exists

Important Notes

  1. Never add a check-fuel-levels cron entry — this endpoint doesn't exist. All fuel logic is in check-deviations.
  2. Samsara API key — Must be set via Settings UI (app_settings_v2.samsaraApiKey), not as environment variable.
  3. Container label changes with Coolify updates — If the label changes, update both cron entries. Check Coolify service settings to find the new label.
  4. Log rotation — Consider adding logrotate rules if logs grow large; currently no auto-rotation is configured.

  • Roadmap — Phase 1 operational stability task checklist
  • Architecture — API endpoint structure and cron systems