Implementing Supercronic in a Django app

Final files can be found in my last comment in this thread.

Hey folks, thanks for all the comments I got on my previous question about using Supercronic. I’m walking through the tutorial and I’ve got three questions about the actual implementation, after reading the Crontab with Supercronic help article.

Calling the right command from the crontab

I’ve added the crontab file to my project, and since I want to run it every 5 minutes this is what it looks like.

*/5 * * * * python mysite/manage.py overdue_checkouts

Is that the correct command syntax, or do I need to use the full path to python? If the latter, how do I find the python path for the machine? Google says that for the docker image I’m using, 3.12-slim-bookworm, that path would be /usr/local/bin/python. So would the crontab text be?

*/5 * * * * /usr/local/bin/python mysite/manage.py overdue_checkouts

Adding the supercronic bits to my Dockerfile

I’m not terribly familiar with Docker. I’ve added the bits for supercronic, but are they in the right order?

ARG PYTHON_VERSION=3.12-slim-bookworm

FROM python:${PYTHON_VERSION}

ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/v0.2.29/supercronic-linux-amd64 \
    SUPERCRONIC=supercronic-linux-amd64 \
    SUPERCRONIC_SHA1SUM=cd48d45c4b10f3f0bfdd3a57d054cd05ac96812b


RUN mkdir -p /code

WORKDIR /code

COPY requirements.txt /tmp/requirements.txt

RUN set -ex && \
    pip install --upgrade pip && \
    pip install -r /tmp/requirements.txt && \
    rm -rf /root/.cache/

