Foreman›Docs›Cloud›Run in the cloud

Run in the cloud

Cloud · page 2 of 3

The container runner and the GitHub Actions job: set-up, secrets and day-to-day control.

Foreman 0.11.05 sections

Besides a laptop, Foreman runs in two places. This page is for anyone who wants the run going while their own machine is off: it covers the container runner, the GitHub Actions job, every setting the runner reads, and the Compose and Fly.io examples.

  • A container, the main way: it runs all the time, builds until the backlog is empty, waits, and picks again, with the dashboard behind a token.
  • GitHub Actions, the lighter way: one story per scheduled job, with no machine of yours switched on.

Both sign in to Claude with ANTHROPIC_API_KEY (a Console API key, billed per token) or CLAUDE_CODE_OAUTH_TOKEN (a subscription token from claude setup-token, for your own projects only). Both need a GitHub token whose pushes start CI. foreman cloud doctor checks a machine before it starts, including that the token can read a pull request’s checks.

The Set up tab’s Cloud runner step does this for you. For GitHub Actions it keeps the run state on a branch, writes the workflow, checks the GitHub token you paste and sets the repository secrets with gh, each value on stdin. For a container it writes .env.runner (mode 600, kept out of git) and shows the docker run line to start it with. Values move only on a dashboard that listens on this computer alone.

A container or an Actions runner has no Claude Code user settings, so Claude Code’s own defaults apply there, whatever your laptop’s settings say. One you may notice: the commits and pull requests it makes get a “Co-Authored-By: Claude” line. To turn that off everywhere, put this in the project’s committed .claude/settings.json:

"attribution": { "commit": "", "pr": "", "sessionUrl": false }

In a container

The image is ghcr.io/finefactory-ai/foreman-runner:<version>, published for each release; this release’s is ghcr.io/finefactory-ai/foreman-runner:0.11.0. It holds Node 22, git, gh and Foreman’s plugin, runs as the user foreman, and has /work as its volume and home. Claude Code is Anthropic’s and is not in the image: the first time the container starts, it fetches the pinned version from npm into /work, which takes a few seconds and needs to reach registry.npmjs.org. It stays on the volume, so later starts are immediate. No secret is in the image. Its entrypoint, foreman cloud run, runs the runner checks, clones the project into /work/repo (or fetches and fast-forwards the clone already there), runs the project checks, puts Foreman’s own keys (such as RESEND_API_KEY) in .env.foreman and the project’s other keys in its env file, and starts the supervisor with the state folder at /work/state. When the backlog runs out it waits cloud.idleMinutes (10 by default) with the dashboard up, pulls the clone, reloads the config and picks again. A stop request, the FOREMAN_MAX stories or a failure end the run.

A docker stop sends SIGTERM, and the run finishes the stories in progress, then stops. A story can take a while, so give it time: --stop-timeout 900 on the docker run line, or stop_grace_period in Compose. A second SIGTERM, or SIGINT or SIGHUP, stops it at once; while the runner is idle, any signal ends it at once. A story cut off midway is resumed at the next start. The container restarts only after a failure, so a stop, from Docker or from the dashboard, stays stopped; docker start foreman starts it again. Set up › Cloud runner writes the container’s settings into .env.runner and shows the docker run line: it names the container foreman, restarts it on failure, mounts the foreman-work volume at /work, and publishes the dashboard’s port 4317 on this machine’s loopback address only.

At start the runner runs foreman cloud doctor, and stops with the reason if a check fails. Run the checks on their own with docker run --rm --env-file .env.runner --entrypoint foreman ghcr.io/finefactory-ai/foreman-runner:0.11.0 cloud doctor.

The dashboard listens beyond the container, so every request needs the token, as Authorization: Bearer <token> or, for a page in a browser, ?token=<token> on the address. To reach it from elsewhere, put it behind HTTPS (the Fly example below does) rather than publishing the port on every address.

