A modern, web-based cron job management UI for Linux systems. Cronmanager lets you create, edit, monitor, and export cron jobs through a clean browser interface, with full execution history, email failure alerts, execution limits, multi-host support, and SSO integration.
- Features
- Architecture Overview
- Docker Hub – Recommended Installation
- Prerequisites
- Guided Setup (Alternative)
- Quick Start
- Detailed Installation
- OIDC / SSO Setup with Authentik
- Configuration Reference
- Failure Alerts (Email & Telegram)
- Multi-Host Execution
- Crontab Import
- Settings
- Multi-Agent Setup
- Maintenance Windows
- Export
- User Management
- External REST API
- Updating
- Troubleshooting
| Feature | Description |
|---|---|
| Job management | Create, edit, copy, and delete cron jobs with schedule, command, description, and tags |
| Execution tracking | Every job run is recorded: start time, end time, exit code, and captured stdout/stderr output |
| Execution limits | Optional maximum runtime per job; alert and/or auto-kill when the limit is exceeded |
| Kill running execution | Admins can terminate a running job mid-flight from the detail page (local: SIGTERM; SSH: remote kill) |
| Singleton mode | Flag a job so that new executions are silently skipped while a previous instance is still running |
| Job monitor | Per-job statistics page with KPI cards (success rate, avg/min/max duration, alerts), an execution duration line chart, and a stacked bar chart – selectable time window from 1 hour to 1 year; period and target switching updates in-place via AJAX with auto-refresh for short windows |
| Dashboard | At-a-glance view of total jobs, active/inactive counts, and recent failures; KPI cards refresh every 60 s via AJAX |
| Bulk operations | Select multiple jobs on the list page to activate, deactivate, delete, or re-tag them in a single action; running executions block bulk delete with a clear error message |
| Timeline | Filterable, paginated history of all executions across all jobs |
| Swimlane | Visual schedule overview: planned fire times per job across a time-of-day axis, filterable by hour range, day of week, tag, and target |
| Multi-host execution | A single job can run on multiple targets (local + remote SSH) in parallel |
| Tags | Label jobs to enable filtering and grouped export |
| Crontab import | Detect and import existing unmanaged crontab entries |
| Export | Download a ready-to-use crontab file or JSON for all managed jobs |
| Auto-retry on failure | Automatically re-run a failed job up to N times with a configurable delay between attempts; notification is suppressed until all retries are exhausted |
| Exit-code filter for restart | Optionally restrict which exit codes trigger an automatic retry using a flexible expression such as 1-5,10,255; empty (default) means any non-zero code |
| Email alerts | Receive an email when a job exits with a non-zero status or exceeds its execution limit |
| Telegram alerts | Receive a Telegram message for the same events via the Bot API |
| Recovery notifications | Optionally receive an email and/or Telegram message when a job succeeds again after a failure streak that triggered an alert |
| Silence detection | Opt-in per job: check-limits.php uses the cron schedule to calculate the last expected start time and alerts (email + Telegram) if no real execution has been recorded within the schedule interval plus a configurable grace period. Three maintenance-window guards prevent false positives. GET /health exposes a silent_jobs counter for external monitors |
| Maintenance Windows | Define per-target scheduled maintenance windows; jobs are either skipped (exit code −4) or executed silently depending on the per-job setting. A special "Cronmanager Agent" target blocks all executions host-wide (useful for VM maintenance cycles). Conflict icons (⚠ amber / ✕ red) appear in the job list and detail view |
| SSH connectivity test | A Test button on the Maintenance Windows page verifies that the agent can reach an SSH target via key-based auth (BatchMode=yes, 10 s timeout). The result (Connected / Failed) is shown inline without a page reload |
| Startup orphan cleanup | On agent restart, executions still marked as "running" with no live process are automatically resolved to exit code −5 ("Interrupted by system restart") |
| Multi-agent | Manage cron jobs across multiple agents (different hosts) from a single web UI; switch the active agent per user session via a sidebar dropdown |
| Settings | Agent management, crontab sync, stuck-execution cleanup, and history bulk-delete |
| Local & SSO auth | Username/password accounts or OAuth 2.0 / OpenID Connect (OIDC) via Authentik |
| Role-based access | Admin (full access) and Viewer (read-only) roles |
| User management | Admins can promote, demote, or remove users |
| Audit log | Every create, update, and delete operation is recorded with actor, timestamp, and a before/after diff or snapshot; viewable in the web UI (/audit, admin-only) and via the REST API (audit:read scope) |
| External REST API | Scope-based JSON API for external applications; authenticated via Bearer tokens generated in the web UI — see API.md |
| Performance Monitor | Optionally persist per-request and per-query timing data to a performance_log table; optionally display the last API and DB durations in the UI footer — both toggles are independent and configurable under Settings → Agent Settings |
| Internationalisation | English and German out of the box; easy to extend |
| Dark mode | System-preference aware, toggle in the nav bar |
Cronmanager supports two deployment modes.
Browser
│
▼
┌──────────────────────────┐
│ Web UI (Docker) │ PHP-FPM + Nginx · Port 8880
│ /opt/cronmanager/www │
└────────────┬─────────────┘
│ HMAC-signed HTTPS (host.docker.internal:8865)
▼
┌──────────────────────────┐
│ Host Agent │ nginx (TLS) → PHP CLI server · Port 8865
│ /opt/cronmanager/agent │ systemd service on the Docker host
└────────────┬─────────────┘
│ reads/writes crontab files
│ reports execution results via PDO
▼
Linux cron daemon MariaDB container (cronmanager-db)
The agent runs directly on the Docker host. The web container reaches it via
host.docker.internal:8865 (provided by Docker's extra_hosts: host-gateway mechanism).
Communication is encrypted with TLS; see Agent TLS for details.
Browser
│
▼
┌──────────────────────────┐
│ Web UI (Docker) │ PHP-FPM + Nginx · Port 8880
│ /opt/cronmanager/www │
└────────────┬─────────────┘
│ HMAC-signed HTTPS (cronmanager-agent:8865)
▼
┌──────────────────────────┐
│ Agent container │ nginx (TLS) → PHP CLI server · Port 8865
│ cs1711/cs_cronmanageragent (internal Docker network)
└────────────┬─────────────┘
│ manages container's crontab (root)
│ reports execution results via PDO
▼
Container cron daemon MariaDB container (cronmanager-db)
In docker mode the agent runs in its own container alongside the web UI.
All three services share a private cronmanager-internal Docker network.
No PHP installation is required on the host.
The web container never touches crontab files directly. All privileged operations are delegated to the agent via HMAC-secured HTTPS calls.
A MariaDB container (cronmanager-db) stores users, job metadata, tags, and execution logs.
The simplest way to run Cronmanager is to pull the pre-built images directly from Docker Hub. No cloning, no Composer, no PHP on the host — just Docker.
| Requirement | Notes |
|---|---|
| Docker + Docker Compose v2 | Any recent Linux host |
| 5 environment variables | See table below |
Step 1 – Create a working directory and a .env file
mkdir cronmanager && cd cronmanager
cat > .env <<'EOF'
DB_NAME=cronmanager
DB_USER=cronmanager
DB_PASSWORD=change-me
DB_ROOT_PASSWORD=change-me-root
AGENT_HMAC_SECRET=$(openssl rand -hex 32)
EOFTip: run
openssl rand -hex 32separately and paste the output intoAGENT_HMAC_SECRET.
Step 2 – Download the Compose file
curl -fsSL https://raw.githubusercontent.com/csoscd/cronmanager/main/docker/docker-compose-full.yml \
-o docker-compose-full.ymlStep 3 – Start the stack
docker compose -f docker-compose-full.yml up -dOpen http://<your-host>:8880/ — the setup wizard appears on first visit and
lets you create the initial admin account.
| Container | Image | Purpose |
|---|---|---|
cronmanager-db |
mariadb:lts |
Stores users, jobs, and execution history |
cronmanager-agent |
cs1711/cronmanager-agent:latest |
Manages crontabs, runs jobs, exposes HMAC API |
cronmanager-web |
cs1711/cronmanager-web:latest |
PHP-FPM + Nginx web UI |
All persistent data lives in Docker-managed named volumes (db-data, agent-log, web-log).
Note: With the default named volumes, log files live inside Docker-managed storage and are not directly readable on the host filesystem. To access them at a regular host path (e.g. for log forwarding or
tail -f), replace the named volume with a bind mount.docker-compose-full.ymlcontains the required lines as commented-out alternatives — see thevolumes:section ofcronmanager-agentandcronmanager-web.
| Tag | Built from | Use for |
|---|---|---|
latest |
main branch (on every release) |
Production — always stable |
2.5.0, 2.4.0, … |
Git tag on main |
Pinning to a specific release |
dev |
Latest development branch push | Testing unreleased features |
Warning: The
:devtag is overwritten on every push to any active development branch. It may contain incomplete features, breaking changes, or unstable code. Never use:devin production.
To use a specific version, replace :latest in docker-compose-full.yml:
image: cs1711/cronmanager-agent:2.5.0
image: cs1711/cronmanager-web:2.5.0No host directory mounts are needed. Optional host-path alternatives are available as
commented-out lines in docker-compose-full.yml.
| Variable | Description |
|---|---|
AGENT_HMAC_SECRET |
Shared HMAC-SHA256 signing secret (generate with openssl rand -hex 32) |
DB_PASSWORD |
MariaDB application user password |
DB_ROOT_PASSWORD |
MariaDB root password (MariaDB container only) |
DB_NAME |
Database name (default: cronmanager) |
DB_USER |
Database user (default: cronmanager) |
| Variable | Default | Description |
|---|---|---|
AGENT_BIND_ADDRESS |
0.0.0.0 |
Bind address for the PHP HTTP server |
AGENT_PORT |
8865 |
Listening port (used by nginx TLS terminator) |
AGENT_TLS_ENABLED |
true |
Enable nginx TLS reverse proxy (set false only for trusted internal networks) |
TLS_CERT_FILE |
/opt/cronmanager/agent/tls/cert.pem |
Path to TLS certificate inside the container (auto-generated self-signed if absent) |
TLS_KEY_FILE |
/opt/cronmanager/agent/tls/key.pem |
Path to TLS private key inside the container |
DB_HOST |
cronmanager-db |
MariaDB hostname |
LOG_PATH |
/opt/cronmanager/agent/log/cronmanager-agent.log |
Log file path |
LOG_LEVEL |
info |
Monolog level (debug, info, warning, error) |
LOG_MAX_DAYS |
30 |
Log retention in days |
MAIL_ENABLED |
false |
Enable email failure alerts |
MAIL_HOST |
smtp.example.com |
SMTP server hostname |
MAIL_PORT |
587 |
SMTP port |
MAIL_USERNAME |
(empty) | SMTP username |
MAIL_PASSWORD |
(empty) | SMTP password |
MAIL_FROM |
alerts@example.com |
Sender address |
MAIL_FROM_NAME |
Cronmanager |
Sender display name |
MAIL_TO |
admin@example.com |
Alert recipient |
MAIL_ENCRYPTION |
tls |
tls or ssl |
TELEGRAM_ENABLED |
false |
Enable Telegram failure alerts |
TELEGRAM_BOT_TOKEN |
(empty) | Bot API token from @BotFather |
TELEGRAM_CHAT_ID |
(empty) | Target chat, channel, or group ID |
TELEGRAM_TIMEOUT |
15 |
HTTP request timeout in seconds |
WEB_URL |
(empty) | Base URL of the web UI (e.g. https://cronmanager.example.com) — appended to alert notification links |
INFLUXDB_ENABLED |
false |
Enable InfluxDB 2.x metrics export |
INFLUXDB_URL |
http://influxdb:8086 |
InfluxDB base URL |
INFLUXDB_TOKEN |
(empty) | InfluxDB API token |
INFLUXDB_ORG |
(empty) | InfluxDB organisation name |
INFLUXDB_BUCKET |
cronmanager |
InfluxDB bucket name |
INFLUXDB_TIMEOUT |
10 |
HTTP write timeout in seconds |
AGENT_SETTINGS_KEY |
(empty) | When set, mail.password, telegram.bot_token and influxdb.token are encrypted with AES-256-CBC before being stored in the agent_settings DB table. Use at least 32 random characters (openssl rand -hex 32). If unset, values are stored as plaintext. Removing the key after setting it makes stored credentials unreadable until re-saved via the web UI. |
| Variable | Default | Description |
|---|---|---|
AGENT_URL |
https://cronmanager-agent:8865 |
Agent base URL |
AGENT_TIMEOUT |
10 |
HTTP timeout in seconds |
AGENT_SSL_VERIFY |
false |
Verify agent TLS certificate (false = accept self-signed; true = require trusted CA) |
AGENT_SSL_CA_BUNDLE |
(empty) | Path to a custom CA bundle PEM inside the container (used when AGENT_SSL_VERIFY=true with a private CA) |
DB_HOST |
cronmanager-db |
MariaDB hostname |
LOG_PATH |
/var/www/log/cronmanager-web.log |
Log file path |
LOG_LEVEL |
info |
Monolog level |
LOG_MAX_DAYS |
30 |
Log retention in days |
SESSION_LIFETIME |
3600 |
Session cookie max-age in seconds |
SESSION_IDLE_TIMEOUT |
3600 |
Server-side idle expiry in seconds (user is logged out after this many seconds of inactivity) |
SESSION_NAME |
cronmanager_sess |
PHP session cookie name |
I18N_LANGUAGE |
en |
Default UI language (en or de) |
OIDC_ENABLED |
false |
Enable OIDC / SSO login |
OIDC_PROVIDER_URL |
(empty) | OIDC provider discovery URL |
OIDC_CLIENT_ID |
(empty) | OAuth 2.0 client ID |
OIDC_CLIENT_SECRET |
(empty) | OAuth 2.0 client secret |
OIDC_REDIRECT_URI |
(empty) | Callback URL registered at the provider |
OIDC_SSL_VERIFY |
true |
Verify TLS certificate of the OIDC provider |
OIDC_SSL_CA_BUNDLE |
(empty) | Path to custom CA bundle (inside container) |
docker compose -f docker-compose-full.yml pull
docker compose -f docker-compose-full.yml up -dThe agent container automatically applies any new SQL migrations on startup.
The docker-compose-full.yml mounts /root/.ssh from the Docker host into the agent
container by default:
- /root/.ssh:/root/.ssh:roThis gives the agent access to all SSH host aliases and key pairs configured for the host's root user, which is the simplest setup.
Security note: Mounting
/root/.sshexposes every SSH key and host alias configured for root — including connections to systems unrelated to Cronmanager. If the agent container were ever compromised, all those keys would be at risk.
Create a dedicated directory with only the keys and hosts Cronmanager needs:
mkdir -p /opt/cronmanager/.ssh
chmod 700 /opt/cronmanager/.ssh
# Generate a dedicated key pair (no passphrase for unattended use)
ssh-keygen -t ed25519 -C "cronmanager-agent" -N "" \
-f /opt/cronmanager/.ssh/id_ed25519
# Create a config file listing only the hosts Cronmanager manages
cat > /opt/cronmanager/.ssh/config <<'EOF'
Host myserver1
HostName 192.168.1.10
User root
IdentityFile ~/.ssh/id_ed25519
BatchMode yes
ConnectTimeout 10
Host myserver2
HostName 192.168.1.11
User root
IdentityFile ~/.ssh/id_ed25519
BatchMode yes
ConnectTimeout 10
EOF
chmod 600 /opt/cronmanager/.ssh/configThen in docker-compose-full.yml, replace the default mount with:
- /opt/cronmanager/.ssh:/root/.ssh:roIf you want to import or run cron jobs on the Docker host itself (the machine running the containers), the agent container must be able to SSH back to it.
-
Add the host to the SSH config (
/root/.ssh/configor/opt/cronmanager/.ssh/config):Host dockerhost HostName host.docker.internal User root IdentityFile ~/.ssh/id_ed25519 BatchMode yes ConnectTimeout 10host.docker.internalresolves to the Docker host's gateway IP inside the container. Add it to the agent service indocker-compose-full.ymlif not already present:extra_hosts: - "host.docker.internal:host-gateway"
-
Authorise the agent's public key on the Docker host:
# Append the public key to root's authorized_keys on the Docker host cat /opt/cronmanager/.ssh/id_ed25519.pub >> /root/.ssh/authorized_keys chmod 600 /root/.ssh/authorized_keys # Ensure the SSH daemon permits key-based root login grep -i permitrootlogin /etc/ssh/sshd_config # Should be: PermitRootLogin prohibit-password
-
Add
StrictHostKeyChecking accept-newto the SSH config:Without this, SSH will silently refuse to connect when the host key is not yet in
known_hosts(becauseBatchMode yessuppresses all interactive prompts). Add the line to thedockerhostblock in/root/.ssh/config:Host dockerhost HostName host.docker.internal User root IdentityFile ~/.ssh/id_ed25519 BatchMode yes ConnectTimeout 10 StrictHostKeyChecking accept-new -
Add the host key to
known_hosts:host.docker.internalonly resolves inside Docker containers — not on the Docker host itself — sossh-keyscancannot be run directly on the host. Instead, run it from inside the agent container and redirect the output to the host'sknown_hostsfile (the>>executes in the host shell):docker exec cronmanager-agent ssh-keyscan -H host.docker.internal \ >> /root/.ssh/known_hosts
Why this works:
ssh-keyscanruns inside the container wherehost.docker.internalresolves correctly to the Docker gateway IP. The>>redirect runs in the host shell and writes directly to the host's/root/.ssh/known_hosts. The container sees the updated file immediately via the read-only mount — no container restart required. -
Verify:
docker exec cronmanager-agent ssh dockerhost 'crontab -l -u root'
You should see the root crontab output. If it works here, the Cronmanager import page will list the Docker host as a target and show its crontab entries.
| Component | Requirement |
|---|---|
| Docker + Docker Compose | v2.0 or later |
| PHP on the host | 8.4 with extensions: cli, json, pdo_mysql, openssl, mbstring — host-agent mode only; not required for docker mode |
| Composer | 2.x (to install shared PHP libraries) |
| curl | For the cron wrapper script |
| openssl | For HMAC-SHA256 signing in the wrapper |
| SSH client | Required only for remote job execution |
The Docker image used for the web container (cs1711/cs_cronmanagerweb:latest) is a
Debian-based image (derived from cs_php-nginx-fpm:latest-debian) that includes
PHP-FPM 8.4, Nginx, and supervisord with production PHP settings pre-configured.
Alternative images: Any Docker image that bundles PHP-FPM 8.4 (or later 8.x) with Nginx (or Apache) and the required PHP extensions (
pdo_mysql,json,mbstring,openssl,curl) is supported. Official images such asphp:8.4-fpmcombined with a separate Nginx container, or community images likewebdevops/php-nginx:8.4, are equally valid. Update theimage:field indocker-compose.ymlaccordingly.
APCu extension: The swimlane view uses APCu for in-memory caching of pre-computed cron fire-time patterns. The
cs1711/cs_cronmanagerwebimage includesphp8.4-apcuout of the box. If you use a custom base image, install thephp8.4-apcupackage (Debian/Ubuntu) for best performance. The swimlane view works without APCu but will recompute all patterns on every page load. Verify with:docker exec <container> php -r "var_dump(extension_loaded('apcu'));"
For a fresh installation on a Debian or Ubuntu host, the easiest path is the interactive setup script included in the repository. It guides you through every step in a single session — no manual config file editing required.
curl -fsSL https://raw.githubusercontent.com/csoscd/cronmanager/main/simple_debian_setup.sh | sudo bashNote: Piping directly into
bashis convenient but means you trust the content of the script at that URL. If you prefer to review it first:curl -fsSL https://raw.githubusercontent.com/csoscd/cronmanager/main/simple_debian_setup.sh \ -o simple_debian_setup.sh less simple_debian_setup.sh # review sudo bash simple_debian_setup.sh # run
| Step | What happens |
|---|---|
| Target host | Choose local installation or a remote server via SSH. SSH connectivity and root access are verified before anything else. |
| Prerequisites | Checks for PHP 8.4, required extensions, Docker, Composer, git, openssl, rsync and more — on the target host. Lists any missing packages and offers to install them via apt. |
| Repository clone | Clones the repository locally, then deploys files to the target. |
| Composer / PHP libraries | Verifies that all required third-party libraries are present on the target. Offers to add missing packages to composer.json and run composer install. |
| Configuration interview | Collects all settings interactively — paths, database credentials, agent and web settings — before touching anything on disk. |
| HMAC secret | Generates a cryptographically random 64-character secret with openssl rand -hex 32. Both the agent and web application receive the same value automatically. |
| Host agent deployment | Deploys agent files, patches paths, writes config/config.json, installs the systemd service, starts it, and runs a health check. |
| Web application deployment | Deploys web files, downloads Tailwind CSS and Chart.js, writes conf/config.json. |
| Docker Compose | Generates a customised docker-compose.yml from your settings, displays it, and optionally runs docker compose up -d. |
| Database schema | Waits for MariaDB to become healthy, then applies schema.sql and all migrations via docker exec. |
| Optional: OIDC | Asks for provider URL, client credentials, redirect URI, and SSL/CA settings. |
| Optional: Email alerts | Asks for SMTP host, port, credentials and encryption. |
| Summary | Prints all paths, management commands, the web UI URL, and the generated HMAC secret. |
Requirements: Debian 12+ or Ubuntu 22.04+, internet access on the target, root access.
# 1. Clone the repository on your development / deployment machine
git clone <repo-url> cronmanager
cd cronmanager
# 2. Configure deployment
cp deploy.env.example deploy.env # edit SSH host and target paths
cp db.credentials.example db.credentials # set database passwords
# 3. Full deployment to the target host
# Use --host-agent if the agent runs as a systemd service on the host,
# or --docker if the agent runs as a Docker container.
./deploy.sh --host-agent full # host-agent mode
# or:
./deploy.sh --docker full # docker mode
# 4. Open the web UI
http://<your-host>:8880/
# → First visit shows the setup wizard to create the initial admin accountCronmanager uses a shared vendor directory (/opt/phplib/vendor) that is loaded by
both the host agent (directly on the filesystem) and the web container (via Docker volume mount).
Install PHP 8.4 on the host (Debian/Ubuntu):
sudo apt-get install -y php8.4-cli php8.4-mysql php8.4-mbstring curl opensslInstall Composer (if not already present):
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composerInstall PHP dependencies into the shared vendor directory:
# Create the shared library directory
sudo mkdir -p /opt/phplib
# Copy the project's composer.json there
# (deploy.sh does this automatically on the first full deployment if the file is absent)
sudo cp composer.json /opt/phplib/composer.json
# Install packages
cd /opt/phplib
sudo composer install --no-dev --optimize-autoloaderThe resulting /opt/phplib/vendor/autoload.php is used by both the host agent and the
web container.
Configure deployment:
cp deploy.env.example deploy.envEdit deploy.env:
DEPLOY_TYPE=SSH # SSH (remote host) or LOCAL (same machine)
DEPLOY_SSH=myserver # Host alias from ~/.ssh/config
DEPLOY_COMPOSER=/opt/phplib/
DEPLOY_COMPOSER_VENDOR=/opt/phplib/vendor/Deployment paths are fixed: agent →
/opt/cronmanager/agent, web →/opt/cronmanager/www, DB →/opt/cronmanager/db. These are not configurable indeploy.env.
Configure database credentials:
cp db.credentials.example db.credentialsEdit db.credentials:
DB_NAME=cronmanager
DB_USER=cronmanager
DB_PASSWORD=<strong-password>
DB_ROOT_USER=root
DB_ROOT_PASSWORD=<strong-root-password>
db.credentialscontains plain-text passwords. Keep it out of version control.
Run the deployment:
./deploy.sh --host-agent full # host-agent mode
# or:
./deploy.sh --docker full # docker modeThe script will:
- In
--host-agentmode: installs and enables the systemd service for the host agent - In
--dockermode: skips systemd; use docker-compose to start the agent container - Create all required directories on the target
- Sync all application files via rsync
- Deploy the example configuration files (only if no config exists yet)
- Generate the MariaDB init script from your credentials
- Attempt to apply the database schema (once the container is running)
The agent configuration is at /opt/cronmanager/agent/config/config.json.
On the first deployment, the example configuration is placed there automatically.
Minimum required changes:
{
"agent": {
"bind_address": "0.0.0.0",
"port": 8865,
"hmac_secret": "<generate-a-random-32-char-string>"
},
"database": {
"host": "127.0.0.1",
"port": 3306,
"name": "cronmanager",
"user": "cronmanager",
"password": "<same-as-DB_PASSWORD-in-db.credentials>"
}
}Generate a strong HMAC secret:
openssl rand -hex 32The same
hmac_secretvalue must appear in both the agent config and the web app config.
The deployment script installs and starts the systemd service automatically. You can manage it with standard systemd commands:
# Check service status
sudo systemctl status cronmanager-agent
# View live logs
sudo journalctl -u cronmanager-agent -f
# Restart after a config change
sudo systemctl restart cronmanager-agent
# Verify the agent is reachable (TLS with self-signed cert)
curl -k https://127.0.0.1:8865/health
# → {"status":"ok","timestamp":"2026-03-18T10:00:00+00:00"}The web configuration is at /opt/cronmanager/www/conf/config.json.
On the first deployment, the example configuration is placed there automatically.
Minimum required changes:
{
"database": {
"host": "cronmanager-db",
"port": 3306,
"name": "cronmanager",
"user": "cronmanager",
"password": "<same-as-DB_PASSWORD-in-db.credentials>"
},
"agent": {
"url": "https://host.docker.internal:8865",
"hmac_secret": "<same-secret-as-in-agent-config>",
"timeout": 10,
"ssl_verify": false,
"ssl_ca_bundle": ""
}
}Docker mode: set
agent.urltohttps://cronmanager-agent:8865instead.deploy.sh --docker fullpatches this automatically on the first deployment.
host.docker.internal resolves to the Docker host from within the container and is
configured automatically via the extra_hosts entry in docker/docker-compose.yml (host-agent mode). In docker mode, use cronmanager-agent as the hostname instead — this is the Docker service name on the shared internal network.
ssl_verify: false is correct for the default self-signed certificate. Set it to true and supply ssl_ca_bundle when using a certificate signed by a private CA.
Option A – docker compose directly on the host:
# Host-agent mode: web + MariaDB only
cd /opt/cronmanager/www # place docker-compose.yml here, or use the file from docker/docker-compose.yml
# Docker mode: agent + web + MariaDB
cd /opt/cronmanager/www # use docker/docker-compose-agent.yml
export DB_NAME=cronmanager
export DB_USER=cronmanager
export DB_PASSWORD=<your-password>
export DB_ROOT_PASSWORD=<your-root-password>
docker compose up -dOption B – Portainer:
- Open Portainer → Stacks → Add Stack
- Paste the contents of
docker-compose.yml - Add the following environment variables:
DB_NAME=cronmanagerDB_USER=cronmanagerDB_PASSWORD= your passwordDB_ROOT_PASSWORD= your root password
- Deploy the stack
Apply the database schema (first deployment only, once the MariaDB container is healthy):
ssh myserver 'docker exec -i cronmanager-db mariadb \
-u cronmanager -p<password> cronmanager \
< /opt/cronmanager/agent/sql/schema.sql'Open http://<your-host>:8880/ in your browser.
If no users exist in the database yet, you are automatically redirected to the Setup wizard:
- Enter a username for the initial admin account
- Enter and confirm a password (minimum 8 characters)
- Click Create admin account
You are then redirected to the login page. Log in with the credentials you just created.
Cronmanager supports Single Sign-On via any OpenID Connect provider. The following instructions use Authentik as the identity provider.
- Go to Applications → Providers → Create
- Choose OAuth2/OpenID Connect Provider
- Configure the provider:
- Name:
Cronmanager - Client type: Confidential
- Redirect URIs:
https://cronmanager.example.com/auth/callback(replace with your actual domain — must matchoidc_redirect_uriexactly) - Scopes:
openid,email,profile
- Name:
- After saving, note the Client ID and Client Secret
- Go to Applications → Applications → Create
- Set a name and slug (e.g.
cronmanager) - Assign the provider created above
- Save
Open the provider detail page and look for the OpenID Configuration URL — it resembles:
https://auth.example.com/application/o/cronmanager/.well-known/openid-configuration
The value you need for oidc_provider_url is everything before .well-known:
https://auth.example.com/application/o/cronmanager/
Edit /opt/cronmanager/www/conf/config.json:
{
"auth": {
"oidc_enabled": true,
"oidc_provider_url": "https://auth.example.com/application/o/cronmanager/",
"oidc_client_id": "<client-id-from-authentik>",
"oidc_client_secret": "<client-secret-from-authentik>",
"oidc_redirect_uri": "https://cronmanager.example.com/auth/callback",
"oidc_ssl_verify": true,
"oidc_ssl_ca_bundle": ""
}
}Restart the web container to apply:
docker restart cronmanager-webThe login page now shows a "Login with SSO" button alongside the local login form.
If your Authentik instance uses a certificate issued by an internal CA:
# Copy the CA certificate (PEM format) to the config directory
cp root_ca.crt /opt/cronmanager/www/conf/root_ca.crt
chmod 644 /opt/cronmanager/www/conf/root_ca.crtThen set in config.json:
"oidc_ssl_ca_bundle": "/var/www/conf/root_ca.crt"The conf/ directory is already mounted as /var/www/conf inside the container.
To disable certificate verification entirely (not recommended):
"oidc_ssl_verify": falseWhen an SSO user logs in for the first time, Cronmanager automatically creates a local record with the Viewer role. An admin can promote them via the User Management page.
Deleting an SSO user's Cronmanager account does not revoke access on the OIDC provider — the account will be re-created on the next login.
| Key | Default | Description |
|---|---|---|
database.host |
cronmanager-db |
MariaDB hostname (Docker service name) |
database.port |
3306 |
MariaDB port |
database.name |
cronmanager |
Database name |
database.user |
cronmanager |
Database user |
database.password |
Database password | |
agent.url |
https://host.docker.internal:8865 (host-agent) / https://cronmanager-agent:8865 (docker) |
Host agent base URL |
agent.hmac_secret |
Shared HMAC secret (must match agent) | |
agent.timeout |
10 |
HTTP timeout in seconds |
agent.ssl_verify |
false |
false = accept self-signed cert; true = require trusted CA; path string = use custom CA bundle |
agent.ssl_ca_bundle |
"" |
Path to a PEM CA bundle inside the container (used when ssl_verify is true with a private CA) |
logging.path |
/var/www/log/cronmanager-web.log |
Log file path |
logging.level |
info |
debug, info, warning, error, critical |
logging.max_days |
30 |
Log file retention in days |
session.lifetime |
3600 |
Session cookie max-age in seconds |
session.idle_timeout |
3600 |
Server-side idle expiry in seconds |
session.name |
cronmanager_sess |
Session cookie name |
i18n.default_language |
en |
Default language (en or de) |
auth.oidc_enabled |
false |
Enable OIDC SSO |
auth.oidc_provider_url |
OIDC provider base URL (with trailing slash) | |
auth.oidc_client_id |
OAuth 2.0 Client ID | |
auth.oidc_client_secret |
OAuth 2.0 Client Secret | |
auth.oidc_redirect_uri |
Callback URL (https://your-domain/auth/callback) |
|
auth.oidc_ssl_verify |
true |
true = system CA, false = disable, or path to CA bundle |
auth.oidc_ssl_ca_bundle |
"" |
Path to custom PEM CA bundle (empty = system CA) |
| Key | Default | Description |
|---|---|---|
agent.bind_address |
0.0.0.0 |
Bind address for the internal PHP server (nginx handles the external port) |
agent.port |
8865 |
External port (nginx TLS) |
agent.tls_enabled |
true |
Written automatically from AGENT_TLS_ENABLED; read by cron-wrapper.sh to decide HTTP vs HTTPS |
agent.hmac_secret |
Shared HMAC secret (must match web config) | |
database.host |
127.0.0.1 |
MariaDB hostname |
database.port |
3306 |
MariaDB port |
database.name |
cronmanager |
Database name |
database.user |
cronmanager |
Database user |
database.password |
Database password | |
logging.path |
/opt/cronmanager/agent/log/cronmanager-agent.log |
Log file path |
logging.level |
info |
Log level |
logging.max_days |
30 |
Log file retention in days |
mail.enabled |
false |
Enable email failure alerts |
mail.host |
SMTP server hostname | |
mail.port |
587 |
SMTP port |
mail.username |
SMTP username | |
mail.password |
SMTP password | |
mail.from |
Sender address | |
mail.from_name |
Cronmanager |
Sender display name |
mail.to |
Recipient address for alerts | |
mail.encryption |
tls |
tls (STARTTLS, port 587) or ssl (SMTPS, port 465) |
mail.smtp_timeout |
15 |
SMTP connection timeout in seconds |
telegram.enabled |
false |
Enable Telegram failure alerts |
telegram.bot_token |
Bot API token from @BotFather | |
telegram.chat_id |
Target chat, channel, or group ID | |
telegram.timeout |
15 |
HTTP request timeout in seconds |
cron.wrapper_script |
/opt/cronmanager/agent/bin/cron-wrapper.sh |
Wrapper script path |
All communication between the web container and the host agent is encrypted with TLS. The agent container runs an nginx reverse proxy that terminates TLS on port 8865 and forwards plain HTTP internally to the PHP built-in server on port 18865.
By default a self-signed RSA-2048 certificate (valid 10 years) is generated
automatically on the first container start and stored in a Docker-managed named volume
(agent-tls) so it persists across container recreations.
To use your own certificate (Let's Encrypt, private CA, etc.), mount the cert and key files into the container and set the corresponding environment variables:
environment:
TLS_CERT_FILE: /opt/cronmanager/agent/tls/cert.pem
TLS_KEY_FILE: /opt/cronmanager/agent/tls/key.pem
volumes:
- /path/to/your/cert.pem:/opt/cronmanager/agent/tls/cert.pem:ro
- /path/to/your/key.pem:/opt/cronmanager/agent/tls/key.pem:roSet agent.ssl_verify (or AGENT_SSL_VERIFY) on the web container:
| Value | When to use |
|---|---|
false |
Self-signed certificate (default) |
true |
Certificate from a public/trusted CA |
/path/to/ca.pem |
Certificate from a private CA – provide the CA bundle path |
When using a custom CA bundle, also set agent.ssl_ca_bundle to the path of the PEM
file inside the web container.
TLS can be disabled by setting AGENT_TLS_ENABLED=false on the agent container and
changing agent.url back to http:// in the web config. This is only recommended
for isolated internal networks where encryption is provided at another layer.
Cronmanager can send failure alerts when a cron job exits with a non-zero status code, is auto-killed after exceeding its execution limit, or is still running past its limit. Both email and Telegram can be enabled independently and fire in parallel.
The recommended way to configure notifications is the Agent Settings page at
Settings → Agent Settings (/settings/agent-config). It provides a form for
all four sections (General, Email, Telegram, InfluxDB) and writes the values directly
to the agent's database — settings persist across container restarts without changing
environment variables.
The page also supports copying settings from one agent to another, which is useful when running multiple agents that share the same SMTP or Telegram configuration.
Encryption at rest: if you set
AGENT_SETTINGS_KEYon the agent container, passwords and tokens are encrypted with AES-256-CBC before being stored. See the environment variables reference for details.
Alternatively, settings can still be supplied through environment variables in Docker Compose (see below). When both exist, the database value takes precedence.
To enable:
- Set
mail.enabled = trueand fill in your SMTP credentials in the agent config (or setMAIL_ENABLED=trueand the otherMAIL_*variables in Docker Compose), or use the web UI at Settings → Agent Settings - Restart the agent when using environment variables:
sudo systemctl restart cronmanager-agent - Per job: check "Notify on failure" when creating or editing the job
Alerts are dispatched asynchronously after the job completes — SMTP runs in a background process so a slow or unreachable server cannot block the agent.
Encryption settings:
- Port 465 (SMTPS / implicit TLS) → set
mail.encryptiontossl - Port 587 (STARTTLS) → set
mail.encryptiontotls
Mixing these will cause the connection to hang until the SMTP timeout is reached.
Cronmanager can also send alerts via a Telegram bot.
Prerequisites:
-
Create a bot via @BotFather — it gives you a Bot API token
-
Start a conversation with your bot (or add it to a group/channel) and retrieve the chat ID
The easiest way to get the chat ID:
https://api.telegram.org/bot<YOUR_TOKEN>/getUpdatesSend any message to the bot first, then call the URL — look for
"chat":{"id":...}in the response.
To enable:
- Set
telegram.enabled = true,telegram.bot_token, andtelegram.chat_idin the agent config (or setTELEGRAM_ENABLED=true,TELEGRAM_BOT_TOKEN, andTELEGRAM_CHAT_IDin Docker Compose) - Restart the agent
- Per job: check "Notify on failure" when creating or editing the job — the same flag controls both channels
Docker Compose example (.env):
TELEGRAM_ENABLED=true
TELEGRAM_BOT_TOKEN=123456789:AABBccDDeeFFggHHiiJJkkLLmmNNoo...
TELEGRAM_CHAT_ID=-1001234567890Messages are sent in HTML parse mode and include the job ID, description, user, schedule, exit code, start/notification time, and the captured output (truncated to 2 000 characters). The same context-aware labels as email apply: jobs that are still running show "N/A – job still running" as the exit code and "Notified At" instead of "Finished".
Cronmanager can write a data point to InfluxDB 2.x after every completed execution. This lets you build dashboards in Grafana (or any Flux-capable tool) showing execution history, success rates, durations, and failure trends across all your jobs.
Every completed execution produces one cron_execution data point:
| Name | Type | Description | |
|---|---|---|---|
| Tag | job_id |
string | Numeric job ID |
| Tag | description |
string | Job description |
| Tag | linux_user |
string | Linux user that ran the job |
| Tag | target |
string | Execution target (local or SSH alias) |
| Tag | status |
string | success, failed, killed, limit_exceeded, maintenance, interrupted |
| Tag | job_tags |
string | Comma-separated job tags (omitted when empty) |
| Field | duration_seconds |
float | Elapsed wall-clock time in seconds |
| Field | exit_code |
int | Raw process exit code |
| Field | output_length |
int | Bytes of captured output |
| Field | during_maintenance |
int | 1 if a maintenance window was active, else 0 |
Writes are dispatched in a background process (send-influx.php) so a slow or
unreachable InfluxDB instance never blocks the agent's HTTP response.
INFLUXDB_ENABLED=true
INFLUXDB_URL=http://influxdb:8086
INFLUXDB_TOKEN=your-api-token
INFLUXDB_ORG=your-org
INFLUXDB_BUCKET=cronmanagerAfter adding these variables, restart the agent container so the entrypoint regenerates
config.json.
An importable Grafana dashboard is included at grafana/cronmanager-overview.json.
To import:
- Grafana → Dashboards → Import
- Upload
grafana/cronmanager-overview.json(or paste its JSON) - Select your InfluxDB datasource when prompted for
DS_INFLUXDB - Set the
bucketvariable to your bucket name (default:cronmanager)
Panels included:
| Panel | Type |
|---|---|
| Total Executions | Stat |
| Success Rate | Gauge |
| Failed | Stat |
| Avg Duration | Stat |
| Maintenance Skipped | Stat |
| Executions over Time by Status | Stacked time series |
| Duration over Time by Job | Time series |
| Executions by Job | Horizontal bar chart |
| Avg Duration by Job | Horizontal bar chart |
| Recent Failures (last 50) | Table |
A single cron job can execute on multiple targets simultaneously:
- local – Runs on the host where the agent is installed
- SSH alias – Runs on a remote host via an alias from
~/.ssh/configof the Linux user whose crontab is managed
When a job has multiple targets, one independent crontab entry is created per target. They all fire at the same scheduled time, run in parallel via SSH (BatchMode=yes), and each reports its execution result back to the agent separately.
Prerequisite for SSH targets: the Linux user must have key-based SSH access
configured for the target host in ~/.ssh/config. Password prompts are not supported.
Existing crontab entries not managed by Cronmanager can be imported:
- Go to Cron Jobs → Import (admin only)
- Select the Linux user whose crontab to scan
- Click Load entries – unmanaged lines are displayed
- Select entries to import; optionally add a description and tags
- Click Import selected
After import, the original unmanaged lines are commented out in the crontab file and replaced with managed wrapper-script entries.
The agent manages crontab files directly on the host for each configured Linux user:
# View the crontab for a specific user
crontab -u <linux-user> -l
# View the raw crontab file
cat /var/spool/cron/crontabs/<linux-user>Managed entries are prefixed with a # Cronmanager: comment line and call the wrapper script:
# Cronmanager: My job id:42
*/5 * * * * /opt/cronmanager/agent/bin/cron-wrapper.sh 42 local
In docker mode the agent runs inside the cronmanager-agent container and cron jobs run as root inside that container. The crontab is the container root user's crontab.
# View the crontab inside the agent container
docker exec cronmanager-agent crontab -l
# View the raw crontab file inside the container
docker exec cronmanager-agent cat /var/spool/cron/crontabs/rootNote: After migrating from host-agent to docker mode, use Housekeeping → Crontab Sync in the web UI to write all active jobs into the container's crontab. Without this step the container crontab will be empty and no jobs will execute.
Linux user requirement: In docker mode all jobs run as
rootinside the container. Ensure every job's Linux user is set torootbefore running Crontab Sync.
The Settings page (/settings, admin only) provides operational tools for keeping the system healthy.
Re-writes all crontab entries from the database in one click. Active jobs have their entries added or updated; inactive jobs have any lingering entries removed. Use this after migrating from host-agent to docker mode, or whenever crontab entries get out of sync with the database.
Lists executions that have been in the "running" state longer than a configurable threshold (default: 2 hours). This can occur when the agent restarted mid-execution, leaving records without a finish timestamp.
Tip: The Startup Orphan Cleanup feature automatically resolves most of these cases on agent restart. The Stuck Executions panel handles any edge cases that slip through (e.g. very recent restarts within the 2-minute grace period).
Per-row actions:
- Mark Finished – sets
exit_code = -1, recordsfinished_at = NOW(), appends a note to the output - Delete – permanently removes the execution record
Bulk actions: rows can be selected individually or all at once with the "Select All" checkbox. The bulk toolbar appears when at least one row is selected and provides the same two actions for all selected rows at once.
The lookback threshold is adjustable with an inline hour selector without leaving the page.
Bulk-deletes finished execution records older than a configurable number of days (default: 90). Only records with a non-NULL finished_at are eligible; running executions are never deleted. Use this to reclaim database space on long-running installations.
Two independently configurable options under Settings → Agent Settings:
| Option | Description |
|---|---|
| Persist performance data | Writes request duration, aggregated DB query time, and query count to the performance_log table after every agent request. Useful for identifying slow endpoints over time. |
| Show performance info in frontend | Enriches every agent JSON response with a _perf field containing request_ms, db_ms, and db_queries. The web UI footer displays these values for the most recent agent call. Works independently of the persist option. |
Lists all configured remote agents with name, URL, live connection status, and edit/delete actions. See Multi-Agent Setup for details.
Cronmanager v4.0.0 can manage cron jobs across multiple agents running on different hosts — all from a single web UI. Each agent is an independent Cronmanager agent container (or host-agent service) with its own MariaDB and crontab. The web UI stores a registry of configured agents in its own agents table and lets each user switch between them via a sidebar dropdown.
Browser
│
▼
┌──────────────────────────┐
│ Web UI (single instance) │
│ agents table (registry) │
└───┬───────────┬───────────┘
│ HMAC │ HMAC
▼ ▼
┌───────────┐ ┌───────────┐
│ Agent A │ │ Agent B │
│ host-1 │ │ host-2 │
│ MariaDB │ │ MariaDB │
└───────────┘ └───────────┘
- The
agentstable lives in the web UI's database (same MariaDB as users and sessions). - Each agent has its own MariaDB storing cronjobs, tags, and execution history.
- When you switch the active agent, the web UI sends all subsequent API calls to that agent's URL. The dashboard, job list, timeline, monitor, and export all reflect the selected agent's data.
- Each user's selection is stored in their PHP session — different users can view different agents simultaneously.
Set up a Cronmanager agent on the second host using the same procedure as the primary agent (Docker Hub image or manual deployment). The second agent needs:
- Its own MariaDB container (or database on a shared MariaDB instance)
- Its own HMAC secret (generate with
openssl rand -hex 32) - A port reachable from the web UI container (default: 8865 / HTTPS)
Make sure the web UI container can reach the second agent's URL. If the agent is on another host in your network, verify firewall rules allow TCP 8865 from the web container's host.
Test connectivity from the web container:
docker exec cronmanager-web curl -sk https://<second-host>:8865/health
# → {"status":"ok","timestamp":"...","version":"..."}-
Log in as admin
-
Go to Settings (
/settings) -
In the Agents section click + Add agent
-
Fill in the form:
Field Example Notes Name host-2Display name in the sidebar switcher Description Production server 2Optional URL https://192.168.1.20:8865Base URL of the agent; must be reachable from the web container HMAC secret <openssl rand -hex 32>Must match the second agent's agent.hmac_secretTimeout 10HTTP timeout in seconds Verify SSL certificate unchecked Uncheck for self-signed certs (default); check + supply CA bundle for private CA Sort order 1Controls the dropdown order (0 = first) Enabled checked Uncheck to hide from the switcher without deleting -
Click Save — the agent appears in the Agents table with a live status badge (green = reachable, red = unreachable)
Tip: Use the Test connection button on the edit form to verify URL and secret before saving.
Once two or more agents are configured, a dropdown selector appears at the top of the sidebar (above the navigation groups). Select the desired agent — the page reloads and all data (jobs, timeline, monitor, export) reflects the newly selected agent.
The selection is per-user and per-session. It does not affect other logged-in users.
- The last agent cannot be deleted. At least one agent must remain to keep the web UI functional.
- Disabling an agent (
Enabled = unchecked) removes it from the switcher but keeps its configuration. Re-enable it at any time. - HMAC secrets are independent per agent. Rotating a secret requires updating it in both the agent's config and the web UI's agent record.
- Existing installs upgrade automatically. On first start after upgrading to v4.0.0 the web UI creates the
agentstable and seeds a "Default" agent entry from the existingagent.*values inconfig.json. No manual migration step is required.
Maintenance windows let you mark scheduled time slots as off-limits for job execution. They are managed via Maintenance in the navigation bar (admin only).
Every time the agent service starts, a cleanup script (startup-cleanup.php) runs before the HTTP server accepts requests. It scans execution_log for records still in the "running" state whose process is no longer alive and resolves them automatically:
| Target type | How checked |
|---|---|
local with a stored PID |
posix_kill($pid, 0) — process existence verified; alive processes are left untouched |
local without a PID |
Assumed dead after a restart — marked interrupted |
| Remote SSH targets | PID is on the remote host; assumed dead — marked interrupted |
Cleaned-up executions receive exit_code = -5 ("Interrupted by system restart") and appear in the timeline and job detail view with a gray Interrupted badge. A 2-minute grace period prevents false positives for jobs that happened to start right as the agent restarted.
The normal use-case: a window defined for local or an SSH host alias blocks job execution on that specific target during the configured period.
Each window has:
| Field | Description |
|---|---|
| Target | local or an SSH host alias — the target this window applies to |
| Schedule | Standard 5-field cron expression for when the window starts |
| Duration | Length of the window in minutes (default: 60) |
| Description | Optional human-readable label |
| Active | Whether this window is currently evaluated |
| Setting | Behaviour |
|---|---|
| Off (default) | The job is skipped. The cron wrapper reports exit code −4 and the execution is recorded as during_maintenance = 1 |
| On | The job runs. Failures are still reported normally |
The job list and job detail pages perform an asynchronous conflict check per target:
- The next 50 upcoming run times for the job/target pair are fetched from the agent
- If 90 % or more of those runs fall inside a maintenance window, the target badge turns red ✕ ("will not be executed")
- Otherwise, if any conflict exists, the badge is amber ⚠ ("some runs may fall in a maintenance window")
Executions skipped because of a maintenance window (exit code −4) are excluded from the "recent failures" list on the dashboard. They are still visible in the Timeline and on the detail page.
Managed cron jobs can be exported from the Export page:
| Format | Description |
|---|---|
| Crontab | Plain text, one line per job/target — ready to paste into a crontab file |
| JSON | Structured data including all job fields, tags, and targets |
Both formats support filtering by Linux user and/or tag. Large exports are streamed directly to the browser without buffering in memory.
Admins can manage accounts via Users in the navigation bar.
| Action | Notes |
|---|---|
| Make Admin | Promotes a Viewer to Admin |
| Make Viewer | Demotes an Admin to Viewer |
| Delete | Permanently removes the account |
- You cannot modify or delete your own account
- SSO users are auto-created as Viewer on first login and can be promoted by an admin
- Deleting an SSO user does not revoke their OIDC provider access
Since version 4.1.0, Cronmanager exposes a versioned REST API at /api/v1/* for external
applications. Every request must carry a Bearer token generated in the web UI under
API Keys (available to every logged-in user):
Authorization: Bearer cm_<your-api-key>Access is controlled by scopes: each key is granted only the permissions it needs
(e.g. jobs:read for read-only access, jobs:write to create and edit jobs).
Keys can also be restricted by expiry date, IP whitelist, and the set of agents they may target.
For the full API reference including all endpoints, request/response examples, and security details, see API.md.
Deploy only changed files (configuration files are never overwritten):
./deploy.sh --host-agent update # host-agent mode
# or:
./deploy.sh --docker update # docker modeRestart the host agent to load code changes:
sudo systemctl restart cronmanager-agentIn docker mode, restart the agent container instead:
docker restart cronmanager-agentApply database migrations when indicated in the release notes:
ssh myserver 'docker exec -i cronmanager-db mariadb \
-u cronmanager -p<password> cronmanager \
< /opt/cronmanager/agent/sql/migrations/<migration-file>.sql'Host-agent mode:
- Check the agent is running:
sudo systemctl status cronmanager-agent curl -k https://127.0.0.1:8865/health
- Verify
agent.urlin the web config points tohttps://host.docker.internal:8865andssl_verifyisfalse - Verify the HMAC secret matches in both config files
- Inspect agent logs:
sudo journalctl -u cronmanager-agent -n 100 # or tail -f /opt/cronmanager/agent/log/cronmanager-agent.log
Docker mode:
- Check the agent container is running and healthy:
docker ps | grep cronmanager-agent docker exec cronmanager-agent curl -sk https://localhost:8865/health
- Verify
agent.urlin the web config points tohttps://cronmanager-agent:8865andssl_verifyisfalse - Verify the HMAC secret matches in both config files
- Inspect agent container logs:
docker logs cronmanager-agent
Host-agent mode:
- Verify the wrapper script is executable:
chmod +x /opt/cronmanager/agent/bin/cron-wrapper.sh
- Check the crontab for the affected user:
crontab -u <linux-user> -l
- Check the system cron log:
grep CRON /var/log/syslog | tail -50 - Test the wrapper manually:
/opt/cronmanager/agent/bin/cron-wrapper.sh <job-id> local
Docker mode:
- Verify the container crontab has entries (use Settings → Crontab Sync if empty):
docker exec cronmanager-agent crontab -l - Verify jobs have
linux_user = root(required in docker mode) - Check the cron log inside the container:
docker exec cronmanager-agent grep CRON /var/log/syslog 2>/dev/null | tail -50 # or check the agent log for execution events: docker logs cronmanager-agent | tail -50
- Test the wrapper manually inside the container:
docker exec cronmanager-agent /opt/cronmanager/agent/bin/cron-wrapper.sh <job-id> local
Multi-agent: If you use multiple agents, verify that the active agent in the sidebar is set to the correct host before checking the job list or timeline. Each agent's data is completely independent.
| Error | Cause | Fix |
|---|---|---|
cURL error 60 |
Server certificate not trusted | Set oidc_ssl_ca_bundle to your CA cert path |
cURL error 77 |
CA cert file not readable | chmod 644 /opt/cronmanager/www/conf/root_ca.crt |
Check the web log for details:
tail -f /opt/cronmanager/www/log/cronmanager-web.log- Check the MariaDB container health:
docker inspect --format='{{.State.Health.Status}}' cronmanager-db - Test connectivity:
docker exec cronmanager-db mariadb -u cronmanager -p<password> -e "SELECT 1"
- Confirm passwords match across
db.credentials, agent config, and web config
Actions like creating, editing, or deleting jobs require the Admin role. An existing admin must promote the user at Users → Make Admin.
The limit checker (check-limits.php) runs every minute via a system cron entry. If it
never produces any log output — even for jobs that visibly exceed their limit — the script
is crashing silently before it can initialise logging.
Diagnose:
# Host-agent mode: run the checker manually and look for PHP errors
sudo php /opt/cronmanager/agent/bin/check-limits.php
# Docker mode
docker exec cronmanager-agent php /opt/cronmanager/agent/bin/check-limits.phpVerify the cron entry exists:
# Host-agent mode
cat /etc/cron.d/cronmanager-limits
# Docker mode (entry is written by the entrypoint)
docker exec cronmanager-agent cat /etc/cron.d/cronmanager-limitsExpected output:
* * * * * root /usr/bin/php /opt/cronmanager/agent/bin/check-limits.php >> /dev/null 2>&1
If the file is missing, reinstall or run simple_debian_setup.sh again (it is idempotent).
Verify the checker runs and logs:
# Host-agent mode
grep "check-limits" /opt/cronmanager/agent/log/cronmanager-agent.log | tail -20
# Docker mode
docker exec cronmanager-agent grep "check-limits" \
/opt/cronmanager/agent/log/cronmanager-agent.log | tail -20If there is no output at all, the script is exiting before Bootstrap initialises the logger. Run manually (see above) to expose the underlying PHP error.
This means the notification was sent (exit code -3) but the kill call failed. The
most common causes are:
1. The job process is not a process-group leader
kill -TERM -$PID sends SIGTERM to the entire process group. This only works when
the child process was launched with setsid so that its PID equals its PGID. Jobs
started via an older wrapper script (before the setsid fix) inherit the wrapper's
process group and cannot be killed this way.
Check the wrapper script version in use:
grep -n "setsid" /opt/cronmanager/agent/bin/cron-wrapper.shIf setsid does not appear, redeploy the agent to get the current wrapper.
2. Remote SSH auto-kill: process group not created on the remote host
For SSH targets the remote command must also be launched via setsid. Check:
grep -A3 "REMOTE_PID_FILE" /opt/cronmanager/agent/bin/cron-wrapper.sh | grep setsidAgain, redeploy if setsid is absent.
3. PID file not written / not found
For SSH targets the agent reads the PID from a temporary file on the remote host. If the wrapper failed to write the file (permissions, disk space, race condition) the kill attempt will log a warning. Check the agent log around the time the limit was exceeded:
grep "auto-kill\|pid_file\|PID" /opt/cronmanager/agent/log/cronmanager-agent.log | tail -30The wrapper script's wait call returns after SIGTERM (exit 143) and calls
POST /execution/finish. If the execution row was already closed by the auto-killer
with exit code -2, the finish endpoint ignores the second update (AND finished_at IS NULL
guard). If you are seeing 0 or 143 in the UI, the running agent code predates this fix.
Redeploy agent/src/Endpoints/ExecutionFinishEndpoint.php and restart the agent.
Executions stay open (no finished_at) when the wrapper script is interrupted before
it can call POST /execution/finish — for example, if the agent restarts mid-run or
the container is recreated.
Clean up via the UI:
- Go to Settings → Stuck Executions
- Adjust the lookback threshold if needed
- Use Mark Finished (sets exit code
-1) or Delete per row, or select all and use the bulk toolbar
Clean up via SQL (emergency):
-- Mark all executions running for more than 2 hours as finished
UPDATE execution_log
SET finished_at = NOW(),
exit_code = -1,
output = CONCAT(COALESCE(output, ''), '\n[Marked finished manually]')
WHERE finished_at IS NULL
AND started_at < DATE_SUB(NOW(), INTERVAL 2 HOUR);-
Check that mail is enabled in the agent config:
grep -A10 '"mail"' /opt/cronmanager/agent/config/config.jsonmail.enabledmust betrue. -
Per-job: verify "Notify on failure / limit exceeded" is checked on the job edit page.
-
Test SMTP connectivity from the agent host:
# Replace with your SMTP host and port nc -zv smtp.example.com 587 -
Check the agent log for mail errors:
# Host-agent mode grep -i "mail\|smtp\|notification" /opt/cronmanager/agent/log/cronmanager-agent.log | tail -30 # Docker mode docker exec cronmanager-agent grep -i "mail\|smtp\|notification" \ /opt/cronmanager/agent/log/cronmanager-agent.log | tail -30
-
Check the send-notification script directly (it runs as a background process):
# Create a minimal test payload echo '{"job_id":1,"description":"Test","linux_user":"root","schedule":"* * * * *","exit_code":1,"output":"test","started_at":"2026-01-01 00:00:00","finished_at":"2026-01-01 00:01:00"}' \ > /tmp/test_notify.json php /opt/cronmanager/agent/bin/send-notification.php /tmp/test_notify.json
-
Test SSH connectivity from the agent:
# Host-agent mode ssh -o BatchMode=yes <host-alias> 'echo ok' # Docker mode docker exec cronmanager-agent ssh -o BatchMode=yes <host-alias> 'echo ok'
-
Verify the SSH config is accessible inside the container:
docker exec cronmanager-agent cat /root/.ssh/config -
Check
known_hosts— SSH silently refuses to connect to hosts not inknown_hostswhenBatchMode=yesis set:# Add the remote host key docker exec cronmanager-agent ssh-keyscan -H <hostname> >> /root/.ssh/known_hosts
Or add
StrictHostKeyChecking accept-newto the SSH config block for that host. -
Verify the crontab entry on the remote host exists after saving the job:
ssh <host-alias> 'crontab -l' # or for a specific user: ssh <host-alias> 'crontab -u <linux-user> -l'
-
Run the wrapper manually to reproduce the exact execution path:
# Host-agent mode /opt/cronmanager/agent/bin/cron-wrapper.sh <job-id> <ssh-host-alias> # Docker mode docker exec cronmanager-agent \ /opt/cronmanager/agent/bin/cron-wrapper.sh <job-id> <ssh-host-alias>
If a job marked as singleton still spawns multiple concurrent instances:
-
Verify the
singletoncolumn exists in the database:DESCRIBE cronjobs; -- Should show a 'singleton' columnIf missing, apply migration
005_singleton.sql:docker exec -i cronmanager-db mariadb -u cronmanager -p<password> cronmanager \ < /opt/cronmanager/agent/sql/migrations/005_singleton.sql
-
Verify the job was saved with the flag — re-open the job edit form and confirm the Singleton checkbox is ticked. Due to a past bug in
CronGetEndpoint, the flag was not returned on GET, causing the form to appear unchecked and re-saving to clear the value. Redeploy the current agent code to get the fix. -
Check the agent log for
409 Conflictresponses, which indicate the singleton guard is working:grep "singleton\|409\|already running" /opt/cronmanager/agent/log/cronmanager-agent.log | tail -20
# Host-agent mode – follow the log in real time
tail -f /opt/cronmanager/agent/log/cronmanager-agent.log
# Docker mode
docker exec cronmanager-agent tail -f /opt/cronmanager/agent/log/cronmanager-agent.log
# or via docker logs (combines stdout + stderr):
docker logs -f cronmanager-agent
# Note: the above docker exec command is required when using the default named volume.
# If you switched to a host bind mount (see docker-compose-full.yml), you can also use:
# tail -f /opt/cronmanager/agent/log/cronmanager-agent.log
# Temporarily increase verbosity (without restarting — change in config.json + restart)
# In config.json: "logging": { "level": "debug" }
sudo systemctl restart cronmanager-agent # host-agent
docker restart cronmanager-agent # docker mode# Host-agent mode
grep "check-limits" /opt/cronmanager/agent/log/cronmanager-agent.log | tail -50
# Docker mode
docker exec cronmanager-agent grep "check-limits" \
/opt/cronmanager/agent/log/cronmanager-agent.log | tail -50Key log lines to look for:
| Message | Meaning |
|---|---|
check-limits: starting execution-limit check |
Checker ran successfully |
check-limits: no executions exceeding their limit |
No jobs over limit at that minute |
check-limits: found executions exceeding limit |
At least one job exceeded its limit |
check-limits: auto-killed execution |
Kill succeeded |
check-limits: auto-kill did not succeed |
Kill attempt failed — see error field |
check-limits: limit-exceeded notification dispatched |
Alert email queued |