What a “cron‑pull” deployment actually does
The most common “quick‑start” deployment script you’ll find in older tutorials looks something like this:
#!/bin/bash
# /usr/local/bin/deploy.sh
cd /var/www/myapp || exit 1
git fetch --all
git reset --hard origin/main
npm install --production
pm2 restart all
System administrators often schedule the script with a simple cron entry:
*/5 * * * * /usr/local/bin/deploy.sh >> /var/log/deploy.log 2>&1
At first glance this looks harmless – every five minutes the server pulls the latest commit and restarts the process manager. The appeal is obvious: no extra services, no external CI platform, and the entire workflow lives in a single file.
Why the “cron‑pull” model is a hidden liability
Below are the three most dangerous internal dynamics that most developers overlook:
-
Race conditions between git fetch and filesystem watchers. When a new commit lands, the fetch step may complete while a file‑watcher (e.g.,
nodemonorwebpack-dev‑server) is still processing the previous change set. This can leave the runtime with a hybrid state – half old code, half new code – leading to obscure runtime errors that only appear under load. -
Unbounded permission creep. The script runs as the
www-datauser, which typically has write access to the entire deployment directory. If an attacker can inject a malicious commit, the same user can overwrite any file, including system‑level scripts or SSH keys stored in.ssh/authorized_keys. Because the cron job runs unattended, the compromise can persist for the full interval between runs. -
Lack of atomicity.
git reset --hardreplaces the working tree file‑by‑file. If the process crashes mid‑reset, the directory ends up in an inconsistent state. There is no built‑in rollback, and the next cron execution may fail to start the service, resulting in downtime that is hard to diagnose.
These problems are rarely mentioned in “how‑to” guides, but they are the reason many production sites experience intermittent “random” crashes after a deployment.
Inspecting the hidden internals with strace
To see what actually happens under the hood, attach strace to the script while it runs:
# In one terminal
sudo strace -f -e trace=file,process -p $(pgrep -f deploy.sh) 2> /tmp/trace.log
# After the cron fires, inspect the log
grep -i rename /tmp/trace.log | head
You’ll notice a flurry of rename() calls as Git rewrites each file. If the process is interrupted, some of those renames will be incomplete, leaving dangling temporary files that the runtime may try to load. This is the low‑level evidence of the atomicity issue described earlier.
A safer alternative: webhook‑driven CI/CD
Instead of polling, let the Git hosting service push a notification when new code arrives. A minimal Node.js webhook receiver can verify the payload, pull the code in a temporary directory, run tests, and swap the live directory atomically.
// webhook.js – a tiny Express receiver
const express = require('express');
const { execSync } = require('child_process');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const app = express();
app.use(express.json());
const SECRET = process.env.WEBHOOK_SECRET;
const DEPLOY_ROOT = '/var/www/myapp';
const TMP_ROOT = '/var/www/myapp_tmp';
function verifySignature(req) {
const sig = req.headers['x-hub-signature-256'];
const hmac = crypto.createHmac('sha256', SECRET);
const digest = 'sha256=' + hmac.update(JSON.stringify(req.body)).digest('hex');
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(digest));
}
app.post('/deploy', (req, res) => {
if (!verifySignature(req)) {
return res.status(401).send('Invalid signature');
}
try {
// 1️⃣ Clone into a fresh temp dir
execSync(`git clone --depth 1 https://github.com/your/repo.git ${TMP_ROOT}`);
// 2️⃣ Run a quick build step
execSync('npm ci --production', { cwd: TMP_ROOT });
// 3️⃣ Atomically replace the live directory
const backup = `${DEPLOY_ROOT}_bak_${Date.now()}`;
fs.renameSync(DEPLOY_ROOT, backup);
fs.renameSync(TMP_ROOT, DEPLOY_ROOT);
// 4️⃣ Restart the process manager
execSync('pm2 restart all');
// 5️⃣ Clean old backup (optional)
execSync(`rm -rf ${backup}`);
res.send('Deployed successfully');
} catch (e) {
console.error(e);
res.status(500).send('Deployment failed');
}
});
app.listen(3000, () => console.log('Webhook listener on :3000'));
Key differences:
- Deployment only runs when the webhook fires – no idle polling.
- The whole operation occurs inside a temporary directory, so a failed build never corrupts the live code.
- Atomic
rename()swaps guarantee that the running service always sees a consistent file set. - Signature verification prevents unauthorized pushes from triggering a deployment.
Integrating the webhook with GitHub
1. In your repository settings, navigate to Webhooks → Add webhook.
2. Set the Payload URL to https://your.server.com/deploy.
3. Choose application/json as the content type.
4. Provide the same secret you set in WEBHOOK_SECRET.
5. Select the “Push events” trigger and save.
After this, any push to main triggers the Node.js listener, which executes the atomic deployment flow.
Security and Best Practices
Even with a webhook, you must harden the environment:
- Run the listener under an unprivileged user. Create a dedicated
deployeraccount with read‑only access to the Git repository and write access only to/var/www. - Limit network exposure. Bind the Express app to
127.0.0.1and expose it through a reverse proxy (e.g., Nginx) that enforces TLS and IP allow‑lists. - Validate the entire git history. Before swapping, run
git log --onelineinside the temporary clone and compare the commit hash to the one announced in the webhook payload. This prevents replay attacks. - Monitor filesystem changes. Use
inotifywaitor a similar tool to watch for unexpected modifications to the deployment directory outside of the controlled swap process.
“Polling is the lazy man’s solution; a webhook is the engineer’s responsibility to keep the system honest.” – Senior DevOps Engineer
Conclusion
The convenience of a cron‑driven git pull script is deceptive. Hidden race conditions, permission creep, and non‑atomic updates make it a fragile foundation for any serious web application. By exposing the internal mechanics with tools like strace and swapping to a webhook‑driven, atomic deployment pipeline, you eliminate those failure modes and gain a clearer security boundary.
Adopt the webhook pattern early, enforce strict user isolation, and keep a watchful eye on filesystem changes. The extra setup effort pays off in reliability, auditability, and, most importantly, a reduction in the silent outages that “cron‑pull” deployments so often mask.