Skip to main content

On-start scripts

onstart is a bash script that runs once, the first time an instance starts. It is where you install packages, pull weights, clone a repository or warm a cache — the setup you would otherwise redo by hand after every deploy.

Paste it into On-start script on the template form, or set onstart over the API.

Where it runs, and where it does not

Launch modeBehavior
sshOn a Superheat base image, runs once after sshd and Jupyter have started. On a third-party image, exported only
jupyterSame as ssh
vmWritten into the guest by cloud-init and run once at first boot
argsExported, not executed by us — delivered as SUPERHEAT_ONSTART, and run only if the image reads it

In args mode the image's own entrypoint owns the process, and that is deliberate: swapping in the Superheat entrypoint there would discard your command, which is the whole content of an args template. So the script is handed to the container as SUPERHEAT_ONSTART and nothing further is done with it.

What decides whether it runs is the image, not the mode. A Superheat base image reads SUPERHEAT_ONSTART from its own entrypoint, runs the script once, and then execs your command — so an args template on one of ours does run it. A third-party image has never heard of the variable and ignores it, in ssh mode as much as in args. If your script is not running, move the template onto a Superheat base image, bake the setup into your own image, or make it the first thing your command does.

It arrives as an environment variable

The script body is handed to the container as the SUPERHEAT_ONSTART environment variable. It is never mounted as a file from the host. The base image entrypoint reads that variable, writes the body to /var/lib/superheat/onstart.sh with mode 0700, and runs it. The bare name ONSTART is accepted as an alias for compatibility with images that already read it.

Four things follow from that, and all four bite people who assume a file mount.

The body is delivered verbatim, so you escape nothing for Superheat's sake. Quotes, backslashes, newlines, heredocs and $ all reach the container exactly as you typed them. Write the script as you would write it in an editor, not as you would write it inside a bash -c "…" argument. Variable expansion happens inside the container, when bash runs the file, against the container's environment — not before.

The script is run with bash, so a shebang is decoration. #!/usr/bin/env python3 at the top does not make it a Python script; it makes it a bash script whose first line is a comment, and the next line is a syntax error. To run something other than bash, have the script write the payload out and invoke the right interpreter.

Length is bounded. The template field caps onstart at 65,536 characters, which is not a place for a large payload. If your setup is longer than a screen or two, put it in a repository and have the on-start script fetch and run it — that also gives you version control over the part that changes most.

It is not readable from your shell. The entrypoint deliberately excludes SUPERHEAT_ONSTART from the environment it exports to login shells, along with the Jupyter token and the SSH key variables. Echoing $SUPERHEAT_ONSTART over SSH returns nothing. Read /var/lib/superheat/onstart.sh instead if you need to see what ran.

Running once means once

Success or failure, the entrypoint writes a sentinel at /var/lib/superheat/.onstart-done and never runs the script again on that container. Stopping and starting an instance re-runs the entrypoint, but the sentinel makes it skip the on-start body, so a partially applied setup is not silently retried on top of itself.

If your script fails, the container stays up. The failure is logged, not fatal, precisely so you can SSH in and fix it by hand. Two log destinations matter:

cat /var/log/superheat/onstart.log # your script's own output

The entrypoint's summary line, including the exit code, also reaches the instance logs in the console. See Logs.

Because sshd starts before the on-start script, you can connect while it is still running and watch it:

tail -f /var/log/superheat/onstart.log

To re-run it after a fix, delete the sentinel and run the script yourself:

rm -f /var/lib/superheat/.onstart-done
bash /var/lib/superheat/onstart.sh

A worked example

This one installs dependencies, fetches a model with the token supplied as a secret environment variable, and leaves a marker so you can tell at a glance whether it finished.

set -euo pipefail

echo "onstart: $(date -u +%FT%TZ) on ${GPU_COUNT} GPU(s)"

pip install --no-cache-dir -r /workspace/requirements.txt

# HF_TOKEN comes from a template env entry marked secret, or from
# env_overrides at deploy time. Never hard-code it here.
if [ -n "${HF_TOKEN:-}" ]; then
huggingface-cli download meta-llama/Llama-3-8B \
--local-dir /workspace/models/llama-3-8b
else
echo "HF_TOKEN unset — skipping model download" >&2
fi

date -u +%FT%TZ > /workspace/.setup-complete
echo "onstart: done"

Points worth copying: set -euo pipefail so a failed step stops the script instead of leaving a half-built environment, ${HF_TOKEN:-} so an unset variable does not abort under set -u, and writing everything into /workspace so it survives a stop and start.

Never put a credential in the on-start script

A public template's on-start script is readable by every Superheat user, and a shared recipe carries it verbatim. Put tokens in environment entries marked secret and read them from the script, as above. See Environment variables.

Editing it later

onstart is part of the launch recipe, so changing it re-mints the template's hash_id. Instances already running keep the script they launched with — an edit only reaches instances deployed afterwards.