Ofelia supports multiple configuration methods that can be used independently or combined:
- INI Configuration File (Traditional, static configuration)
- Docker Labels (Dynamic, container-specific configuration)
- Environment Variables (Override specific settings)
- Command-line Flags (Runtime overrides)
Configuration sources are evaluated in the following order (highest to lowest priority):
- Command-line flags
- Environment variables
- INI configuration file
- 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:
- 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- Docker Compose with config volume and labels (
docker-compose.yml):
version: '3.8'
services:
ofelia:
image: 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 recipientHow 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.
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: 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.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 Type | Configuration Method | Notes |
|---|---|---|
| Daemon options | Environment variables | OFELIA_* prefix |
| Global notifications | Labels on Ofelia container | Requires ofelia.service=true |
| job-exec | Labels on target container | Container auto-detected |
| job-run, job-local, job-service-run | Labels on Ofelia container | Requires ofelia.service=true |
| Webhook definitions | Labels on Ofelia container | Requires ofelia.service=true |
Available Environment Variables:
| Variable | Description | Default |
|---|---|---|
OFELIA_CONFIG |
Config file path | /etc/ofelia/config.ini |
OFELIA_LOG_LEVEL |
Logging level (DEBUG, INFO, WARNING, ERROR) | INFO |
OFELIA_DOCKER_FILTER |
Docker container filter | (none) |
OFELIA_POLL_INTERVAL |
Deprecated legacy poll interval (affects config and container polling) | (unset) |
OFELIA_DOCKER_EVENTS |
Use Docker events instead of polling | true |
OFELIA_DOCKER_NO_POLL |
Disable Docker polling | false |
OFELIA_DOCKER_INCLUDE_STOPPED |
Include stopped containers when reading Docker labels (only for job-run) | false |
OFELIA_ENABLE_WEB |
Enable web UI | false |
OFELIA_WEB_ADDRESS |
Web UI bind address | :8081 |
OFELIA_WEB_AUTH_ENABLED |
Enable web UI authentication | false |
OFELIA_WEB_USERNAME |
Web UI username | (none) |
OFELIA_WEB_PASSWORD_HASH |
bcrypt hash of password | (none) |
OFELIA_WEB_SECRET_KEY |
Secret for token signing | (auto-generated) |
OFELIA_WEB_TOKEN_EXPIRY |
Token expiry in hours | 24 |
OFELIA_WEB_MAX_LOGIN_ATTEMPTS |
Max login attempts per minute | 5 |
OFELIA_ENABLE_PPROF |
Enable pprof profiling | false |
OFELIA_PPROF_ADDRESS |
pprof bind address | 127.0.0.1:8080 |
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=trueon 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
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-stoppedis 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-filteror[docker]filters) to limit which containers Ofelia inspects; this reduces the set of running and stopped containers considered.
INI config files support ${VAR} syntax for environment variable substitution. Variables are resolved before the INI file is parsed.
| Syntax | Behavior |
|---|---|
${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 —$VARwithout 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}
[global]
# Global configuration options
[job-TYPE "NAME"]
# Job-specific configuration
# TYPE: exec, run, local, service, compose
# NAME: Unique job identifier[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
# Notification Settings
slack-url = https://hooks.slack.com/services/XXX/YYY/ZZZ
slack-channel = #alerts
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
# 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
# Monitoring
enable-pprof = false
pprof-address = :6060
# Security
enable-strict-validation = falseRuns 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
delay = 5s # Delay before execution
# Middleware Configuration
slack-webhook = https://hooks.slack.com/...
slack-channel = #db-alerts
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 runsCreates 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
# Restart Policy
restart-on-failure = 3 # Max restart attemptsRuns 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 allowedDeploys 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 = 1hManages 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=myappConfigure jobs using container labels:
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"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 settings
ofelia.webhooks: "slack-alerts"
ofelia.webhook-allowed-hosts: "hooks.slack.com"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.
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"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: 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 Type | Docker Labels | Environment Variables | INI Config |
|---|---|---|---|
| Job schedules | ✅ Yes | ❌ No | ✅ Yes |
| Job commands | ✅ Yes | ❌ No | ✅ Yes |
| Log level | ❌ No | ✅ OFELIA_LOG_LEVEL |
✅ log-level |
| Save folder | ❌ No | ❌ No | ✅ save-folder |
| Save only on error | ❌ No | ❌ No | ✅ save-only-on-error |
| Docker host | ❌ No | ❌ No | ✅ docker-host |
| Web UI | ❌ No | ✅ OFELIA_ENABLE_WEB |
✅ enable-web |
| Webhook definitions | ✅ ofelia.webhook.NAME.* |
❌ No | ✅ [webhook "NAME"] |
| Webhook assignment | ✅ ofelia.job-*.NAME.webhooks |
❌ No | ✅ webhooks 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.
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)
│ │ │ │ │ │
│ │ │ │ │ │
* * * * * *
@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# 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-5Override 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.18ofelia 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=debugThe 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-notifySee Webhook Documentation for complete configuration options, all bundled presets, custom preset creation, and security considerations.
Note: The
slack-webhookoption 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)
slack-webhook = https://hooks.slack.com/services/XXX/YYY/ZZZ
slack-channel = #alerts
slack-only-on-error = false
slack-mentions = @channel
slack-icon-emoji = :robot:
slack-username = Ofelia Bot[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 = trueNotification 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 Type | Inherited Fields | Notes |
|---|---|---|
smtp-host, smtp-port, smtp-user, smtp-password, email-from, email-to, email-subject, mail-only-on-error |
SMTP connection details and behavior flags are inherited | |
| Slack | slack-webhook, slack-only-on-error |
Webhook URL and behavior flags are inherited |
| Save | save-folder, save-only-on-error |
Save 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.comImportant 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-toorslack-webhookat either global or job level
[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
save-format = json # json or text
save-retention = 30d # Keep for 30 days[job-exec "long-running"]
schedule = */10 * * * *
container = worker
command = long-task.sh
# Prevent overlapping runs
overlap = falseOfelia supports job dependencies to create workflows where jobs can depend on other jobs, or trigger other jobs on success or failure.
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.shNote: Jobs triggered only via
on-successoron-failureshould 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.
| Option | Description | Example |
|---|---|---|
depends-on |
Jobs that must complete successfully before this job runs | depends-on = init-job |
on-success |
Jobs to trigger when this job completes successfully | on-success = cleanup-job |
on-failure |
Jobs to trigger when this job fails | on-failure = alert-job |
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"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 serviceapp.process- Process job on the app serviceapp.notify- Notify job on the app service
For non-Compose containers (without the com.docker.compose.service label), the container name is used instead.
- Circular dependencies are detected - Ofelia will reject configurations with circular dependency chains
- Dependencies must exist - Referenced jobs must be defined in the configuration
- All job types supported - Dependencies work across all job types (exec, run, local, service, compose)
- Multiple dependencies - Use multiple
depends-onlines in INI format to specify multiple dependencies - Service name precedence - Docker Compose service names take precedence over container names for job naming
[global]
# Prevent LocalJobs from Docker labels
allow-host-jobs-from-labels = falseOfelia'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 IPGenerating 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:
- POST
/api/loginwith{"username":"...", "password":"..."} - Receive token in response and
auth_tokencookie - Include token as
Authorization: Bearer <token>or cookie for subsequent requests - POST
/api/logoutto 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
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 = trueWhen 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
.localdomains - Most production deployments (Ofelia runs commands inside isolated containers)
-
Use environment variables for secrets
smtp-password = ${SMTP_PASSWORD} jwt-secret = ${JWT_SECRET}
-
Enable Docker events for real-time updates
docker-events = true -
Set appropriate job timeouts
delete-timeout = 30s -
Use overlap prevention for long-running jobs
overlap = false -
Configure appropriate resource limits
memory = 512m cpu-shares = 512
-
Use save-only-on-error for debugging
save-only-on-error = true -
Implement health checks
[job-exec "health-check"] schedule = @every 5m container = app command = health-check.sh slack-only-on-error = true
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-runSee also: API Documentation | CLI Package | Project Index