Slot environments: env.adapter: "none" works as it is. docker and supabase need the host’s Docker: mount /var/run/docker.sock into the container (the line is in the Compose file, commented out), add the Docker CLI with Compose and, for supabase, the Supabase CLI to the image, and give the foreman user access to the socket (--group-add <the socket's group id>). A project in another language needs its toolchain: build your own image on the runner image, switch to root to install it, and switch back to foreman.

For projects with "pack": "salesforce", each release also publishes a -salesforce variant of the image. It adds sf, the pinned agent plugin, sfdx-git-delta, Code Analyzer v5, Python 3 and Java 17, at pinned versions. foreman cloud doctor checks those tools when the pack is on and stops a runner that lacks them, naming the image that has them. The Salesforce pack is a preview: its release paths pass their own tests, but no story has yet shipped through them on a live org, and the Gearset path has not run against a live Gearset pipeline.

In GitHub Actions

GitHub Actions runs Foreman on a schedule, one story per job. It needs the state on a branch (Run state on a branch), since nothing on a runner lasts beyond its job.

Set up › Cloud runner does these three for you; from a terminal:

  1. Set "state": { "store": "branch" } in foreman.config.json, and list the project’s own keys in cloud.envVars, such as ["RESEND_API_KEY"].
  2. Run foreman cloud actions init. It writes .github/workflows/foreman.yml and lists the repository secrets to add:
    • FOREMAN_GH_TOKEN: a classic token with the repo scope (add workflow if a story may change .github/workflows), or a fine-grained token for the repository with Contents and Pull requests read and write, and Actions and Commit statuses read (add Workflows read and write if a story may change .github/workflows, and Deployments read if ship.steps has deploy-github). A fine-grained token can’t read check runs, so Foreman waits on GitHub Actions jobs and commit statuses instead: a check that other apps post is not seen. If your CI is such an app, use a classic token. Pushes made with GitHub’s own job token don’t start CI, so Foreman needs this one. The job also fetches Foreman itself from Foreman’s public home with this token.
    • ANTHROPIC_API_KEY (a Console API key), or CLAUDE_CODE_OAUTH_TOKEN (from claude setup-token; a subscription is for your own projects only). Either will do.
    • each name in cloud.envVars.
  3. Commit the workflow. It runs every 30 minutes (cloud.actions.cron, five cron fields, */30 * * * * by default), and from the Actions tab, where it takes an optional story id to build next.

The file is written once; foreman cloud actions init --force rewrites it, and Foreman never writes through a link in its path. foreman doctor warns when the file is not what this Foreman writes for this config (another version, or changed by hand), and fails when a secret the workflow reads is not set on the repository.

Each job runs foreman cloud tick. It pulls the state branch (a failed pull builds nothing, rather than build on a stale copy), builds at most one story end to end (pick, build, test, review, ship, waiting for CI inside the job, with the supervisor run as --max 1 --workers 1 --once) and pushes the state back. When nothing can happen, the job ends in seconds: the run is paused, or halted, or stopped, or every story is done, blocked or waiting on another. An answered decision that puts a story back in line counts as something to do, as does a credit pause whose retry time has come. When a story was built and learning.auto is on, foreman learning tick runs in the same job, so what the story taught is learned there. Only one Foreman job runs at a time (the workflow’s concurrency group), for at most 300 minutes. On the runner, Foreman’s own keys go to .env.foreman, which no story workspace gets, and the project’s other keys to its env file, only when git ignores it; neither reaches the Claude sessions’ environment. There is no dashboard in this mode: use foreman status, foreman decide list and the other commands on your laptop, which read the branch.

foreman pause and foreman resume hold and restart the scheduled jobs. foreman stop does the same as pause here: a STOP seen at the start of a job becomes a pause once the stories in progress are finished (at once when there are none), since a plain stop would only skip one job and the next would build again; foreman resume starts the jobs again. On a private repository, GitHub bills each job as at least one minute, so a 30-minute schedule uses about 1,440 minutes a month even when idle; a longer interval costs less.

Runner settings, in full

All the container’s settings come from the environment. Pass secrets as secrets, never in the image. On your laptop, Set up › Cloud runner writes them into .env.runner from the keys saved on its other steps.

Name What it does Default Needed
FOREMAN_REPO The project, as owner/name. none yes
FOREMAN_REPO_URL Where to clone from, when it is not the GitHub address of FOREMAN_REPO. the GitHub address no
FOREMAN_BRANCH The base branch to clone and build from. main no
GH_TOKEN A GitHub token for the project repository: the same kinds as FOREMAN_GH_TOKEN above. Pushes made with it start CI. none yes
ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN The Claude sign-in: a Console API key, or a subscription token (your own projects only). The API key wins when both are set. none one of them
FOREMAN_DASHBOARD_TOKEN The dashboard listens beyond the container, so it needs a token. Make it long and random, for instance with openssl rand -hex 24. none yes
FOREMAN_WORKERS How many slots build at once, 1 to the config’s slots. the workers setting no
FOREMAN_MAX Stop after this many stories. run until stopped no
the project’s own keys The names in cloud.envVars, and any an adapter declares. Foreman’s own keys among them (RESEND_API_KEY, JIRA_CLIENT_ID, JIRA_CLIENT_SECRET) go to .env.foreman in the clone, mode 600, excluded from git through the clone’s .git/info/exclude and never put in a story workspace; the rest go to the project’s env file there, which git must ignore. None is ever logged. none as needed
FOREMAN_WORK The work folder that holds the clone and the state. The image sets it. /work no
FOREMAN_DASHBOARD_HOST The address the dashboard listens on. The image sets it to every address, which is why the token is needed. loopback no
FOREMAN_PLUGIN_DIR The Foreman plugin folder. The image sets it. set by the image no

Two settings come from the project’s foreman.config.json rather than the environment: cloud.idleMinutes (10), how long the runner waits before it looks again once the backlog is empty, and cloud.envVars, the project’s key names the runner and the workflow carry. cloud.actions.cron is the workflow’s schedule. See foreman.config.json.

Compose and Fly.io

With Docker Compose, copy .env.runner to the Compose file’s env file and bring the service up. The Compose file in Foreman’s public home is a starting point: it mounts a foreman-work volume at /work, publishes 4317 on the loopback address only, restarts on failure only (so foreman stop and docker compose stop stay stopped), and gives a stop 15 minutes of grace so the stories in progress can finish. For Acme:

services:
  foreman:
    image: ghcr.io/finefactory-ai/foreman-runner:0.11.0
    env_file: foreman.env
    volumes:
      - foreman-work:/work
      # For the docker or supabase slot environments:
      # - /var/run/docker.sock:/var/run/docker.sock
    # ports: publish 4317 on this machine's loopback address only
    restart: on-failure
    stop_grace_period: 15m

volumes:
  foreman-work:
cp .env.runner runner/foreman.env
docker compose -f runner/compose.yaml up -d
docker compose -f runner/compose.yaml logs -f

Fly.io mounts volumes owned by root, so the Fly example starts the image as root just long enough for its entrypoint to give /work to the foreman user, which then runs everything. The fly.toml names the app and the project, mounts the foreman_work volume at /work, serves the dashboard over HTTPS on port 4317 with a machine always running, gives a stop 300 seconds, and asks for a shared 2 CPU machine with 2 GB of memory. For Acme:

app = "acme-foreman"
primary_region = "iad"
kill_timeout = 300

[build]
  dockerfile = "fly.Dockerfile"

[env]
  FOREMAN_REPO = "acme/acme-app"
  FOREMAN_BRANCH = "main"

[mounts]
  source = "foreman_work"
  destination = "/work"

[http_service]
  internal_port = 4317
  force_https = true
  auto_stop_machines = "off"
  auto_start_machines = false
  min_machines_running = 1

[[vm]]
  size = "shared-cpu-2x"
  memory = "2gb"
fly apps create acme-foreman
fly volumes create foreman_work --size 10 --region iad
fly secrets set GH_TOKEN=… CLAUDE_CODE_OAUTH_TOKEN=… FOREMAN_DASHBOARD_TOKEN=…
fly deploy --local-only

--local-only builds with your own Docker. The dashboard is then at https://acme-foreman.fly.dev/?token=….

Signing in to Jira without a browser

A Jira sign-in needs a browser for Atlassian’s consent page, and a container has none. The tokens live in /work/.config/foreman, on the volume, so they survive restarts; sign in once from inside the container:

docker exec -it -w /work/repo foreman foreman jira login --paste

--paste starts no server. It prints the consent address; open it in any browser and approve. The browser then lands on the callback address on port 8765, a page that does not load, which is expected. Paste that whole address back into the command and press Enter. Its state is checked and its code swapped for tokens exactly as a normal sign-in does, and the token file is saved with mode 600. First save the two keys, JIRA_CLIENT_ID and JIRA_CLIENT_SECRET, on Set up › Jira app, and list them in cloud.envVars, before Set up › Cloud runner writes .env.runner. Stories from Jira has the rest of the Jira set-up.