n8nctl — Management Script for n8n and Cloudflare Tunnel
What n8nctl Does
n8nctl is a Bash script that:
No systemd, no supervisor. Just a script you can run manually or wire up to a cron check.
The Script
Save as ~/projects/n8n/n8nctl:
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TUNNEL_CONFIG="$HOME/.cloudflared/n8n-proxy.yml"
TUNNEL_NAME="n8n-proxy"
LOG_FILE="$SCRIPT_DIR/tunnel2.log"
stop_tunnel() {
PIDS=$(pgrep -f "cloudflared.*n8n-proxy" 2>/dev/null || true)
if [ -n "$PIDS" ]; then
echo "Stopping tunnel (PID: $PIDS)..."
echo "$PIDS" | xargs kill 2>/dev/null || true
sleep 2
fi
}
start_tunnel() {
stop_tunnel
echo "Starting n8n Cloudflare tunnel..."
cloudflared --config "$TUNNEL_CONFIG" tunnel run "$TUNNEL_NAME" > "$LOG_FILE" 2>&1 &
sleep 5
if grep -q "Registered tunnel connection" "$LOG_FILE" 2>/dev/null; then
echo "Tunnel is live."
else
echo "Warning: tunnel may not be connected. Check: tail -f $LOG_FILE"
fi
}
case "${1:-}" in
start)
cd "$SCRIPT_DIR" && sudo docker compose up -d
sleep 5
start_tunnel
;;
stop)
cd "$SCRIPT_DIR" && sudo docker compose down
stop_tunnel
;;
restart)
cd "$SCRIPT_DIR" && sudo docker compose restart
sleep 5
stop_tunnel
start_tunnel
;;
status)
echo "=== n8n ==="
sudo docker ps --filter "name=n8n" --format "table {{.Names}} {{.Status}} {{.Ports}}"
echo ""
echo "=== n8n tunnel ==="
PIDS=$(pgrep -f "cloudflared.*n8n-proxy" 2>/dev/null || true)
if [ -n "$PIDS" ]; then
echo "Running (PID: $PIDS)"
else
echo "Not running"
fi
;;
logs)
cd "$SCRIPT_DIR" && sudo docker compose logs -f
;;
tunnel-logs)
tail -f "$LOG_FILE"
;;
*)
echo "Usage: $0 {start|stop|restart|status|logs|tunnel-logs}"
exit 1
;;
esac
Make it executable:
chmod +x ~/projects/n8n/n8nctl
Commands
./n8nctl start — Start n8n + tunnel (waits 5s, checks tunnel health)./n8nctl stop — Stop n8n + tunnel./n8nctl restart — Restart n8n, reconnect tunnel./n8nctl status — Show container and tunnel running state./n8nctl logs — Tail n8n container logs (-f)./n8nctl tunnel-logs — Tail cloudflared tunnel logs (-f)How the Tunnel Health Check Works
After starting cloudflared, the script waits 5 seconds, then greps the tunnel log for "Registered tunnel connection". That string appears once per successful edge registration. If found, the tunnel is live. If not, it prints a warning.
The tunnel log captures both stdout and stderr from cloudflared:
cloudflared --config "$TUNNEL_CONFIG" tunnel run "$TUNNEL_NAME" > "$LOG_FILE" 2>&1 &
The trailing & runs cloudflared in the background so the script returns immediately.
stop_tunnel Uses pgrep
The stop_tunnel function finds cloudflared processes by matching the command line:
pgrep -f "cloudflared.*n8n-proxy"
This is safer than killing by a hardcoded PID — it finds whatever process is currently running the n8n tunnel even if the script restarted it between sessions.
What the Tunnel Log Tells You
A healthy log entry:
2026-06-23T06:47:38Z INF Starting tunnel tunnelID=cecb5bc3-8ea6-4651-b817-d2c49526988a
2026-06-23T06:47:39Z INF Registered tunnel connection connIndex=0 connection=f9b19d2f... location=blr02 protocol=quic
2026-06-23T06:47:39Z INF Registered tunnel connection connIndex=1 connection=ebcb380f... location=bom09 protocol=quic
Four connections registered to four edges (QUIC protocol). This is normal. Cloudflare maintains multiple connections to different PoPs for redundancy.
An unhealthy log — connection terminated errors immediately after startup:
2026-06-02T11:03:14Z ERR failed to serve tunnel connection error="accept stream listener..."
2026-06-02T11:03:14Z ERR Connection terminated connIndex=1
This usually means cloudflared started before n8n was ready. ./n8nctl restart fixes it — n8n is already up, so the tunnel connects immediately.
Series: ← n8n + Cloudflare Tunnel Setup | Next: OpenWA Docker Setup →
Related Posts
Built something similar or want to talk through the architecture? Get in touch.