Run a Dependency-Vulnerability Scanner as a Scheduled Job on a Droplet
Disclosure: Some links in this guide are affiliate links. If you sign up for DigitalOcean through them, ToolsHubKit earns a small commission — at no cost to you.
A one-off dependency scan tells you about the CVEs known today. But new vulnerabilities get disclosed against libraries you already shipped — code that was clean last week can be vulnerable this morning without you changing a line. This guide turns an on-demand scanner into a scheduled service on a DigitalOcean Droplet: it re-scans your projects on a timer, stores results, and alerts you only when something new appears.
Prerequisites:
- Basic Linux comfort (SSH,
sudo, systemd) - One or more Git repositories (or container images) to scan
- An email relay or a Slack/Discord webhook for alerts
1. From On-Demand Tool to Scheduled Scanner
A manual scanner answers "is this vulnerable right now?" A scheduled scanner answers the more useful question: "has anything I depend on become vulnerable since I last looked?" The design:
systemd timer (daily)
│
▼
Scan runner
├─ pull/refresh each tracked repo
├─ run Trivy/Grype against its lockfiles + images
├─ diff findings against last run
▼
Store results (JSON, dated)
│
└─ New CVE? → alert via email / webhook
The key is the diff: you don't want a daily email listing the same 40 known issues — you want a ping the moment a new one appears.
2. Provision the Droplet
Scanning is light and bursty — a small Droplet is plenty. Create one at DigitalOcean. Get $200 in free credits for 60 days when you sign up.
| Workload | RAM | vCPUs | Droplet |
|---|---|---|---|
| A handful of repos | 1 GB | 1 | Basic $6/mo |
| Many repos + image scanning | 2 GB | 1 | Basic $12/mo |
Choose Ubuntu 24.04 LTS x64 and add your SSH key. Scanning container images pulls layers, so give it the 2 GB tier if you'll scan images heavily.
3. Base Setup and a Scanner
Create a dedicated user and install Trivy (a fast, well-maintained scanner for filesystems, lockfiles, and images):
ssh root@YOUR_SERVER_IP
adduser scanner && usermod -aG sudo scanner
su - scanner
# Install Trivy
sudo apt-get install -y wget gnupg
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | \
sudo gpg --dearmor -o /usr/share/keyrings/trivy.gpg
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | \
sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt-get update && sudo apt-get install -y trivy jq git
Test a scan against a project directory:
git clone https://github.com/your-org/your-repo.git ~/repos/your-repo
trivy fs --scanners vuln --format json --output /tmp/scan.json ~/repos/your-repo
jq '.Results[].Vulnerabilities | length' /tmp/scan.json
4. The Scan Runner Script
Create ~/scan/repos.txt — one repo per line:
https://github.com/your-org/api.git
https://github.com/your-org/web.git
https://github.com/your-org/worker.git
Create ~/scan/run-scans.sh:
#!/bin/bash
set -euo pipefail
BASE=/home/scanner/scan
REPO_DIR=/home/scanner/repos
RESULT_DIR=$BASE/results
STATE_DIR=$BASE/state
STAMP=$(date +%Y%m%d_%H%M%S)
mkdir -p "$REPO_DIR" "$RESULT_DIR" "$STATE_DIR"
NEW_FINDINGS=""
while IFS= read -r repo || [ -n "$repo" ]; do
[ -z "$repo" ] && continue
name=$(basename "$repo" .git)
target="$REPO_DIR/$name"
# Clone or refresh
if [ -d "$target/.git" ]; then
git -C "$target" fetch --quiet origin && git -C "$target" reset --hard --quiet origin/HEAD
else
git clone --quiet --depth 1 "$repo" "$target"
fi
# Scan -> JSON
out="$RESULT_DIR/${name}_${STAMP}.json"
trivy fs --scanners vuln --severity HIGH,CRITICAL --format json --quiet \
--output "$out" "$target" || true
# Extract the set of vulnerability IDs found now
current=$(jq -r '[.Results[]?.Vulnerabilities[]?.VulnerabilityID] | sort | unique | .[]' "$out" 2>/dev/null || true)
# Compare against the last known set for this repo
state="$STATE_DIR/${name}.txt"
touch "$state"
new=$(comm -13 <(sort "$state") <(echo "$current" | sort) || true)
if [ -n "$new" ]; then
NEW_FINDINGS+=$'\n'"### $name"$'\n'"$new"$'\n'
fi
# Persist current state
echo "$current" > "$state"
done < "$BASE/repos.txt"
# Alert only if something new appeared
if [ -n "$NEW_FINDINGS" ]; then
"$BASE/alert.sh" "$NEW_FINDINGS"
fi
# Retain 30 days of raw results
find "$RESULT_DIR" -name '*.json' -mtime +30 -delete
chmod +x ~/scan/run-scans.sh
The comm -13 line is the whole trick: it emits only CVE IDs present in this run that weren't in the last, so you're alerted on change, not on the standing backlog.
5. The Alert Script
Create ~/scan/alert.sh for a Slack/Discord webhook (swap the URL for email via mail if you prefer):
#!/bin/bash
FINDINGS="$1"
WEBHOOK="${SCAN_WEBHOOK_URL:?set SCAN_WEBHOOK_URL}"
payload=$(jq -n --arg text "🚨 New HIGH/CRITICAL vulnerabilities detected:${FINDINGS}" '{text: $text}')
curl -s -X POST "$WEBHOOK" -H 'Content-type: application/json' -d "$payload" > /dev/null
chmod +x ~/scan/alert.sh
For email instead, install a lightweight relay and swap the body of alert.sh:
sudo apt install -y msmtp msmtp-mta
# configure ~/.msmtprc with your SMTP provider, then:
# printf "Subject: New CVEs\n\n%s" "$FINDINGS" | msmtp you@example.com
6. Schedule with a systemd Timer
systemd timers are more robust than cron — they log to journald, survive reboots, and can catch up on missed runs. Create /etc/systemd/system/depscan.service:
[Unit]
Description=Dependency vulnerability scan
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=scanner
Environment=SCAN_WEBHOOK_URL=https://hooks.slack.com/services/XXX/YYY/ZZZ
ExecStart=/home/scanner/scan/run-scans.sh
Create /etc/systemd/system/depscan.timer:
[Unit]
Description=Run dependency scan daily
[Timer]
OnCalendar=*-*-* 07:00:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
Enable it:
sudo systemctl daemon-reload
sudo systemctl enable --now depscan.timer
# Verify the schedule and run once now to seed baseline state
systemctl list-timers depscan.timer
sudo systemctl start depscan.service
sudo journalctl -u depscan.service -f
First run seeds the baseline — it records the current CVE set per repo without alerting on the entire backlog. From the second run on, you're only pinged about genuinely new disclosures.
7. Scanning Container Images Too
If you ship Docker images, scan those as well — a base image can develop CVEs independent of your code. Add to the runner loop:
# Scan a published image by ref
trivy image --severity HIGH,CRITICAL --format json --quiet \
--output "$RESULT_DIR/api-image_${STAMP}.json" \
registry.example.com/your-org/api:latest || true
Trivy caches its vulnerability DB locally and refreshes it automatically, so scheduled runs always check against the latest advisories.
8. Secure the Box
This server holds source code and can reach your registry — lock it down:
# Firewall: SSH only, no inbound services needed
sudo ufw allow OpenSSH
sudo ufw enable
# Harden SSH
sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sudo systemctl restart ssh
# fail2ban
sudo apt install -y fail2ban && sudo systemctl enable --now fail2ban
Use read-only deploy keys for the repos and a read-only registry token — the scanner never needs write access to anything. Store secrets in the systemd unit's Environment= (or better, an EnvironmentFile= with chmod 600), never in the scripts.
9. Wiring It Into ToolsHubKit
If you use ToolsHubKit's dependency-scanner tool for ad-hoc checks, this scheduled job is its always-on counterpart: the tool for "check this project now," the Droplet job for "watch these projects forever." Point the alert webhook at the same channel your team already reads, and newly disclosed CVEs land in front of you without anyone remembering to run a scan.
Related Guides
- Self-host your entire developer stack on one Droplet — run this alongside your other self-hosted services
- GitHub Actions auto-deploy to a Droplet — add scanning as a CI gate too
- Object storage with DigitalOcean Spaces for file-based tools — archive scan results long-term
Resources
| Resource | Link |
|---|---|
| DigitalOcean Droplets (+ $200 free credit) | Sign up here |
| Trivy | https://github.com/aquasecurity/trivy |
| Grype (alternative scanner) | https://github.com/anchore/grype |
| systemd timers | https://www.freedesktop.org/software/systemd/man/systemd.timer.html |