Cron Job Setup for DigitalOcean Droplet
Last updated: February 26, 2026
Configures host-level crontab to run
/api/cron/check-deviationsevery 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=wgg0oso8w00sgoco048w8k0sto 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
$PORTenvironment variable from inside the container viadocker exec sh -c "echo \$PORT", falls back to80if unset - 30-second interval: Two cron entries: first runs at
:00of every minute, second runs at:30(viasleep 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
| Variable | Value | Notes |
|---|---|---|
| Container label | wgg0oso8w00sgoco048w8k0s | Found in Coolify service settings; used to identify container by name rather than ID |
| Cron secret | c549994f99f672ab4cf949f030a242c5 | Must match CRON_SECRET in app env vars; treat as sensitive |
| Log file | /var/log/cron-deviations.log | Monitored by Uptime Kuma; check for recent activity to diagnose failures |
| Interval | 30 seconds | Staggered via two cron entries: :00 and :30 |
| Telegram chat ID | Set via Settings UI | Required 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:
- Open Coolify service settings for Itinera
- Check the current PORT environment variable
- The crontab should auto-detect it via
docker exec $CID sh -c "echo \$PORT"— if detection fails, it falls back to port 80 - If still failing, manually test:
docker exec $CID sh -c "echo \$PORT"to verify the env var exists
Important Notes
- Never add a
check-fuel-levelscron entry — this endpoint doesn't exist. All fuel logic is incheck-deviations. - Samsara API key — Must be set via Settings UI (
app_settings_v2.samsaraApiKey), not as environment variable. - Container label changes with Coolify updates — If the label changes, update both cron entries. Check Coolify service settings to find the new label.
- Log rotation — Consider adding logrotate rules if logs grow large; currently no auto-rotation is configured.
Related Documentation
- Roadmap — Phase 1 operational stability task checklist
- Architecture — API endpoint structure and cron systems