Skip to content

Repository files navigation

Cronmanager

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.


Support me

ko-fi


Table of Contents

  1. Features
  2. Architecture Overview
  3. Docker Hub – Recommended Installation
  4. Prerequisites
  5. Guided Setup (Alternative)
  6. Quick Start
  7. Detailed Installation
  8. OIDC / SSO Setup with Authentik
  9. Configuration Reference
  10. Failure Alerts (Email & Telegram)
  11. Multi-Host Execution
  12. Crontab Import
  13. Settings
  14. Multi-Agent Setup
  15. Maintenance Windows
  16. Export
  17. User Management
  18. External REST API
  19. Updating
  20. Troubleshooting

Features

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

Architecture Overview

Cronmanager supports two deployment modes.

Host-agent mode

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.

Docker mode

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.


Docker Hub – Recommended Installation

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.

What you need

Requirement Notes
Docker + Docker Compose v2 Any recent Linux host
5 environment variables See table below

Three-step setup

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)
EOF

Tip: run openssl rand -hex 32 separately and paste the output into AGENT_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.yml

Step 3 – Start the stack

docker compose -f docker-compose-full.yml up -d

Open http://<your-host>:8880/ — the setup wizard appears on first visit and lets you create the initial admin account.

What the stack creates

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.yml contains the required lines as commented-out alternatives — see the volumes: section of cronmanager-agent and cronmanager-web.

Available image tags

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 :dev tag is overwritten on every push to any active development branch. It may contain incomplete features, breaking changes, or unstable code. Never use :dev in 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.0

No host directory mounts are needed. Optional host-path alternatives are available as commented-out lines in docker-compose-full.yml.

Environment variables reference

Required (both containers share AGENT_HMAC_SECRET and DB_PASSWORD)

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)

Agent container optional variables

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.

Web container optional variables

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)

Updating to a new release

docker compose -f docker-compose-full.yml pull
docker compose -f docker-compose-full.yml up -d

The agent container automatically applies any new SQL migrations on startup.

SSH keys for remote job execution and crontab import

The docker-compose-full.yml mounts /root/.ssh from the Docker host into the agent container by default:

- /root/.ssh:/root/.ssh:ro

This 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/.ssh exposes 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.

Alternative: agent-specific SSH directory

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/config

Then in docker-compose-full.yml, replace the default mount with:

- /opt/cronmanager/.ssh:/root/.ssh:ro

Reaching the Docker host itself

If 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.

  1. Add the host to the SSH config (/root/.ssh/config or /opt/cronmanager/.ssh/config):

    Host dockerhost
        HostName host.docker.internal
        User root
        IdentityFile ~/.ssh/id_ed25519
        BatchMode yes
        ConnectTimeout 10
    

    host.docker.internal resolves to the Docker host's gateway IP inside the container. Add it to the agent service in docker-compose-full.yml if not already present:

    extra_hosts:
      - "host.docker.internal:host-gateway"
  2. 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
  3. Add StrictHostKeyChecking accept-new to the SSH config:

    Without this, SSH will silently refuse to connect when the host key is not yet in known_hosts (because BatchMode yes suppresses all interactive prompts). Add the line to the dockerhost block in /root/.ssh/config:

    Host dockerhost
        HostName host.docker.internal
        User root
        IdentityFile ~/.ssh/id_ed25519
        BatchMode yes
        ConnectTimeout 10
        StrictHostKeyChecking accept-new
    
  4. Add the host key to known_hosts:

    host.docker.internal only resolves inside Docker containers — not on the Docker host itself — so ssh-keyscan cannot be run directly on the host. Instead, run it from inside the agent container and redirect the output to the host's known_hosts file (the >> executes in the host shell):

    docker exec cronmanager-agent ssh-keyscan -H host.docker.internal \
        >> /root/.ssh/known_hosts

    Why this works: ssh-keyscan runs inside the container where host.docker.internal resolves 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.

  5. 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.


Prerequisites

Component Requirement
Docker + Docker Compose v2.0 or later
PHP on the host 8.4 with extensions: cli, json, pdo_mysql, openssl, mbstringhost-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 as php:8.4-fpm combined with a separate Nginx container, or community images like webdevops/php-nginx:8.4, are equally valid. Update the image: field in docker-compose.yml accordingly.

