Standard cron works fine, but if you're running a Docker-based setup, Ofelia is a cleaner alternative. It's a Docker-native job scheduler that reads job definitions from Docker container labels — no crontab files, no editing shell configs. Add a label to a Docker container and Ofelia runs that container's command on your schedule.
It also includes a web UI showing job execution history, success/failure status, and output logs. If you manage a Docker Compose stack with multiple scheduled tasks, Ofelia keeps everything in one place.
I run Ofelia on Tencent Cloud Lighthouse alongside my Docker Compose stack. It's extremely lightweight — a few MB of memory. The combination of Ofelia + Lighthouse works well because the server runs continuously, which means time-triggered jobs (backups at 2am, weekly reports) execute on schedule without needing your laptop on. Lighthouse's OrcaTerm terminal also lets you check Docker logs to see Ofelia's job execution output from any browser, which is useful for verifying that scheduled tasks are running correctly.
- Key Takeaways
| Feature | Ofelia | Standard Cron |
|---|---|---|
| Configuration | Docker labels or YAML | Crontab file |
| Job visibility | Web UI with history | Log files only |
| Docker integration | Native (runs containers) | Manual docker exec |
| Success/failure tracking | Built-in | Parse logs manually |
| Email/Slack notifications | Built-in | Manual scripting |
| Execution logs | Stored and viewable | Redirect to files |
Use Ofelia when: you're already using Docker Compose and want job scheduling integrated with your container setup.
Stick with cron when: you have simple scripts that don't need Docker, or you prefer traditional Unix tools.
| Requirement | Details |
|---|---|
| Server | Ubuntu 22.04 with Docker |
| Docker Compose | v2.x |
Create or update your /opt/mystack/docker-compose.yml:
version: "3.8"
services:
ofelia:
image: mcuadros/ofelia:latest
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
command: daemon --docker
labels:
ofelia.enabled: "true"
# Your web application
webapp:
image: your-app:latest
labels:
# Run database backup every day at 2 AM
ofelia.enabled: "true"
ofelia.job-exec.db-backup.schedule: "@daily"
ofelia.job-exec.db-backup.command: "/app/scripts/backup.sh"
# Nginx
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
# A dedicated container for scheduled jobs
scheduler-tasks:
image: alpine:latest
command: tail -f /dev/null # Keep container running
volumes:
- /opt/scripts:/scripts
labels:
ofelia.enabled: "true"
# Task 1: Log cleanup every Sunday at 3 AM
ofelia.job-exec.cleanup-logs.schedule: "0 3 * * 0"
ofelia.job-exec.cleanup-logs.command: "sh -c 'find /scripts/logs -mtime +7 -delete && echo Cleanup done'"
# Task 2: Health check every 5 minutes
ofelia.job-exec.health-check.schedule: "@every 5m"
ofelia.job-exec.health-check.command: "wget -q -O /dev/null http://webapp:3000/health"
Start the stack:
cd /opt/mystack
docker compose up -d
docker compose logs -f ofelia
Ofelia reads job definitions from container labels in this format:
ofelia.job-exec.[JOB_NAME].[SETTING]: value
# Standard cron (minute hour day month weekday)
ofelia.job-exec.myjob.schedule: "30 2 * * *" # Every day at 2:30 AM
# Special keywords
ofelia.job-exec.myjob.schedule: "@hourly"
ofelia.job-exec.myjob.schedule: "@daily"
ofelia.job-exec.myjob.schedule: "@weekly"
ofelia.job-exec.myjob.schedule: "@monthly"
# Intervals
ofelia.job-exec.myjob.schedule: "@every 5m"
ofelia.job-exec.myjob.schedule: "@every 1h"
ofelia.job-exec.myjob.schedule: "@every 30s"
labels:
ofelia.enabled: "true"
# Job name: wordpress-backup
ofelia.job-exec.wordpress-backup.schedule: "@daily"
ofelia.job-exec.wordpress-backup.command: "sh /scripts/backup-wordpress.sh"
# Email notification on failure
ofelia.job-exec.wordpress-backup.on-error: "send_email"
ofelia.job-exec.wordpress-backup.smtp-host: "smtp.sendgrid.net"
ofelia.job-exec.wordpress-backup.smtp-port: "587"
ofelia.job-exec.wordpress-backup.email-to: "you@example.com"
ofelia.job-exec.wordpress-backup.email-from: "ofelia@yourdomain.com"
Ofelia's web UI runs on port 8080 by default. Enable it in the Ofelia service:
services:
ofelia:
image: mcuadros/ofelia:latest
restart: always
ports:
- "127.0.0.1:8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
command: daemon --docker
View via SSH tunnel:
ssh -L 8080:localhost:8080 ubuntu@YOUR_SERVER_IP
Open http://localhost:8080 — you'll see:
job-exec (most common) — runs a command inside an existing running container:
ofelia.job-exec.cleanup.schedule: "@daily"
ofelia.job-exec.cleanup.command: "rm -rf /tmp/cache/*"
job-run — starts a new container instance to run the command:
ofelia.job-run.report.schedule: "@weekly"
ofelia.job-run.report.image: "python:3.10-slim"
ofelia.job-run.report.command: "python /opt/scripts/weekly_report.py"
ofelia.job-run.report.volume: "/opt/scripts:/opt/scripts"
ofelia.job-run.report.environment: "DB_URL=postgresql://user:pass@db/myapp"
job-local — runs a command directly on the host:
ofelia.job-local.host-backup.schedule: "@daily"
ofelia.job-local.host-backup.command: "tar -czf /backups/data-$(date +%Y%m%d).tar.gz /opt/data"
For complex setups, use a YAML config file:
nano /opt/mystack/ofelia.yaml
[job-exec "db-backup"]
schedule = @daily
container = mystack-db-1
command = /scripts/backup.sh
[job-exec "log-cleanup"]
schedule = 0 3 * * 0
container = mystack-webapp-1
command = find /app/logs -mtime +7 -delete
[job-run "weekly-report"]
schedule = @weekly
image = python:3.10-slim
command = python /opt/report.py
volume = /opt/scripts:/opt
Update Ofelia to use the config file:
services:
ofelia:
image: mcuadros/ofelia:latest
command: daemon --config=/etc/ofelia/config.yaml
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /opt/mystack/ofelia.yaml:/etc/ofelia/config.yaml
Expose the web UI securely:
sudo nano /etc/nginx/sites-available/ofelia
server {
listen 80;
server_name jobs.yourdomain.com;
auth_basic "Ofelia Job Scheduler";
auth_basic_user_file /etc/nginx/.htpasswd;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
sudo htpasswd -c /etc/nginx/.htpasswd admin
sudo ln -s /etc/nginx/sites-available/ofelia /etc/nginx/sites-enabled/
sudo certbot --nginx -d jobs.yourdomain.com
My Ofelia jobs were failing silently. The web UI showed them as "Success" but the commands weren't doing anything — no files created, no logs written.
The issue: I was using job-exec to run commands in a container, but the container I was targeting had a different actual name than what I specified.
Docker Compose names containers as projectname-servicename-1 (or with variations). My label said container = myapp but the actual container name was mystack-myapp-1.
How to find the actual container name:
docker ps --format "{{.Names}}"
# Shows: mystack-webapp-1, mystack-db-1, mystack-ofelia-1
The fix: Match the container name exactly:
labels:
ofelia.job-exec.backup.container: "mystack-webapp-1" # Exact name from docker ps
Or use a job-local job to avoid container name confusion when running scripts on the host.
Also: Ofelia needs to be restarted after changing container labels:
docker compose restart ofelia
| Issue | Likely Cause | Fix |
|---|---|---|
| Jobs not appearing | Labels not picked up | Check docker compose logs ofelia; restart Ofelia |
| "Container not found" | Wrong container name | Run docker ps --format "{{.Names}}" to find exact name |
| Job shows "Success" but did nothing | Script error silently caught | Check command directly: docker exec CONTAINER your-command |
| Web UI empty | Port not mapped | Add ports: - "127.0.0.1:8080:8080" to Ofelia service |
| Schedule not matching | Timezone mismatch | Set TZ=America/New_York in Ofelia environment |
| Email notifications not sent | SMTP config | Test SMTP separately; check Ofelia logs for auth errors |
✅ What you built:
Ofelia makes scheduled jobs first-class citizens in your Docker setup. Adding a new scheduled task is as simple as adding two label lines to a container definition.
What's the difference between Ofelia and built-in Linux cron?
Linux cron runs commands on a schedule. Ofelia provides additional capabilities like visual management, dependency handling, error notifications, and often Docker-native integration.
How do I debug a failing Ofelia task?
Check the execution logs first. Verify the command works when run manually with the same user/environment. Common issues: incorrect paths, missing environment variables, permission problems.
How do I make Ofelia tasks resilient to failures?
Implement retry logic, alert on failures (email/Slack notification), and log output to a file. For critical tasks, consider writing a simple success/failure status to a monitoring endpoint.
What happens if the server restarts — do scheduled tasks continue?
If configured as a systemd service (as shown in this guide), Ofelia restarts automatically on server reboot and resumes its schedule. Configure Restart=on-failure for crash recovery.
systemctl or its own web interface. For important tasks, write output to a log file and use a monitoring tool to check for failures.👉 Get started with Tencent Cloud Lighthouse
👉 View current pricing and launch promotions
👉 Explore all active deals and offers