Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ RUN apk update --quiet && \
su-exec \
tini \
wget \
shadow && \
shadow \
python3 \
py3-flask \
py3-supervisor && \
rm /var/cache/apk/* && \
rm -rf /etc/periodic /etc/crontabs/root && \
# Set SUID on crontab command so it can modify crontab files
Expand All @@ -89,11 +92,16 @@ RUN apk update --quiet && \
(getent group | grep -q ":${DOCKER_GID}:" && addgroup docker || addgroup -g ${DOCKER_GID} docker) && \
# Create docker user and add to docker group
adduser -S docker -D -G docker && \
mkdir -p ${HOME_DIR}/jobs ${HOME_DIR}/crontabs && \
mkdir -p ${HOME_DIR}/jobs ${HOME_DIR}/crontabs ${HOME_DIR}/data && \
chown -R docker:docker ${HOME_DIR}

COPY --from=builder /usr/bin/rq/rq /usr/local/bin
COPY entrypoint.sh /opt
COPY supervisord.conf /opt/crontab/
COPY webapp/ /opt/crontab/webapp/

# Expose web UI port
EXPOSE 8080

ENTRYPOINT ["/sbin/tini", "--", "/opt/entrypoint.sh"]

Expand Down
118 changes: 118 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,124 @@ A great project, don't get me wrong. It was just missing certain key enterprise
- Run command in a container using `container`.
- Ability to trigger scripts in other containers on completion cron job using `trigger`.
- Ability to share settings between cron jobs using `~~shared-settings` as a key.
- **Web Dashboard UI** for monitoring and controlling cron jobs.

## Web Dashboard

The crontab container includes a built-in web dashboard for monitoring and managing your cron jobs.

### Features

- 📊 **Job Monitoring**: View all scheduled jobs with their current status
- 📅 **Schedule Information**: See when jobs last ran and when they'll run next
- 📝 **Execution History**: Browse past executions with timestamps and exit codes
- 🔍 **Log Viewer**: View stdout and stderr output from job executions
- ▶️ **Manual Triggering**: Run jobs on-demand with a single click
- 📈 **Dashboard Stats**: Overview of total jobs, failures, and recent activity
- 🔄 **Auto-Refresh**: Dashboard automatically updates every 30 seconds

### Accessing the Web UI

The web dashboard is available on port **8080** by default.

**Docker Run:**

```bash
docker run -d \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v ./config.json:/opt/crontab/config.json:ro \
-v crontab-data:/opt/crontab/data \
-p 8080:8080 \
crontab
```

Then open http://localhost:8080 in your browser.

**Docker Compose:**

```yaml
services:
crontab:
build: .
ports:
- "8080:8080"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./config.json:/opt/crontab/config.json:ro"
- "crontab-data:/opt/crontab/data" # Persistent database
environment:
- WEB_UI_PORT=8080
- JOB_HISTORY_RETENTION_DAYS=30
- JOB_HISTORY_RETENTION_COUNT=1000

volumes:
crontab-data:
```

### Configuration

Configure the web UI using environment variables:

| Variable | Default | Description |
| ----------------------------- | ------- | ----------------------------------------- |
| `WEB_UI_PORT` | `8080` | Port for the web dashboard |
| `JOB_HISTORY_RETENTION_DAYS` | `30` | Keep execution history for this many days |
| `JOB_HISTORY_RETENTION_COUNT` | `1000` | Keep at least this many recent executions |

### Data Persistence

Job execution history is stored in a SQLite database at `/opt/crontab/data/crontab.db`. To persist this data across container restarts, mount a volume:

```bash
-v crontab-data:/opt/crontab/data
```

### Health Check

The web UI includes a health check endpoint at `/api/health`:

```bash
curl http://localhost:8080/api/health
```

Response:

```json
{
"status": "healthy",
"crond_running": true,
"database_accessible": true,
"uptime_seconds": 86400
}
```

### API Endpoints

The dashboard exposes a REST API for programmatic access:

- `GET /api/jobs` - List all jobs
- `GET /api/executions/<job_name>` - Get execution history for a job
- `POST /api/trigger/<job_name>` - Manually trigger a job
- `GET /api/stats` - Get dashboard statistics
- `GET /api/health` - Health check

### Security Considerations

The web UI does **not** include authentication by default. For production deployments:

1. **Reverse Proxy**: Use a reverse proxy (nginx, Traefik) with authentication
1. **Network Isolation**: Run on a private network, not exposed to the internet
1. **Firewall Rules**: Restrict access to trusted IP addresses

Example nginx reverse proxy with basic auth:

```nginx
location /crontab/ {
auth_basic "Crontab Dashboard";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://crontab:8080/;
}
```

## Config file

Expand Down
10 changes: 10 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ services:
# Or alternatively: stat -c '%g' /var/run/docker.sock
DOCKER_GID: 999
restart: always
ports:
- "8080:8080"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "${PWD}/config-samples/config.sample.mapping.json:/opt/crontab/config.json:ro"
- "crontab-data:/opt/crontab/data"
environment:
- WEB_UI_PORT=8080
- JOB_HISTORY_RETENTION_DAYS=30
- JOB_HISTORY_RETENTION_COUNT=1000

volumes:
crontab-data:
49 changes: 48 additions & 1 deletion entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,33 @@ function build_crontab() {
echo "#\!/usr/bin/env bash"
echo "set -e"
echo ""
echo "JOB_NAME=\"${SCRIPT_NAME}\""
echo "TIMESTAMP=\$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "PID=\$\$"
echo ""
echo "# Log job start to database"
echo "python3 /opt/crontab/webapp/db_logger.py start \"\${JOB_NAME}\" \"\${TIMESTAMP}\" \"cron\" \"\${PID}\" 2>&1 || true"
echo ""
echo "# Capture output to temp files"
echo "STDOUT_FILE=\"/tmp/job-\${JOB_NAME}-\$\$.stdout\""
echo "STDERR_FILE=\"/tmp/job-\${JOB_NAME}-\$\$.stderr\""
echo ""
echo "echo \"start cron job __${SCRIPT_NAME}__\""
echo "${CRON_COMMAND}"
echo "set +e"
echo "${CRON_COMMAND} > \"\${STDOUT_FILE}\" 2> \"\${STDERR_FILE}\""
echo "EXIT_CODE=\$?"
echo "set -e"
echo ""
echo "# Log job completion to database"
echo "END_TIMESTAMP=\$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "python3 /opt/crontab/webapp/db_logger.py end \"\${JOB_NAME}\" \"\${END_TIMESTAMP}\" \"\${EXIT_CODE}\" \"\${STDOUT_FILE}\" \"\${STDERR_FILE}\" 2>&1 || true"
echo ""
echo "# Output to container logs"
echo "cat \"\${STDOUT_FILE}\" 2>/dev/null || true"
echo "cat \"\${STDERR_FILE}\" >&2 2>/dev/null || true"
echo ""
echo "# Clean up temp files"
echo "rm -f \"\${STDOUT_FILE}\" \"\${STDERR_FILE}\""
} > "${SCRIPT_PATH}"

TRIGGER=$(echo "${KEY}" | jq -r '.trigger')
Expand Down Expand Up @@ -245,6 +270,18 @@ function build_crontab() {
printf "##### cron running #####\n"
}

init_webapp() {
printf "##### initializing web app #####\n"

# Initialize database schema
python3 /opt/crontab/webapp/init_db.py

# Sync jobs from config to database
python3 /opt/crontab/webapp/sync_jobs.py "${CONFIG}"

printf "##### web app initialized #####\n"
}

start_app() {
normalize_config
export CONFIG=${HOME_DIR}/config.working.json
Expand All @@ -254,6 +291,16 @@ start_app() {
fi
if [ "${1}" == "crond" ]; then
build_crontab
init_webapp
fi

# Use supervisord to manage crond and Flask if we're starting crond
if [ "${1}" == "crond" ]; then
if [ "$(id -u)" = "0" ]; then
exec su-exec docker supervisord -c /opt/crontab/supervisord.conf
else
exec supervisord -c /opt/crontab/supervisord.conf
fi
fi

# Filter out invalid crond flags
Expand Down
28 changes: 28 additions & 0 deletions supervisord.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[supervisord]
nodaemon=true
user=docker
logfile=/dev/null
logfile_maxbytes=0
loglevel=info
pidfile=/tmp/supervisord.pid

[program:crond]
command=crond -f -d 7 -c /opt/crontab/crontabs
autostart=true
autorestart=true
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
stderr_logfile=/dev/fd/2
stderr_logfile_maxbytes=0
priority=10

[program:flask]
command=python3 /opt/crontab/webapp/app.py
autostart=true
autorestart=true
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
stderr_logfile=/dev/fd/2
stderr_logfile_maxbytes=0
environment=FLASK_ENV="production",WEB_UI_PORT="8080"
priority=20
Loading
Loading