APCu extension: The swimlane view uses APCu for in-memory caching of pre-computed cron fire-time patterns. The cs1711/cs_cronmanagerweb image includes php8.4-apcu out of the box. If you use a custom base image, install the php8.4-apcu package (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'));"

Guided Setup (Alternative)

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.

One-command download and run

curl -fsSL https://raw.githubusercontent.com/csoscd/cronmanager/main/simple_debian_setup.sh | sudo bash

Note: Piping directly into bash is 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

What the script covers

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.


Quick Start

# 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 account

Detailed Installation

Step 1 – Install PHP and shared libraries on the host

Cronmanager 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 openssl

Install Composer (if not already present):

curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer

Install 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-autoloader

The resulting /opt/phplib/vendor/autoload.php is used by both the host agent and the web container.


Step 2 – Deploy the files

Configure deployment:

cp deploy.env.example deploy.env

Edit 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 in deploy.env.

Configure database credentials:

cp db.credentials.example db.credentials

Edit db.credentials:

DB_NAME=cronmanager
DB_USER=cronmanager
DB_PASSWORD=<strong-password>
DB_ROOT_USER=root
DB_ROOT_PASSWORD=<strong-root-password>

db.credentials contains 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 mode

The script will:

  • In --host-agent mode: installs and enables the systemd service for the host agent
  • In --docker mode: 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)

Step 3 – Configure the host agent

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 32

The same hmac_secret value must appear in both the agent config and the web app config.


Step 4 – Start the host agent service

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"}

Step 5 – Configure the web application

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.url to https://cronmanager-agent:8865 instead. deploy.sh --docker full patches 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.


Step 6 – Start the Docker stack

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 -d

Option B – Portainer:

  1. Open Portainer → Stacks → Add Stack
  2. Paste the contents of docker-compose.yml
  3. Add the following environment variables:
    • DB_NAME = cronmanager
    • DB_USER = cronmanager
    • DB_PASSWORD = your password
    • DB_ROOT_PASSWORD = your root password
  4. 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'

Step 7 – First login and initial setup

Open http://<your-host>:8880/ in your browser.

If no users exist in the database yet, you are automatically redirected to the Setup wizard:

  1. Enter a username for the initial admin account
  2. Enter and confirm a password (minimum 8 characters)
  3. Click Create admin account

You are then redirected to the login page. Log in with the credentials you just created.


OIDC / SSO Setup with Authentik

Cronmanager supports Single Sign-On via any OpenID Connect provider. The following instructions use Authentik as the identity provider.

1. Create a provider in Authentik

  1. Go to Applications → Providers → Create
  2. Choose OAuth2/OpenID Connect Provider
  3. Configure the provider:
    • Name: Cronmanager
    • Client type: Confidential
    • Redirect URIs: https://cronmanager.example.com/auth/callback (replace with your actual domain — must match oidc_redirect_uri exactly)
    • Scopes: openid, email, profile
  4. After saving, note the Client ID and Client Secret

2. Create an Application in Authentik

  1. Go to Applications → Applications → Create
  2. Set a name and slug (e.g. cronmanager)
  3. Assign the provider created above
  4. Save

3. Find the Provider URL

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/

4. Configure 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-web

The login page now shows a "Login with SSO" button alongside the local login form.

5. Private CA certificates (homelab)

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.crt

Then 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": false

6. SSO user provisioning

When 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.


Configuration Reference

Web application config

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)

Agent config

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

Agent TLS

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.

Certificate

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:ro

Web container – certificate verification

Set 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.

Disabling TLS

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.


Failure Alerts (Email & Telegram)

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.

Configuring via the web UI

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_KEY on 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.

Email alerts

To enable:

  1. Set mail.enabled = true and fill in your SMTP credentials in the agent config (or set MAIL_ENABLED=true and the other MAIL_* variables in Docker Compose), or use the web UI at Settings → Agent Settings
  2. Restart the agent when using environment variables: sudo systemctl restart cronmanager-agent
  3. 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.encryption to ssl
  • Port 587 (STARTTLS) → set mail.encryption to tls

Mixing these will cause the connection to hang until the SMTP timeout is reached.

Telegram alerts

Cronmanager can also send alerts via a Telegram bot.

Prerequisites:

  1. Create a bot via @BotFather — it gives you a Bot API token

  2. 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>/getUpdates
    

    Send any message to the bot first, then call the URL — look for "chat":{"id":...} in the response.

To enable:

  1. Set telegram.enabled = true, telegram.bot_token, and telegram.chat_id in the agent config (or set TELEGRAM_ENABLED=true, TELEGRAM_BOT_TOKEN, and TELEGRAM_CHAT_ID in Docker Compose)
  2. Restart the agent
  3. 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=-1001234567890

Messages 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".


InfluxDB Metrics

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.

What is written

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.

Enable (Docker Compose / .env)

INFLUXDB_ENABLED=true
INFLUXDB_URL=http://influxdb:8086
INFLUXDB_TOKEN=your-api-token
INFLUXDB_ORG=your-org
INFLUXDB_BUCKET=cronmanager

After adding these variables, restart the agent container so the entrypoint regenerates config.json.

Grafana dashboard

An importable Grafana dashboard is included at grafana/cronmanager-overview.json.

To import:

  1. Grafana → Dashboards → Import
  2. Upload grafana/cronmanager-overview.json (or paste its JSON)
  3. Select your InfluxDB datasource when prompted for DS_INFLUXDB
  4. Set the bucket variable 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

Multi-Host Execution

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/config of 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.


Crontab Import

Existing crontab entries not managed by Cronmanager can be imported:

  1. Go to Cron Jobs → Import (admin only)
  2. Select the Linux user whose crontab to scan
  3. Click Load entries – unmanaged lines are displayed
  4. Select entries to import; optionally add a description and tags
  5. Click Import selected

After import, the original unmanaged lines are commented out in the crontab file and replaced with managed wrapper-script entries.


Reading the Crontab

Host-agent mode

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

Docker mode

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/root

Note: 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 root inside the container. Ensure every job's Linux user is set to root before running Crontab Sync.


Settings

The Settings page (/settings, admin only) provides operational tools for keeping the system healthy.

Crontab Sync

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.

Stuck Executions

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, records finished_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.

History Cleanup

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.

Performance Monitor

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.

Agents

Lists all configured remote agents with name, URL, live connection status, and edit/delete actions. See Multi-Agent Setup for details.


Multi-Agent Setup

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.

Architecture

Browser
  │
  ▼
┌──────────────────────────┐
│  Web UI (single instance) │
│  agents table (registry) │
└───┬───────────┬───────────┘
    │ HMAC      │ HMAC
    ▼           ▼
┌───────────┐ ┌───────────┐
│  Agent A  │ │  Agent B  │
│  host-1   │ │  host-2   │
│  MariaDB  │ │  MariaDB  │
└───────────┘ └───────────┘
  • The agents table 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.

Step 1 — Deploy a second agent

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":"..."}

Step 2 — Register the agent in the web UI

  1. Log in as admin

  2. Go to Settings (/settings)

  3. In the Agents section click + Add agent

  4. Fill in the form:

    Field Example Notes
    Name host-2 Display name in the sidebar switcher
    Description Production server 2 Optional
    URL https://192.168.1.20:8865 Base 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_secret
    Timeout 10 HTTP timeout in seconds
    Verify SSL certificate unchecked Uncheck for self-signed certs (default); check + supply CA bundle for private CA
    Sort order 1 Controls the dropdown order (0 = first)
    Enabled checked Uncheck to hide from the switcher without deleting
  5. 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.

Step 3 — Switch between agents

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.

Notes

  • 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 agents table and seeds a "Default" agent entry from the existing agent.* values in config.json. No manual migration step is required.

Maintenance Windows

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).

Startup Orphan Cleanup

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.

Per-target windows

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.

Defining a maintenance window

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

Per-job behaviour (run_in_maintenance flag)

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

Conflict detection

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")

Dashboard filtering

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.


Export

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.


User Management

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

External REST API

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.


Updating

Deploy only changed files (configuration files are never overwritten):

./deploy.sh --host-agent update   # host-agent mode
# or:
./deploy.sh --docker update       # docker mode

Restart the host agent to load code changes:

sudo systemctl restart cronmanager-agent

In docker mode, restart the agent container instead:

