I am working on a new project and decided to use Rails 8 and deploying it via Kamal to see how it fares compared to Kubernetes. I love the convenience of containers and I am very pleased to see that we have sensible defaults for developing and deploying from day one.
The default setup of Rails 8 is very pleasant to work with, and even deploying your app with Kamal once you get the initial hang of it, however I stumbled into many issues when following their documentation to use Cron.
As far as the documentation states it looks as easy as:
The default setup of Rails 8 is very pleasant to work with, and even deploying your app with Kamal once you get the initial hang of it, however I stumbled into many issues when following their documentation to use Cron.
As far as the documentation states it looks as easy as:
- Writing your crontab to config/crontab
- Using a custom command to run cron on your Container
This was certainly not the case. The default Rails Dockerfile uses the non root rails user, and for reasons which I still can't understand using the Kamal documentation example successfully writes the crontab of the rails user, however it never runs any of the commands you specify. After reading through some Kamal issues and PRs I was able to come up with a solution that works well enough and even integrate the whenever gem into the process.
Here's my step by step description. I am assuming you are already familiarized with the Rails project structure, whenever, Docker and Kamal:
Make sure you install cron in your Dockerfile
I installed mine right above copying the bundle gems and the application code like so:
# Final stage for app image FROM base # Install cron RUN apt-get update -qq && \ apt-get install --no-install-recommends -y cron && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives && \ rm -rf /etc/cron.*/* # Copy built artifacts: gems, application COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" COPY --from=build /rails /rails
Create a cron_executor script
You will use this to execute anything you want in cron and I borrowed the idea from Glauco Custodio's blog. I saved it under bin/cron_executor:
#!/bin/bash -e PATH=$PATH:/usr/local/bin cd /rails || exit echo "CRON: ${@}" exec "${@}"
Make sure to chmod +x /bin/cron_executor
Create a dummy rake task for testing
I want to see the impact of my changes right away so I came up with this very simple rake task:
namespace :test do task log: :environment do puts "[#{DateTime.now}]\tRunning from Cron!" end end
Modify whenever's config/schedule.rb
You have to modify the output so whenever correctly shows the logs if you're running this on Docker, and you also need to modify how rake tasks are executed to use our previously created cron_executor script:
# Sending output to STDOUT for Docker set :output, { standard: '/proc/1/fd/1', error: '/proc/1/fd/2' } # TODO: Modify whenever's other job_types as needed, for now I'm only using rake job_type :rake, "/rails/bin/cron_executor bundle exec rake :task :output" every 3.minutes do rake "test:log" end
Modify Kamal deploy to use cron
This is the trickiest part of the process and it takes care of:
- Setting the root user's crontab using whenever (bundle exec whenever --update-crontab). This is extremely convenient since whenever is very readable.
- Copies the environment variables into /etc/environment (env > /etc/environment). This steps is necessary because as the Kamal documentation states, cron doesn't get your full environment so this is a way to inject it
- Runs cron in the foreground (cron -f)
- Runs the container as root (As stated in servers -> options -> user)
servers: cron: hosts: - cron-host.demoapp.com cmd: bash -c "bundle exec whenever --update-crontab && env > /etc/environment && cron -f" options: user: root
See everything working
Deploy your Rails application and after a couple of minutes you should see something like this on your cron server's logs (kamal app logs -r cron -f)
2025-01-20T23:30:01.577774776Z CRON: bundle exec rake test:log 2025-01-20T23:30:04.981673616Z [2025-01-20T23:30:03+00:00] Running from Cron! 2025-01-20T23:32:01.707032800Z CRON: bundle exec rake test:log 2025-01-20T23:32:05.105227021Z [2025-01-20T23:32:04+00:00] Running from Cron! 2025-01-20T23:34:01.827464354Z CRON: bundle exec rake test:log 2025-01-20T23:34:05.245636145Z [2025-01-20T23:34:04+00:00] Running from Cron! 2025-01-20T23:36:01.975579684Z CRON: bundle exec rake test:log 2025-01-20T23:36:05.351346327Z [2025-01-20T23:36:04+00:00] Running from Cron!
If you get output similar to this you're past the hurdles and ready to use cron via Kamal with confidence.
References
- Migrating From Dokku to Kamal: Scheduling Cron Jobs: Glauco Custodio's guide was extremely helpful for both structure and "putting the last nail in the coffin" using root
- Support long running cron tasks: I borrowed Jankees van Woezik's solution to update the crontab with whenever
- Support for periodic task (cron jobs): I used Bounmy Stéphane's comment about not being able to run cron as a non-root user and Mat Harvard's solution to tight everything together
Alan