feat: empty CRON_SCHEDULE keeps container alive for manual docker exec

Three container lifetime modes via CRON_SCHEDULE:
  unset          — run once on startup, exit
  empty string   — sleep infinity; use docker exec -it winnow winnow
  cron expression — run on startup, then on schedule

This replaces the need for a separate MANUAL_MODE env var. The empty
string is a natural "I want the container alive but unscheduled" signal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 15:01:00 +00:00
co-authored by Claude Sonnet 4.6
parent 2804b21f9e
commit 015db63d19
3 changed files with 28 additions and 9 deletions
+10 -2
View File
@@ -148,7 +148,15 @@ See [compose.yml](compose.yml) for the full annotated example.
### Scheduling Behaviour
On startup the container always runs once immediately. If `CRON_SCHEDULE` is set, it then starts a scheduler that fires on the defined interval, keeping the process (and loaded models) alive between runs. Without `CRON_SCHEDULE` the container exits after the first run.
`CRON_SCHEDULE` controls container lifetime:
| `CRON_SCHEDULE` value | Behaviour |
| :-- | :-- |
| *(unset)* | Run once on startup, then exit |
| *(empty string)* | Stay alive, run nothing — trigger manually with `docker exec -it winnow winnow` |
| Cron expression | Run on startup, then repeat on schedule |
In scheduled mode the process (and loaded models) stays resident between runs. In manual mode the container idles indefinitely with `sleep infinity` — useful when you want to trigger runs interactively on demand without pulling a new container each time.
The first run after a fresh install downloads the embedding models (~1-2 GB). Subsequent runs use the cached models from the mounted volume and start immediately.
@@ -217,7 +225,7 @@ The first run after a fresh install downloads the embedding models (~1-2 GB). Su
| Variable | Default | Description |
| :--- | :--- | :--- |
| `CRON_SCHEDULE` | *(unset)* | Cron expression for recurring runs — unset exits after first run |
| `CRON_SCHEDULE` | *(unset)* | Unset = run once and exit; empty = stay alive for manual `docker exec`; cron expression = scheduled |
---
+8 -4
View File
@@ -48,10 +48,14 @@ services:
# - RESET_PERSON=John # Clear uploaded+rejected history for one person
# ── Scheduling ────────────────────────────────────────────────────────
# Cron expression (unset = run once and exit)
# - CRON_SCHEDULE=0 3 * * 0 # Every Sunday at 3 AM
# - CRON_SCHEDULE=0 3 1 * * # First of every month
# - CRON_SCHEDULE=*/30 * * * * # Every 30 minutes
# CRON_SCHEDULE controls container lifetime:
# unset — run once on startup, then exit
# empty string — stay alive, run nothing; trigger manually with:
# docker exec -it winnow winnow
# cron expression — run on startup, then on schedule
# - CRON_SCHEDULE= # Manual mode (keep alive, no auto-run)
# - CRON_SCHEDULE=0 3 * * 0 # Every Sunday at 3 AM
# - CRON_SCHEDULE=0 3 1 * * # First of every month
volumes:
# Replace with absolute paths on your host, e.g. /opt/winnow/models
- /path/to/winnow/models:/models
+10 -3
View File
@@ -2,11 +2,19 @@
set -e
export PYTHONUNBUFFERED=1
# 1. Run the job immediately on startup
# CRON_SCHEDULE controls container lifetime:
# unset — run once and exit
# empty string — stay alive, run nothing (use: docker exec -it winnow winnow)
# cron expression — run immediately, then on schedule
if [ "${CRON_SCHEDULE+isset}" = "isset" ] && [ -z "$CRON_SCHEDULE" ]; then
echo "▶ CRON_SCHEDULE is empty — manual mode. Use 'docker exec -it winnow winnow' to run."
exec sleep infinity
fi
echo "▶ Running on startup..."
/app/.venv/bin/winnow
# 2. If a schedule exists, start the scheduler
if [ -n "${CRON_SCHEDULE:-}" ]; then
echo "▶ CRON_SCHEDULE set to: $CRON_SCHEDULE"
echo "▶ Switching to scheduled mode..."
@@ -14,4 +22,3 @@ if [ -n "${CRON_SCHEDULE:-}" ]; then
else
echo "▶ No schedule set, exiting."
fi