# install psycopg2 dependencies
RUN apt-get update && apt-get install -y \
    libpq-dev \
    gcc \
    && rm -rf /var/lib/apt/lists/*  # <-- Updated!

RUN curl -fsSLO "$SUPERCRONIC_URL" \
 && echo "${SUPERCRONIC_SHA1SUM}  ${SUPERCRONIC}" | sha1sum -c - \
 && chmod +x "$SUPERCRONIC" \
 && mv "$SUPERCRONIC" "/usr/local/bin/${SUPERCRONIC}" \
 && ln -s "/usr/local/bin/${SUPERCRONIC}" /usr/local/bin/supercronic

COPY . /code/
COPY crontab crontab

ENV PYTHONPATH="/code/mysite:$PYTHONPATH"

RUN python mysite/manage.py collectstatic --noinput

EXPOSE 8000

# replace demo.wsgi with <project_name>.wsgi
CMD ["gunicorn", "--bind", ":8000", "--workers", "2", "mysite.wsgi"]

Updating my fly.toml file

In the Setup a web & cron process section of the help doc, after adding the processes block to my fly.toml file, it says to “replace with the command you’re using to launch your server”.

[processes]
  # The command below is used to launch a Rails server; be sure to
  # replace with the command you're using to launch your server.
  web = "bin/rails fly:server"
  cron = "supercronic /app/crontab"

I’m not sure what command that would be. Is that the gunicorn call in my Dockerfile (snipped out here)?

CMD ["gunicorn", "--bind", ":8000", "--workers", "2", "mysite.wsgi"]

Then it says that I have to “tell Fly that your web process matches up with a service by having this under the [[services]].”

[[services]]
  processes = ["web"]

Here’s my entire fly.toml file. My services.processes property is currently set to [“app”]. Does the above statement mean that I should change “app” to “web”?

# fly.toml file generated for nashville-tabletop-day on 2023-04-10T13:44:04-05:00

app = "nashville-tabletop-day"
kill_signal = "SIGINT"
kill_timeout = 5
primary_region = "dfw"
processes = []

[env]
  PORT = "8000"

[experimental]
  auto_rollback = true

[deploy]
  release_command = "python mysite/manage.py migrate --noinput"

[[services]]
  http_checks = []
  internal_port = 8000
  processes = ["app"]
  protocol = "tcp"
  script_checks = []
  [services.concurrency]
    hard_limit = 25
    soft_limit = 20
    type = "connections"

  [[services.ports]]
    force_https = true
    handlers = ["http"]
    port = 80

  [[services.ports]]
    handlers = ["tls", "http"]
    port = 443

  [[services.tcp_checks]]
    grace_period = "1s"
    interval = "15s"
    restart_limit = 0
    timeout = "2s"

[[statics]]
  guest_path = "/code/static"
  url_prefix = "/static/"

[processes]
  # The command below is used to launch a Rails server; be sure to
  # replace with the command you're using to launch your server.
  web = "bin/rails fly:server"
  cron = "supercronic /app/crontab"

Assuming Docker is installed on your dev machine, do docker pull python:3.12-slim-bookworm, then docker run -it python:3.12-slim-bookworm sh, then in the container shell, which python. That will give you a path for your binary.

(Some distros prefer python3 as a command, feel free to try that too).

Right… It needs to match one of the keys in the [processes] stanza. This is connecting your Django server to the Fly Proxy, and thence to the public Internet.

Yes, the values in quotes under [processes] override the CMD from the Dockerfile, basically.

https://fly.io/docs/reference/configuration/#the-processes-section

Probably you also want "supercronic /code/crontab" for the cron line, since your Dockerfile copied crontab into /code/ instead of /app/ (due to WORKDIR).

Moreover, this line should be removed, since it conflicts with [processes] below.

(TOML syntax is a little strange, sometimes.)

I missed that on first reading…

Nice, thank you!

$ which python
/usr/local/bin/python

Just to be clear, does that mean that line should be removed from the Dockerfile?

Super. You can also do docker build -t mycron . from your local project folder, and assuming that builds, you can do docker run -it mycron sh. From there you have a container that appears as it would in Fly. Your pwd should be /code and your relative pathnames such as mysite/manage.py can be confirmed/amended in this shell.

Okay, I think it’s almost working. :clap:

  • The Docker image builds fine locally.
  • The app runs fine locally (started with python mysite/manage.py runserver).
  • The deploy itself was successful.
  • But results in an application error (it hit the 10 restart limit):

The following lines were found in the Fly error logs.

18:01:49 ModuleNotFoundError: No module named 'mysite.wsgi'
18:01:49 No module named 'mysite.wsgi'

I’m sure this has to do with the fly.toml file. I removed that empty processes attribute you called out, and now my processes block (at the bottom of the file) is

[processes]
  web = "gunicorn --bind :8000 --workers 2 mysite.wsgi"
  cron = "supercronic /code/crontab"

What am I missing here? The line from the Dockerfile (currently commented out) is

# CMD ["gunicorn", "--bind", ":8000", "--workers", "2", "mysite.wsgi"]

And just in case, here are the complete Dockerfile and fly.toml files

# DOCKERFILE

ARG PYTHON_VERSION=3.12-slim-bookworm

FROM python:${PYTHON_VERSION}

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/v0.2.29/supercronic-linux-amd64
ENV SUPERCRONIC=supercronic-linux-amd64
ENV SUPERCRONIC_SHA1SUM=cd48d45c4b10f3f0bfdd3a57d054cd05ac96812b
# ENV PYTHONPATH="/code/mysite:$PYTHONPATH"

RUN mkdir -p /code

WORKDIR /code

COPY requirements.txt /tmp/requirements.txt

RUN set -ex && \
    pip install --upgrade pip && \
    pip install -r /tmp/requirements.txt && \
    rm -rf /root/.cache/

# install psycopg2 dependencies
RUN apt-get update && apt-get install -y libpq-dev gcc curl && rm -rf /var/lib/apt/lists/*

RUN curl -fsSLO "$SUPERCRONIC_URL" \
 && echo "${SUPERCRONIC_SHA1SUM} ${SUPERCRONIC}" | sha1sum -c - \
 && chmod +x "$SUPERCRONIC" \
 && mv "$SUPERCRONIC" "/usr/local/bin/${SUPERCRONIC}" \
 && ln -s "/usr/local/bin/${SUPERCRONIC}" /usr/local/bin/supercronic

COPY . /code/
COPY crontab crontab

RUN python mysite/manage.py collectstatic --noinput

EXPOSE 8000

# replace demo.wsgi with <project_name>.wsgi
# CMD ["gunicorn", "--bind", ":8000", "--workers", "2", "mysite.wsgi"]

# FLY.TOML

# fly.toml file generated for nashville-tabletop-day on 2023-04-10T13:44:04-05:00

app = "nashville-tabletop-day"
kill_signal = "SIGINT"
kill_timeout = 5
primary_region = "dfw"

[env]
  PORT = "8000"

[experimental]
  auto_rollback = true

[deploy]
  release_command = "python mysite/manage.py migrate --noinput"

[[services]]
  http_checks = []
  internal_port = 8000
  processes = ["web"]
  protocol = "tcp"
  script_checks = []
  [services.concurrency]
    hard_limit = 25
    soft_limit = 20
    type = "connections"

  [[services.ports]]
    force_https = true
    handlers = ["http"]
    port = 80

  [[services.ports]]
    handlers = ["tls", "http"]
    port = 443

  [[services.tcp_checks]]
    grace_period = "1s"
    interval = "15s"
    restart_limit = 0
    timeout = "2s"

[[statics]]
  guest_path = "/code/static"
  url_prefix = "/static/"

[processes]
  web = "gunicorn --bind :8000 --workers 2 mysite.wsgi"
  cron = "supercronic /code/crontab"

Would you supply here the full docker command you used to try this locally? I am guessing that you used docker run with a custom command, which would stop the default CMD from running, and that is exactly where the problem is.

Just to be clear, if a line is commented out in a Dockerfile, it won’t do anything. I would guess that you do need this line, so un-comment and rebuild locally and retest. Then redeploy.

Not really, only that the setting in fly.toml takes precedence, when both are specified.

I usually do leave a working CMD in the Dockerfile, for reasons similar to what @halfer was saying re: local testing…

Of course.

$ docker build -f ./Dockerfile -t ntd .

$ docker image list
IMAGE                       ID             DISK USAGE   CONTENT SIZE   EXTRA
ntd:latest                  61682485a8c1        617MB             0B    U
python:3.12-slim-bookworm   9e71df4e30c4        150MB             0B

$ docker run ntd

And I can confirm that when I uncommented the gunicorn CMD line, I get the ModuleNotFoundError error.

But to clarify, when I mentioned that line earlier, those errors were coming from the Fly error logs, not my own error logs. However I can duplicate it locally now that I’ve uncommented the CMD line.

Great, so I assume you need to fix that. I don’t know what the two processes do; do you? At a guess, one is the scheduler, and the other is a web dashboard for that system. Do you need the web dashboard?

Ah, great; so the lack of command here means that your image CMD will be run. This is the correct approach to local testing.

I am not familiar with Python, but if you want gunicorn to work: a quick shufti with AI tools shows that you’ll need to look for a wsgi.py in your project, and the mysite is the folder that contains it.

Aside: you can omit this from a docker build, as it is the default image definition name.

I’m curious why this error is coming up, since it was working before the crontab changes. So I checked out my main branch and ran the following, and it started up just fine.

$ docker build -f ./Dockerfile -t ntd .
$ docker run ntd
[2026-04-12 20:11:07 +0000] [1] [INFO] Starting gunicorn 25.3.0
[2026-04-12 20:11:07 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1)
[2026-04-12 20:11:07 +0000] [1] [INFO] Using worker: sync
[2026-04-12 20:11:07 +0000] [7] [INFO] Booting worker with pid: 7
[2026-04-12 20:11:07 +0000] [8] [INFO] Booting worker with pid: 8
mysite/mysite/settings/__init__.py > ENV: production
mysite/mysite/settings/__init__.py > ENV: production
[2026-04-12 20:11:07 +0000] [1] [INFO] Control socket listening at /root/.gunicorn/gunicorn.ctl

I do have that file…from the project root, where I’m running the docker commands:

$ find . -iname wsgi.py
./mysite/mysite/wsgi.py

and the error again:

ModuleNotFoundError: No module named 'mysite.wsgi'
No module named 'mysite.wsgi'

Just a guess here, do you need two levels of mysite?

Honestly probably not? but I literally only work on this site once a year, for a specific event I’m running. My first priority is to get it fully functional. Afterwards, if I have time, I can rearrange.

Here’s my directory tree:

.
├── frontend
└── mysite
    ├── mysite
    │   ├── __pycache__
    │   └── settings
    │       ├── __pycache__
    │       ├── components
    │       └── environments
    ├── ntd
    │   ├── __pycache__
    │   ├── management
    │   │   └── commands
    │   │       ├── __pycache__
    │   │       └── data
    │   ├── middleware
    │   │   └── __pycache__
    │   ├── migrations
    │   │   └── __pycache__
    │   ├── static
    │   │   ├── css
    │   │   ├── fonts
    │   │   └── js
    │   ├── templates
    │   │   └── components
    │   ├── templatetags
    │   │   └── __pycache__
    │   └── views
    │       └── __pycache__
    └── static
        ├── css
        ├── fonts
        └── js

I will admit to being a little embarassed about the mysite folder, since I know that’s the Django default. :smiley: I’ve got lots of experience working with Django, but almost all with existing codebases. So when I started this app, I sort of left it alone.

The mysite.ntd folder is where my actual application code lives. The mysite.mysite is the Django admin.

OK, I’ve consulted AI again. Be aware this is the blind leading the blind. :eyes:


Ah! I think this will work; AI generated this advice, and I find you have this commented out in your Dockerfile anyway.

ENV PYTHONPATH="/code/mysite:$PYTHONPATH"

So, yes, the double mysite folder is probably wrong, but from a quick fix perspective, adding (uncommenting) the ENV should sort it. Python will use /code/mysite/ as a base folder, and from there it can append the second-level mysite/, and find the .py file it needs in there.

Hah. I also just figured out that missing line was the problem. I removed it because docker build was throwing a warning:

 => WARN: UndefinedVar: Usage of undefined variable '$PYTHONPATH' (line 41)

Thanks a lot for your help @halfer, appreciated. Sometimes it’s just useful to have someone looking over your shoulder…metaphorically at least. :smiley: