For a recent project I needed a cron job on Render to run some Django management commands. Here’s the approach that worked well.
Why a separate Cron Job service?#
Render’s Cron Jobs run from their own service/repo (or a prebuilt image) on a schedule you define. My project EchoEV - Sealed TCG EV Tracker uses SQLite with a persistent disk attached to the main service. Because a Cron Job deploys a separate container, it can’t share that disk directly. Instead of duplicating state, I spin up a tiny container that SSHs into the main instance and runs the command there. This requires paid services because free tiers don’t support SSH access.
Overview#
- Generate an SSH key without a passphrase.
- Add the public key to your Render account (so Render recognizes it for SSH).
- Store the private key as an environment secret for the Cron Job.
- Build a minimal image that decodes the key and runs
ssh
into your main service. - Execute your Django command remotely.
Tip: Cron Jobs can initiate outbound connections but can’t receive inbound traffic; that’s fine here because we initiate SSH from the job to the target service.
1) Create an SSH key (no passphrase)#
ssh-keygen -t ed25519 -f cronjob_key -C "cronjob-render"
This creates cronjob_key
(private) and cronjob_key.pub
(public). Never commit the private key.
2) Add the public key to Render#
Follow Render’s guide to add an SSH public key to your account. This lets the key authenticate against your services via the dashboard’s “Account Settings → SSH Public Keys”. (Adding an SSH public key to your Render account)
3) Store the private key as a secret#
To avoid multiline formatting issues, store the private key Base64-encoded in an environment variable (e.g., SSH_KEY_B64
) for the Cron Job:
# Linux:
base64 -w 0 cronjob_key > cronjob_key.b64
# macOS (no -w):
base64 < cronjob_key | tr -d '\n' > cronjob_key.b64
Create an env var SSH_KEY_B64
in the Cron Job (or an Environment Group) with the content of cronjob_key.b64
.
4) Minimal Docker image for the Cron Job#
Use a tiny image with the OpenSSH client and a small runner script.
Dockerfile
FROM alpine:3.18
RUN apk add --no-cache openssh-client
COPY run.sh /run.sh
RUN chmod +x /run.sh
CMD ["/run.sh"]
run.sh
#!/bin/sh
set -eu
# Recreate the private key from the env var
mkdir -p ~/.ssh && chmod 700 ~/.ssh
echo "$SSH_KEY_B64" | base64 -d > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
# Use the EXACT user@host from your service page: Connect → SSH
RENDER_SSH_HOST="ssh.frankfurt.render.com" # change based on your region
SERVICE_USER="your-service-slug" # you can find this on your project 'Shell' page
USER_HOST="${SERVICE_USER}@${RENDER_SSH_HOST}"
# (optional) Pre-populate known_hosts to avoid interactive prompts
ssh-keyscan -H "$RENDER_SSH_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true
# let's call our script
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no "$USER_HOST" "bash path/of/your_script.sh"
Get the correct SSH username@host from your service’s Connect → SSH tab (it looks like
YOUR_SERVICE@ssh.YOUR_REGION.render.com
). Don’t hand-type or line-wrap it. (Connecting with SSH)
5) Create the Cron Job#
- In the Render dashboard: New → Cron Job.
- Point it at this repo (Docker), pick a schedule (e.g.,
0 * * * *
). - Add the env var
SSH_KEY_B64
(from step 3). - Deploy and test with a manual run. Check logs for SSH output.
And that’s it, a lightweight, secure way to run any command on a Render schedule without duplicating state or sharing disks between services.