docker restart cronmanager-agent

Apply 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'

Troubleshooting

"Agent unavailable" error in the web UI

Host-agent mode:

  1. Check the agent is running:
    sudo systemctl status cronmanager-agent
    curl -k https://127.0.0.1:8865/health
  2. Verify agent.url in the web config points to https://host.docker.internal:8865 and ssl_verify is false
  3. Verify the HMAC secret matches in both config files
  4. Inspect agent logs:
    sudo journalctl -u cronmanager-agent -n 100
    # or
    tail -f /opt/cronmanager/agent/log/cronmanager-agent.log

Docker mode:

  1. Check the agent container is running and healthy:
    docker ps | grep cronmanager-agent
    docker exec cronmanager-agent curl -sk https://localhost:8865/health
  2. Verify agent.url in the web config points to https://cronmanager-agent:8865 and ssl_verify is false
  3. Verify the HMAC secret matches in both config files
  4. Inspect agent container logs:
    docker logs cronmanager-agent

Jobs are not executing

Host-agent mode:

  1. Verify the wrapper script is executable:
    chmod +x /opt/cronmanager/agent/bin/cron-wrapper.sh
  2. Check the crontab for the affected user:
    crontab -u <linux-user> -l
  3. Check the system cron log:
    grep CRON /var/log/syslog | tail -50
  4. Test the wrapper manually:
    /opt/cronmanager/agent/bin/cron-wrapper.sh <job-id> local

Docker mode:

  1. Verify the container crontab has entries (use Settings → Crontab Sync if empty):
    docker exec cronmanager-agent crontab -l
  2. Verify jobs have linux_user = root (required in docker mode)
  3. 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
  4. 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.

OIDC login fails with SSL error

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

Database connection fails

  1. Check the MariaDB container health:
    docker inspect --format='{{.State.Health.Status}}' cronmanager-db
  2. Test connectivity:
    docker exec cronmanager-db mariadb -u cronmanager -p<password> -e "SELECT 1"
  3. Confirm passwords match across db.credentials, agent config, and web config

403 Forbidden for non-admin users

Actions like creating, editing, or deleting jobs require the Admin role. An existing admin must promote the user at Users → Make Admin.


Execution limit checker produces no log output / auto-kill never fires

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.php

Verify 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-limits

Expected 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 -20

If 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.


Auto-kill fires the notification but the job keeps running

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.sh

If 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 setsid

Again, 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 -30

Job shows exit code 0 (or 143) after being auto-killed

The 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.


Jobs stuck in "running" state

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:

  1. Go to Settings → Stuck Executions
  2. Adjust the lookback threshold if needed
  3. 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);

Email alerts are not being sent

  1. Check that mail is enabled in the agent config:

    grep -A10 '"mail"' /opt/cronmanager/agent/config/config.json

    mail.enabled must be true.

  2. Per-job: verify "Notify on failure / limit exceeded" is checked on the job edit page.

  3. Test SMTP connectivity from the agent host:

    # Replace with your SMTP host and port
    nc -zv smtp.example.com 587
  4. 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
  5. 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

Remote SSH jobs are not executing

  1. 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'
  2. Verify the SSH config is accessible inside the container:

    docker exec cronmanager-agent cat /root/.ssh/config
  3. Check known_hosts — SSH silently refuses to connect to hosts not in known_hosts when BatchMode=yes is set:

    # Add the remote host key
    docker exec cronmanager-agent ssh-keyscan -H <hostname> >> /root/.ssh/known_hosts

    Or add StrictHostKeyChecking accept-new to the SSH config block for that host.

  4. 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'
  5. 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>

Singleton mode does not prevent duplicate runs

If a job marked as singleton still spawns multiple concurrent instances:

  1. Verify the singleton column exists in the database:

    DESCRIBE cronjobs;
    -- Should show a 'singleton' column

    If 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
  2. 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.

  3. Check the agent log for 409 Conflict responses, which indicate the singleton guard is working:

    grep "singleton\|409\|already running" /opt/cronmanager/agent/log/cronmanager-agent.log | tail -20

Viewing live agent activity

# 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

Checking what the execution-limit checker last did

# 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 -50

Key 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

About

WebUI + Agent to manage cron jobs.

Resources

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages