Ofelia Configuration Guide

June 25, 2026 · View on GitHub

Configuration Methods

Ofelia supports multiple configuration methods that can be used independently or combined:

  1. INI Configuration File (Traditional, static configuration)
  2. Docker Labels (Dynamic, container-specific configuration)
  3. Environment Variables (Override specific settings)
  4. Command-line Flags (Runtime overrides)

Configuration Precedence

Daemon/global settings are evaluated in the following order (highest to lowest priority):

  1. Command-line flags
  2. Environment variables
  3. INI configuration file
  4. Docker labels

Job state uses a separate precedence — for jobs with the same name across sources:

  1. API state file (--state-file) — highest; persisted API/UI mutations are authoritative for their own job names
  2. Docker labels
  3. INI configuration file

Disable flags from the state file apply on top regardless of where the underlying job came from, so an INI/label job paused via the UI stays paused after restart.

State File (--state-file)

When the daemon runs with --state-file=/path/to/state.json (or OFELIA_STATE_FILE=...), Ofelia persists API-mutated state to disk so changes survive restarts (#593):

  • Jobs created/updated via POST /api/jobs/create or /api/jobs/update are recorded on each successful call and reapplied on the next daemon boot
  • Disable flags (POST /api/jobs/disable / /enable) are persisted regardless of the job's origin — operators can pause an INI-defined job from the UI and the pause survives restart

Lifecycle:

  1. Boot loads INI + scans Docker labels (unchanged)
  2. State file loads last; persisted API-origin jobs shadow same-named INI/label jobs
  3. Persisted disable flags are applied to whatever jobs ended up in the scheduler

Delete semantics:

  • POST /api/jobs/delete is only allowed against API-origin jobs; INI/Docker-label jobs return 403 with a message naming the origin so operators know which source file to edit. (Pre-fix this handler silently removed any job from memory until the next reload — a sharp edge that #593 tightens.)
  • To suppress an INI/label job temporarily, use POST /api/jobs/disable instead — that does persist across restart.

On-disk format (JSON, atomic tmp+rename, schema-versioned):

{
  "version": 1,
  "jobs": {
    "backup-db": {
      "type": "run",
      "schedule": "@daily",
      "image": "postgres:15",
      "command": "pg_dump mydb"
    }
  },
  "disabled": ["paused-job"]
}

Operator workflow (typical):

services:
  ofelia:
    image: ghcr.io/netresearch/ofelia:latest
    command: daemon --config=/etc/ofelia/config.ini --state-file=/var/lib/ofelia/state.json
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./config.ini:/etc/ofelia/config.ini:ro
      - ofelia-state:/var/lib/ofelia    # mount-mapped so the file survives container recycle
volumes:
  ofelia-state:

Empty path = disabled (the default) preserves the pre-#593 behavior.

Hybrid Configuration (INI + Docker Labels)

A common pattern is using INI configuration for global settings (like email credentials) while using Docker labels for job definitions. This keeps sensitive credentials out of container metadata and allows dynamic job discovery.

Example Setup:

  1. Create INI config with global settings only (/etc/ofelia/config.ini):
[global]
# Email notification settings (credentials hidden from labels)
smtp-host = smtp.gmail.com
smtp-port = 587
smtp-user = notifications@example.com
smtp-password = ${SMTP_PASSWORD}
email-from = notifications@example.com
email-to = admin@example.com

# Slack notifications
slack-webhook = https://hooks.slack.com/services/XXX/YYY/ZZZ
slack-only-on-error = true

# Output settings
save-folder = /var/log/ofelia
save-only-on-error = true

# No job definitions in INI - jobs come from Docker labels
  1. Docker Compose with config volume and labels (docker-compose.yml):
version: '3.8'
services:
  ofelia:
    image: ghcr.io/netresearch/ofelia:latest
    command: daemon --config=/etc/ofelia/config.ini
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./config.ini:/etc/ofelia/config.ini:ro
      - ./logs:/var/log/ofelia
    environment:
      - SMTP_PASSWORD=${SMTP_PASSWORD}  # Injected from .env file

  database:
    image: postgres:15
    labels:
      ofelia.enabled: "true"
      # Jobs inherit email settings from INI [global] section
      ofelia.job-exec.backup.schedule: "@daily"
      ofelia.job-exec.backup.command: "pg_dump -U postgres mydb > /backup/db.sql"
      ofelia.job-exec.backup.email-to: "dba@example.com"  # Override recipient

How It Works:

  • Ofelia always reads the config file first (defaults to /etc/ofelia/config.ini)
  • Global settings (email, slack, save options) are loaded from INI
  • Docker labels provide job definitions dynamically
  • Jobs inherit global notification settings unless explicitly overridden
  • If a job is defined in both INI and labels with the same name, the INI version takes precedence

Benefits:

  • Credentials stay in config files, not exposed in docker inspect
  • Jobs can be added/removed by updating container labels
  • Global settings centralized in one place
  • Environment variable substitution for secrets (${SMTP_PASSWORD})

Note

Please check the Include stopped containers documentation below if you are using --docker-include-stopped flag.

Labels-Only Configuration (No INI File)

Ofelia can run entirely without an INI configuration file, using only Docker labels and environment variables. This is ideal for simple setups, Kubernetes environments, or when you want all configuration in one place.

When the INI file is missing or unreadable, Ofelia:

  • Logs a warning but continues running
  • Creates an empty internal configuration
  • Relies entirely on Docker labels for job definitions
  • Uses environment variables for daemon settings

Example: Pure Docker Labels Setup

version: '3.8'
services:
  ofelia:
    image: ghcr.io/netresearch/ofelia:latest
    command: daemon --docker-events
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      # Daemon settings via environment variables
      - OFELIA_LOG_LEVEL=info
      - OFELIA_ENABLE_WEB=true
      - OFELIA_WEB_ADDRESS=:8081
    labels:
      # Mark this as the Ofelia service container
      ofelia.service: "true"
      ofelia.enabled: "true"
      # Global settings via labels (on Ofelia container with ofelia.service=true)
      ofelia.slack-webhook: "https://hooks.slack.com/services/XXX/YYY/ZZZ"
      ofelia.slack-only-on-error: "true"
      # Webhook notifications (recommended over deprecated slack-webhook)
      ofelia.webhook.slack.preset: "slack"
      ofelia.webhook.slack.id: "T00000000/B00000000000"
      ofelia.webhook.slack.secret: "XXXXXXXXXXXXXXXXXXXXXXXX"
      ofelia.webhook.slack.trigger: "error"
      ofelia.webhook-webhooks: "slack"
      # job-run can be defined on Ofelia container
      ofelia.job-run.cleanup.schedule: "@daily"
      ofelia.job-run.cleanup.image: "alpine:latest"
      ofelia.job-run.cleanup.command: "echo 'Daily cleanup'"
      ofelia.job-run.cleanup.delete: "true"
    ports:
      - "8081:8081"

  app:
    image: myapp:latest
    labels:
      ofelia.enabled: "true"
      # job-exec defined on target container
      ofelia.job-exec.health.schedule: "*/5 * * * *"
      ofelia.job-exec.health.command: "curl -f http://localhost:8080/health"

Key Points:

Setting TypeConfiguration MethodNotes
Daemon optionsEnvironment variablesOFELIA_* prefix
Global notificationsLabels on Ofelia containerRequires ofelia.service=true
job-execLabels on target containerContainer auto-detected
job-run, job-local, job-service-runLabels on Ofelia containerRequires ofelia.service=true
Webhook definitionsLabels on Ofelia containerRequires ofelia.service=true

Available Environment Variables:

VariableDescriptionDefault
OFELIA_CONFIGConfig file path/etc/ofelia/config.ini
OFELIA_LOG_LEVELLogging level (DEBUG, INFO, WARNING, ERROR)INFO
OFELIA_DOCKER_FILTERDocker container filter(none)
OFELIA_POLL_INTERVALDeprecated legacy poll interval (affects config and container polling)(unset)
OFELIA_DOCKER_EVENTSUse Docker events instead of pollingtrue
OFELIA_DOCKER_NO_POLLDisable Docker pollingfalse
OFELIA_DOCKER_INCLUDE_STOPPEDInclude stopped containers when reading Docker labels (only for job-run)false
OFELIA_ENABLE_WEBEnable web UIfalse
OFELIA_WEB_ADDRESSWeb UI bind address:8081
OFELIA_WEB_AUTH_ENABLEDEnable web UI authenticationfalse
OFELIA_WEB_USERNAMEWeb UI username(none)
OFELIA_WEB_PASSWORD_HASHbcrypt hash of password(none)
OFELIA_WEB_SECRET_KEYSecret for token signing(auto-generated)
OFELIA_WEB_TOKEN_EXPIRYToken expiry in hours24
OFELIA_WEB_MAX_LOGIN_ATTEMPTSMax login attempts per minute5
OFELIA_ENABLE_PPROFEnable pprof profilingfalse
OFELIA_PPROF_ADDRESSpprof bind address127.0.0.1:8080
OFELIA_STATE_FILEJSON file persisting API-mutated jobs and disable flags across restarts (#593)(none, disabled)

Limitations of Labels-Only Configuration:

  • No environment variable substitution in label values (${VAR} won't expand)
  • Sensitive values (passwords, API keys) visible in docker inspect
  • Global notification settings require ofelia.service=true on Ofelia container
  • Per-job SMTP credentials not recommended (use INI for credentials)

When to Use Labels-Only:

  • Simple setups without email notifications
  • Slack-only notifications (webhook URL is less sensitive)
  • Development and testing environments
  • When all configuration should be in docker-compose.yml

When to Use Hybrid (INI + Labels):

  • Production environments with email notifications
  • When credentials must be protected
  • When you need environment variable substitution for secrets

Include stopped containers (OFELIA_DOCKER_INCLUDE_STOPPED, --docker-include-stopped)

You can enable include-stopped via the env var OFELIA_DOCKER_INCLUDE_STOPPED, the flag --docker-include-stopped, or in config under [docker] as include-stopped = true. Default is false.

Purpose

  • Job-run on stopped containers: This option is for running job-run jobs whose Ofelia labels are defined on stopped containers (e.g. scheduled backup, configure or migrate tasks on containers that stops after executing a task). Other job types (job-exec, job-local, job-service-run, job-compose) from stopped containers are ignored; only job-run labels are parsed on stopped containers.
  • Decentralization: Each service can own its job-run definitions via Docker labels on its own container instead of configuring them only on the Ofelia service container.

Behaviour

  • When include-stopped is true, Ofelia searches for job-run labels across all matching containers (running and stopped). Stopped containers are included in the label scan.
  • Only one definition per job-run name is kept. Definitions from running containers take precedence over definitions from stopped containers.

Recommendations

  • Set up container pruning (or a clear lifecycle for stopped containers) so that old stopped containers with Ofelia labels do not accumulate and are not used unintentionally.
  • Avoid defining the same job-run name on multiple stopped containers if you need a predictable result.
  • Prefer specifying a Docker filter (--docker-filter or [docker] filters) to limit which containers Ofelia inspects; this reduces the set of running and stopped containers considered.

INI Configuration

Environment Variable Substitution

INI config files support ${VAR} syntax for environment variable substitution. Variables are resolved before the INI file is parsed.

SyntaxBehavior
${VAR}Replaced with env value if defined and non-empty; kept as literal ${VAR} if undefined
${VAR:-default}Replaced with env value if defined and non-empty; uses default if undefined or empty
[global]
smtp-host = ${SMTP_HOST:-mail.example.com}
smtp-password = ${SMTP_PASS}

[job-run "backup"]
schedule = @daily
image = ${BACKUP_IMAGE:-postgres:15}
command = pg_dump ${DB_NAME:-mydb}

Notes:

  • Only ${VAR} syntax is supported — $VAR without braces is not substituted, keeping cron expressions and shell commands safe.
  • Substitution happens in all config values, including command. If your command uses ${VAR} shell syntax, Ofelia will substitute it before the shell sees it. Use $VAR (without braces) in commands if you want shell expansion instead of Ofelia expansion.
  • Undefined variables without a default stay as the literal string ${VAR}, making typos visible in logs.
  • Defaults can contain special characters including colons (${IMG:-nginx:1.25-alpine}) but not closing braces (}).

Tip: If you need advanced substitution features (error on undefined, conditional replacement), use Docker Compose's own variable substitution to set environment variables on the Ofelia container, then reference those variables in the INI config:

# compose.yml — Compose handles validation
services:
  ofelia:
    environment:
      - SMTP_PASS=${SMTP_PASS:?SMTP_PASS must be set}
      - DB_HOST=${DB_HOST:-localhost}
# ofelia.ini — Ofelia handles simple substitution
smtp-password = ${SMTP_PASS}

Basic Structure

[global]
# Global configuration options

[job-TYPE "NAME"]
# Job-specific configuration
# TYPE: exec, run, local, service, compose
# NAME: Unique job identifier

Global Settings

[global]
# Docker Configuration
docker-host = unix:///var/run/docker.sock
docker-poll-interval = 30s
docker-events = true
allow-host-jobs-from-labels = false
default-user = nobody        # Default for exec/run/service; empty uses container default

# How label-defined job-exec names are scoped (see "Cross-Container Job
# References" below). Default `service` is collision-prone when one daemon
# watches several Compose projects that reuse a service name; use `container`
# or `container-service` to keep them distinct.
#   service           - {service}.{job} (default; falls back to container name)
#   container         - {container}.{job} (collision-safe per Docker daemon)
#   container-service - {container}.{service}.{job} (descriptive + collision-safe)
job-exec-label-scope = service

# Notification deduplication: suppress duplicate error notifications (Slack, email,
# save) for jobs that fail repeatedly with the same error. Set to 0 (default) to
# disable deduplication and emit every notification. Accepts any Go duration
# (`5m`, `1h`, `30s`).
notification-cooldown = 0

# Notification Settings (deprecated Slack middleware - prefer the webhook system below)
slack-webhook = https://hooks.slack.com/services/XXX/YYY/ZZZ
slack-only-on-error = true

# Email Settings
email-from = ofelia@example.com
email-to = admin@example.com
smtp-host = smtp.gmail.com
smtp-port = 587
smtp-user = ofelia@example.com
smtp-password = ${SMTP_PASSWORD}  # Environment variable reference
# Skip TLS certificate verification when dialing the SMTP server. Default: false.
# WARNING: Disables MITM protection — only enable for test environments or when
# the SMTP server uses a private/internal CA that cannot be trusted system-wide.
# See docs/TROUBLESHOOTING.md for the security trade-off.
smtp-tls-skip-verify = false
# STARTTLS posture for the outbound SMTP dialer. Default: mandatory.
# Valid values:
#   mandatory     - Require STARTTLS; abort with an error if the server does not advertise it.
#                   This is the default and prevents silent cleartext fallback.
#   opportunistic - Use STARTTLS when offered, silently fall back to plaintext otherwise.
#                   Use only with a fully trusted network path (e.g. localhost relay).
#   none          - Disable STARTTLS entirely; messages and credentials sent in cleartext.
#                   Required for some test fixtures (MailHog); intentionally insecure.
# See docs/TROUBLESHOOTING.md for the rationale and the security trade-offs.
smtp-tls-policy = mandatory

# Output Settings
save-folder = /var/log/ofelia
save-only-on-error = false

# Web UI Settings
enable-web = true
# Bind to localhost by default to avoid exposing the web UI to untrusted networks.
# Change only when you have proper network access controls in place.
web-address = 127.0.0.1:8080

# Web UI Authentication (RECOMMENDED for production)
# WARNING: Disabling auth exposes /api/* endpoints including job creation/execution.
# If auth is disabled, ensure web-address is bound to localhost or a protected interface.
web-auth-enabled = true
web-username = admin
web-password-hash = \$2a\$12$...  # bcrypt hash - use 'ofelia hash-password' to generate
web-secret-key = ${WEB_SECRET_KEY}  # Required for persistent sessions across restarts
web-token-expiry = 24  # hours
web-max-login-attempts = 5
# Trusted proxy CIDRs (comma-separated). Only requests originating from these
# networks will have their X-Forwarded-For / X-Real-IP headers honored when
# determining the client IP for login rate-limiting and audit logs.
# SECURITY: leave empty if Ofelia is exposed directly (no reverse proxy) — any
# entry here lets a request from that network spoof its source IP via headers.
# Set to your reverse proxy's network only (e.g. Docker bridge, k8s pod CIDR,
# load-balancer subnet). Accepts CIDRs ("172.17.0.0/16") or single IPs.
web-trusted-proxies = 172.17.0.0/16, 10.0.0.0/8

# Monitoring
enable-pprof = false
pprof-address = :6060

# Security
enable-strict-validation = false

Job Types

ExecJob - Execute in Existing Container

Runs commands inside an already-running container.

[job-exec "database-backup"]
# Required
schedule = @midnight        # Cron expression or preset
container = postgres         # Container name or ID
command = pg_dump mydb > /backup/db.sql

# Optional
user = postgres             # User to run command as
environment = DB_NAME=mydb,BACKUP_RETENTION=7
tty = false                 # Allocate TTY
console-height = 24         # Initial pseudo-TTY rows (only honored when tty=true; #235)
console-width  = 80         # Initial pseudo-TTY columns
delay = 5s                  # Delay before execution

# Middleware Configuration
slack-webhook = https://hooks.slack.com/...
slack-only-on-error = true

email-to = dba@example.com
email-subject = Database Backup Report

save-folder = /logs/backups
save-only-on-error = false

overlap = false             # Prevent overlapping runs

RunJob - Execute in New Container

Creates a new container for each job execution.

[job-run "data-processor"]
# Required
schedule = 0 */6 * * *      # Every 6 hours
image = myapp/processor:latest

# Optional
command = process-data --mode=batch
pull = always               # always, never, if-not-present
network = backend           # Docker network
user = 1000:1000           # UID:GID or username
hostname = processor-job

# Container Configuration
environment = ENV=production,LOG_LEVEL=info
volumes = /data:/data:ro,/output:/output:rw
devices = /dev/fuse:/dev/fuse
capabilities-add = SYS_ADMIN
capabilities-drop = NET_RAW
dns = 8.8.8.8,8.8.4.4
labels = job=processor,env=prod
working-dir = /app
memory = 512m
memory-swap = 1g
cpu-shares = 512
cpu-quota = 50000

# Cleanup
delete = true               # Delete container after execution
delete-timeout = 30s        # Timeout for deletion
stop-signal = SIGINT        # Signal sent at stop time (#234); empty = image's STOPSIGNAL or SIGTERM
stop-timeout = 30s          # Grace period before SIGKILL in cleanupOnDeadline (#234); 0 = legacy 10s default

# Restart Policy
restart-on-failure = 3      # Max restart attempts

LocalJob - Execute on Host

Runs commands directly on the host machine.

[job-local "system-cleanup"]
# Required
schedule = @daily
command = /usr/local/bin/cleanup.sh

# Optional
user = maintenance          # System user
dir = /var/maintenance      # Working directory
environment = CLEANUP_DAYS=30,LOG_FILE=/var/log/cleanup.log

# Security Warning: LocalJobs run with host privileges
# Not available from Docker labels unless explicitly allowed

ServiceJob - Docker Swarm Service

Deploys as a Docker Swarm service (requires Swarm mode).

[job-service-run "distributed-task"]
schedule = @hourly
image = myapp/worker:latest
command = run-distributed-task
network = swarm_network
environment = DB_HOST=postgres
environment = DB_PORT=5432
hostname = worker-1
dir = /opt/app
user = appuser
delete = true
max-runtime = 1h

ComposeJob - Docker Compose Operations

Manages Docker Compose projects.

[job-compose "stack-restart"]
# Required
schedule = 0 4 * * *        # 4 AM daily
project = myapp             # Project name
command = restart           # Compose command

# Optional
service = web               # Specific service
timeout = 300s              # Operation timeout
dir = /opt/compose/myapp    # Working directory with docker-compose.yml
environment = COMPOSE_PROJECT_NAME=myapp

Docker Labels Configuration

Configure jobs using container labels:

Basic Label Format

labels:
  # Enable Ofelia for this container
  ofelia.enabled: "true"
  
  # Job configuration: ofelia.JOB-TYPE.JOB-NAME.PROPERTY
  ofelia.job-exec.backup.schedule: "@midnight"
  ofelia.job-exec.backup.command: "backup.sh"
  ofelia.job-exec.backup.user: "root"

Webhook Labels

Define named webhooks using Docker labels on the service container (ofelia.service: "true"):

labels:
  ofelia.enabled: "true"
  ofelia.service: "true"

  # Define a webhook: ofelia.webhook.NAME.PROPERTY
  ofelia.webhook.slack-alerts.preset: slack
  ofelia.webhook.slack-alerts.id: "T00000000/B00000000000"
  ofelia.webhook.slack-alerts.secret: "XXXXXXXXXXXXXXXXXXXXXXXX"
  ofelia.webhook.slack-alerts.trigger: error

  # Global webhook keys exposed via labels. SSRF-sensitive globals
  # (allowed-hosts, allow-remote-presets, etc.) must be set in the INI
  # [global] section. See webhooks.md and #486.
  ofelia.webhook-webhooks: "slack-alerts"
  ofelia.webhook-preset-cache-ttl: "12h"

Assign webhooks to jobs on any container:

labels:
  ofelia.enabled: "true"
  ofelia.job-exec.backup.schedule: "@daily"
  ofelia.job-exec.backup.command: "pg_dump mydb"
  ofelia.job-exec.backup.webhooks: "slack-alerts"

See Webhook Documentation for all parameters and presets.

Complete Example

version: '3.8'
services:
  database:
    image: postgres:15
    labels:
      # Enable Ofelia
      ofelia.enabled: "true"
      
      # Backup job
      ofelia.job-exec.db-backup.schedule: "0 2 * * *"
      ofelia.job-exec.db-backup.command: "pg_dump -U postgres mydb > /backup/db.sql"
      ofelia.job-exec.db-backup.user: "postgres"
      ofelia.job-exec.db-backup.environment: "PGPASSWORD=secret"
      
      # Maintenance job
      ofelia.job-exec.db-vacuum.schedule: "@weekly"
      ofelia.job-exec.db-vacuum.command: "vacuumdb --all --analyze"
      
      # Health check job
      ofelia.job-exec.db-health.schedule: "@every 5m"
      ofelia.job-exec.db-health.command: "pg_isready -U postgres"
      ofelia.job-exec.db-health.slack-only-on-error: "true"
      
  app:
    image: myapp:latest
    labels:
      ofelia.enabled: "true"
      
      # Cache warming
      ofelia.job-exec.cache-warm.schedule: "0 */4 * * *"
      ofelia.job-exec.cache-warm.command: "php artisan cache:warm"
      
      # Queue processing
      ofelia.job-exec.queue-process.schedule: "@every 1m"
      ofelia.job-exec.queue-process.command: "php artisan queue:work --stop-when-empty"

Global Settings in Docker Compose

Docker labels configure jobs and webhooks, but not global settings like logging or output storage. For global configuration in Docker Compose, use environment variables on the Ofelia container:

version: '3.8'
services:
  ofelia:
    image: ghcr.io/netresearch/ofelia:latest
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./logs:/var/log/ofelia  # Mount for save-folder
    environment:
      # Logging Configuration
      - OFELIA_LOG_LEVEL=debug            # DEBUG, INFO, WARNING, ERROR

      # Docker Settings
      - OFELIA_DOCKER_EVENTS=true         # Use events instead of polling
      - OFELIA_POLL_INTERVAL=30s          # Poll interval for labels
      - OFELIA_DOCKER_FILTER=label=monitored=true  # Filter containers

      # Web UI
      - OFELIA_ENABLE_WEB=true
      - OFELIA_WEB_ADDRESS=:8080

      # Performance Profiling
      - OFELIA_ENABLE_PPROF=false
    labels:
      ofelia.enabled: "true"
      # Job-run labels go on Ofelia container
      ofelia.job-run.cleanup.schedule: "@daily"
      ofelia.job-run.cleanup.image: "alpine:latest"
      ofelia.job-run.cleanup.command: "rm -rf /tmp/*"

Configuration by Method:

Setting TypeDocker LabelsEnvironment VariablesINI Config
Job schedules✅ Yes❌ No✅ Yes
Job commands✅ Yes❌ No✅ Yes
Log level❌ NoOFELIA_LOG_LEVELlog-level
Save folder❌ No❌ Nosave-folder
Save only on error❌ No❌ Nosave-only-on-error
Docker host❌ No❌ Nodocker-host
Web UI❌ NoOFELIA_ENABLE_WEBenable-web
Webhook definitionsofelia.webhook.NAME.*❌ No[webhook "NAME"]
Webhook assignmentofelia.job-*.NAME.webhooks❌ Nowebhooks in job section

Note: For output capture (save-folder, save-only-on-error), use an INI configuration file. These settings require file system paths and are not available via environment variables or labels.

Schedule Expressions

Cron Format

Standard cron expressions with seconds (optional):

┌───────────── second (0-59) [OPTIONAL]
│ ┌───────────── minute (0-59)
│ │ ┌───────────── hour (0-23)
│ │ │ ┌───────────── day of month (1-31)
│ │ │ │ ┌───────────── month (1-12)
│ │ │ │ │ ┌───────────── day of week (0-7, 0 and 7 are Sunday)
│ │ │ │ │ │
│ │ │ │ │ │
* * * * * *

Preset Schedules

@yearly     # Run once a year (0 0 1 1 *)
@annually   # Same as @yearly
@monthly    # Run once a month (0 0 1 * *)
@weekly     # Run once a week (0 0 * * 0)
@daily      # Run once a day (0 0 * * *)
@midnight   # Same as @daily
@hourly     # Run once an hour (0 * * * *)
@every 5m   # Run every 5 minutes
@every 1h30m # Run every 1.5 hours
@triggered  # Only run when triggered (via on-success, on-failure, or RunJob)
@manual     # Alias for @triggered
@none       # Alias for @triggered

Examples

# Every 15 minutes
schedule = */15 * * * *

# Monday to Friday at 9 AM
schedule = 0 9 * * 1-5

# First day of month at 2:30 AM
schedule = 30 2 1 * *

# Every 30 seconds
schedule = */30 * * * * *

# Complex: Every 2 hours between 8 AM and 6 PM on weekdays
schedule = 0 8-18/2 * * 1-5

Environment Variables

Override configuration using environment variables:

# Global settings
OFELIA_DOCKER_HOST=tcp://docker:2376
OFELIA_DOCKER_POLL_INTERVAL=1m
OFELIA_SLACK_URL=https://hooks.slack.com/...
OFELIA_JWT_SECRET=my-secret-key

# Job-specific (format: OFELIA_JOB_TYPE_NAME_PROPERTY)
OFELIA_JOB_EXEC_BACKUP_SCHEDULE=@hourly
OFELIA_JOB_RUN_CLEANUP_IMAGE=alpine:3.18

Command-line Flags

ofelia daemon \
  --config=/etc/ofelia/config.ini \
  --docker-host=unix:///var/run/docker.sock \
  --docker-poll-interval=30s \
  --docker-events \
  --enable-web \
  --web-address=:8080 \
  --enable-pprof \
  --pprof-address=:6060 \
  --log-level=debug

Middleware Configuration

The new webhook notification system supports multiple services (Slack, Discord, Teams, Matrix, ntfy, Pushover, PagerDuty, Gotify) with named webhooks that can be assigned to specific jobs.

[global]
webhook-allow-remote-presets = false

[webhook "slack-alerts"]
preset = slack
id = T00000000/B00000000000
secret = XXXXXXXXXXXXXXXXXXXXXXXX
trigger = error

[webhook "discord-notify"]
preset = discord
id = 1234567890123456789
secret = abcdefghijklmnopqrstuvwxyz1234567890ABCDEF
trigger = always

[job-exec "important-task"]
schedule = @daily
container = worker
command = important-task.sh
webhooks = slack-alerts, discord-notify

See Webhook Documentation for complete configuration options, all bundled presets, custom preset creation, and security considerations.

Slack Notifications (Deprecated)

Note: The slack-webhook option is deprecated. Please migrate to the new webhook notification system which provides better flexibility, multiple service support, and per-job webhook assignment.

[job-exec "important-task"]
schedule = @daily
container = worker
command = important-task.sh

# Slack settings (DEPRECATED - use [webhook "name"] sections instead)
# Only slack-webhook and slack-only-on-error are accepted by the legacy
# middleware. For channel routing, mentions, custom username/avatar, etc.,
# migrate to the webhook notification system above (preset = slack).
slack-webhook = https://hooks.slack.com/services/XXX/YYY/ZZZ
slack-only-on-error = false

Email Notifications

[job-exec "critical-job"]
schedule = @hourly
container = app
command = critical-check.sh

# Email settings
email-to = ops@example.com,alerts@example.com
email-subject = Critical Job Report
email-from = ofelia@example.com
mail-only-on-error = true

Configuration Inheritance

Notification settings support inheritance from global to job-level configuration. When a job specifies only some notification settings, the missing values are automatically inherited from the [global] section.

Inheritance Rules:

Setting TypeInherited FieldsNotes
Emailsmtp-host, smtp-port, smtp-user, smtp-password, email-from, email-to, email-subject, mail-only-on-errorSMTP connection details and behavior flags are inherited
Slackslack-webhook, slack-only-on-errorWebhook URL and behavior flags are inherited
Savesave-folder, save-only-on-errorSave folder and behavior flags are inherited

Example: Partial Override

[global]
# Define SMTP settings once
smtp-host = smtp.example.com
smtp-port = 587
smtp-user = notifications@example.com
smtp-password = ${SMTP_PASSWORD}
email-from = ofelia@example.com
email-to = ops@example.com

[job-exec "backup"]
schedule = @daily
container = postgres
command = pg_dump mydb > /backup/db.sql
# Only override error-only behavior - inherits all SMTP settings from global
mail-only-on-error = true

[job-exec "critical-check"]
schedule = @hourly
container = app
command = health-check.sh
# Override recipient - inherits SMTP settings from global
email-to = critical-alerts@example.com

Important Notes:

  • Boolean fields (mail-only-on-error, slack-only-on-error, save-only-on-error) are fully inherited from global config and can be overridden per-job in both directions (global true + job false, or global false + job true)
  • Job-level settings always take precedence over global settings when explicitly set
  • To enable notifications for a job, at minimum specify email-to or slack-webhook at either global or job level

Output Saving

[job-exec "data-export"]
schedule = @daily
container = exporter
command = export-data.sh

# Save output
save-folder = /var/log/ofelia/exports
save-only-on-error = false

# File/directory permissions for saved output (octal). Defaults are
# security-hardened: 0600 files (daemon-uid only) and 0750 folder. Widen them
# when a non-root operator or shared group must read the logs on the host.
# Accepts 0644, 0o644 or 644. Like save-folder, these are not in the global
# Docker-label allow-list, so a container cannot change the daemon-wide default.
# save-folder-mode applies only when Ofelia creates the folder; an existing
# folder keeps its current permissions (mkdir -p semantics).
save-mode = 0644          # default 0600
save-folder-mode = 0755   # default 0750

# History restoration on startup (set on the [global] section)
# - restore-history: enable/disable replaying saved executions on daemon start
#   (default: enabled when save-folder is set)
# - restore-history-max-age: only replay executions newer than this duration
#   (default: 24h)
restore-history = true
restore-history-max-age = 48h

Overlap Prevention

[job-exec "long-running"]
schedule = */10 * * * *
container = worker
command = long-task.sh

# Prevent overlapping runs
overlap = false

Job Dependencies

Ofelia supports job dependencies to create workflows where jobs can depend on other jobs, or trigger other jobs on success or failure.

Dependency Configuration

Define job execution order and conditional triggers:

[job-exec "init-database"]
schedule = @daily
container = postgres
command = /scripts/init-db.sh

[job-exec "backup-database"]
schedule = @daily
container = postgres
command = /scripts/backup.sh
# Wait for init-database to complete first
depends-on = init-database

[job-exec "process-data"]
schedule = @daily
container = worker
command = /scripts/process.sh
# Multiple dependencies (use multiple lines)
depends-on = init-database
depends-on = backup-database
# Trigger these jobs on success
on-success = notify-complete
# Trigger these jobs on failure
on-failure = alert-ops

[job-exec "notify-complete"]
schedule = @triggered
container = notifier
command = /scripts/success-notify.sh

[job-exec "alert-ops"]
schedule = @triggered
container = notifier
command = /scripts/failure-alert.sh

Note: Jobs triggered only via on-success or on-failure should use @triggered (or aliases @manual/@none). These jobs are registered but not scheduled in cron - they only run when triggered by another job or manually.

Dependency Options

OptionDescriptionExample
depends-onJobs that must complete successfully before this job runsdepends-on = init-job
on-successJobs to trigger when this job completes successfullyon-success = cleanup-job
on-failureJobs to trigger when this job failson-failure = alert-job

Docker Labels Syntax

version: '3.8'
services:
  worker:
    image: myapp:latest
    labels:
      ofelia.enabled: "true"

      # Main processing job
      ofelia.job-exec.process.schedule: "@hourly"
      ofelia.job-exec.process.command: "process.sh"
      ofelia.job-exec.process.depends-on: "setup"
      ofelia.job-exec.process.on-success: "cleanup"
      ofelia.job-exec.process.on-failure: "alert"

      # Setup job (dependency)
      ofelia.job-exec.setup.schedule: "@hourly"
      ofelia.job-exec.setup.command: "setup.sh"

      # Cleanup job (triggered on success)
      ofelia.job-exec.cleanup.schedule: "@triggered"
      ofelia.job-exec.cleanup.command: "cleanup.sh"

      # Alert job (triggered on failure)
      ofelia.job-exec.alert.schedule: "@triggered"
      ofelia.job-exec.alert.command: "alert.sh"

Cross-Container Job References (Docker Compose)

When using Docker Compose, jobs are automatically named using the service name from docker-compose.yml, not the generated container name. This enables intuitive cross-container job references:

version: '3.8'
services:
  database:
    image: postgres:15
    labels:
      ofelia.enabled: "true"
      ofelia.job-exec.backup.schedule: "@daily"
      ofelia.job-exec.backup.command: "pg_dump -U postgres mydb"
      ofelia.job-exec.backup.on-success: "app.notify"  # Reference job on 'app' service

  app:
    image: myapp:latest
    labels:
      ofelia.enabled: "true"
      ofelia.job-exec.process.schedule: "@hourly"
      ofelia.job-exec.process.command: "process.sh"
      ofelia.job-exec.process.depends-on: "database.backup"  # Wait for database backup
      ofelia.job-exec.notify.schedule: "@triggered"
      ofelia.job-exec.notify.command: "notify.sh"

Jobs are named as {service}.{job-name}:

  • database.backup - Backup job on the database service
  • app.process - Process job on the app service
  • app.notify - Notify job on the app service

For non-Compose containers (without the com.docker.compose.service label), the container name is used instead.

Collision-safe scoping for multi-project hosts (job-exec-label-scope)

The default {service}.{job} naming is load-bearing for the cross-container references above, but it is not collision-safe when one central Ofelia daemon watches several independent Compose projects that share a service name. Two stacks deployed from the same template — e.g. acme-web and globex-web, both with Compose service web and an identical ofelia.job-exec.sync-news label — both collapse to the single key web.sync-news. Only the first container (running → newest → name order) wins; the other stack's job is dropped (see #734). Whenever this happens Ofelia logs a job-exec name collision warning naming both containers and this option, so a dropped job is never lost silently — but you still need to pick a collision-safe scope below to actually run both jobs.

The [global] option job-exec-label-scope controls how job-exec names are scoped:

ValueJob nameNotes
service (default)web.sync-newsCompose service name (falls back to container name). Required for cross-container depends-on/on-success references. Collides across projects that reuse a service name.
containeracme-web.sync-newsContainer name, which Docker guarantees unique per daemon. Collision-safe.
container-serviceacme-web.web.sync-newsContainer and service name — descriptive and collision-safe. Falls back to {container}.{job} for non-Compose containers.
[global]
# One daemon serving acme-web and globex-web (both Compose service "web"):
job-exec-label-scope = container

This is a daemon-wide setting configured in the Ofelia [global] section (INI), not via container labels — a single container must not redefine how every other container's jobs are named. When you switch away from service, update any cross-container depends-on/on-success/on-failure references to use the new scoped names (e.g. acme-database.backup instead of database.backup).

Important Notes

  1. Circular dependencies are detected - Ofelia will reject configurations with circular dependency chains
  2. Dependencies must exist - Referenced jobs must be defined in the configuration
  3. All job types supported - Dependencies work across all job types (exec, run, local, service, compose)
  4. Multiple dependencies - Use multiple depends-on lines in INI format to specify multiple dependencies
  5. Service name precedence - Docker Compose service names take precedence over container names for job naming

Security Considerations

Restricting Host Jobs

[global]
# Prevent LocalJobs from Docker labels
allow-host-jobs-from-labels = false

Web UI Authentication

Ofelia's web UI supports optional authentication to protect API endpoints:

[global]
# Enable authentication
web-auth-enabled = true
web-username = admin
web-password-hash = \$2a\$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.F3V7Y8GdDmz7hG

# Token configuration
web-secret-key = ${WEB_SECRET_KEY}  # Auto-generated if not set
web-token-expiry = 24               # Hours
web-max-login-attempts = 5          # Per minute per IP

Generating a password hash:

# Using htpasswd (Apache utils)
htpasswd -bnBC 12 "" 'your-password' | tr -d ':\n'

# Using Python
python3 -c "import bcrypt; print(bcrypt.hashpw(b'your-password', bcrypt.gensalt(12)).decode())"

Authentication flow:

  1. POST /api/login with {"username":"...", "password":"..."}
  2. Receive token in response and auth_token cookie
  3. Include token as Authorization: Bearer <token> or cookie for subsequent requests
  4. POST /api/logout to invalidate token

Security features:

  • bcrypt password hashing (cost 12)
  • Rate limiting per IP
  • CSRF token protection
  • Secure cookie settings (HttpOnly, SameSite=Strict)
  • Constant-time credential comparison

Input Validation

Ofelia provides two levels of input validation:

Basic Validation (Default)

  • Cron expression validation
  • Required field checks
  • Docker image name format validation

Strict Validation (Opt-in)

Enable strict validation for security-conscious environments:

[global]
enable-strict-validation = true

When enabled, strict validation provides:

  • Command injection prevention (blocks shell metacharacters)
  • Path traversal protection (blocks ../ patterns)
  • Network restriction (blocks private IP ranges)
  • File extension filtering (blocks .sh, .exe, etc.)
  • Tool restrictions (blocks wget, curl, rsync, etc.)

Default: false (disabled)

When to enable:

  • Multi-tenant environments with untrusted users
  • Strict security compliance requirements (SOC2, PCI-DSS)
  • Public-facing job scheduling systems
  • Highly regulated environments

When to keep disabled:

  • Infrastructure automation requiring shell scripts
  • Backup operations using rsync, wget, curl
  • Jobs accessing private networks (192.168., 10., 172.*)
  • Airgapped/restricted environments with .local domains
  • Most production deployments (Ofelia runs commands inside isolated containers)

Best Practices

  1. Use environment variables for secrets

    smtp-password = ${SMTP_PASSWORD}
    jwt-secret = ${JWT_SECRET}
    
  2. Enable Docker events for real-time updates

    docker-events = true
    
  3. Set appropriate job timeouts

    delete-timeout = 30s
    
  4. Use overlap prevention for long-running jobs

    overlap = false
    
  5. Configure appropriate resource limits

    memory = 512m
    cpu-shares = 512
    
  6. Use save-only-on-error for debugging

    save-only-on-error = true
    
  7. Implement health checks

    [job-exec "health-check"]
    schedule = @every 5m
    container = app
    command = health-check.sh
    slack-only-on-error = true
    

Validation

Validate configuration before deployment:

# Validate INI file
ofelia validate --config=/etc/ofelia/config.ini

# Test specific job
ofelia test --config=/etc/ofelia/config.ini --job=backup

# Dry run (show what would be executed)
ofelia daemon --config=/etc/ofelia/config.ini --dry-run

See also: API Documentation | CLI Package | Project Index