Kamal and Cron
Setting up a Cron scheduler with Kamal was harder than I would have liked it to be, here are the steps I've taken to get it working.
Background
I have a small Rails service for my family which needs to perform periodic tasks. Solid Queue can handle the queue of jobs but it (or any Active Job adapter) is not designed to run periodically.
There have been a number of requests to support it in the kamal Github which resulted in this PR from dhh. The resulting documentation page is... sparse.
Acknowledgments
This article is very heavily based on this incredibly helpful article from Glauco Custodio, which in turn is based on this article about running Cron in Docker from Jason Kulatunga. Errors and omissions are mine.
Setting it up
We need to do a few things:
- configure the kamal deployment to add a couple of new roles;
- build a crontab file with our schedule;
- create a script that Cron runs to access our environment variables;
- add some dependencies to the Docker image;
- ship it.
Kamal deployment configuration
We're going to ask kamal to deploy some additional containers. Here I'm creating two extra ones, jobs
which runs the Solid Queue worker which you can skip if it's not relevant to you, and cron
which is the actual scheduler.
This is only a little service so I'm running them all on the one physical machine.
servers:
web:
hosts:
- <ip address>
jobs:
hosts:
- <ip address>
cmd: bundle exec rake solid_queue:start
cron:
hosts:
- <ip address>
cmd: bash -c "cron -f -L 2"
deploy.yml
That's it. We're asking to start three containers with roles web
, jobs
and cron
all using the same Docker image but we're overriding the start command for jobs
and cron
.
The Cron command runs it in the foreground and sets the logging verbosity. Handy for debugging.
The crontab
This is the file which contains the schedule we want to run. Here I'm creating one job which wakes every 15 minutes and asks Active Job to schedule a task. Put it in config/crontab
*/15 * * * * /rails/bin/cron-executor.sh bin/rails runner -e production 'MyJob.perform_later'
config/crontab
We'll create the cron-executor.sh
in a second, it's going to run whatever arguments we pass to it.
A script for cron to run
This script is bin/cron-executor.sh
. I've lifted it completely from Glauco's post.
#!/bin/bash -e
PATH=$PATH:/usr/local/bin
cd /rails || exit
echo "CRON: ${@}" >/proc/1/fd/1 2>/proc/1/fd/2
exec "${@}" >/proc/1/fd/1 2>/proc/1/fd/2
bin/cron-executor.sh
Set the executable bit, chmod +x bin/cron-executor.sh
.
Dockerfile
There are a few stages in the Dockerfile and it's important that these changes go in the right stage.
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
starts to build the base image which we'll deploy- A little later there is
FROM base AS build
when we create a separate build image which will get thrown away - Farther down again is,
FROM base
when we switch back to the deployable image.
In stage 1, just before stage 2, add this block:
# Install cron
RUN apt-get update -qq \
&& apt-get install -y --no-install-recommends -y cron \
&& rm -rf /var/lib/apt/lists/* \
&& which cron \
&& rm -rf /etc/cron.*/*
I cannot (and seemingly neither can anyone else) get Cron to run without root. In stage 3, change the user to root and move the cronfile to the right place:
# USER 1000:1000
USER root
COPY config/crontab /etc/cron.d/cronfile
RUN chmod 0644 /etc/cron.d/cronfile
RUN crontab /etc/cron.d/cronfile
Dockerfile
Deploy
To ship, we need to run setup....
kamal setup
And that should be it - Cron running on host.
Member discussion