One container holds the whole of Factorial: the dashboard, the API, the worker and the database. You build the image on your machine, and Docker is all you need to run it.
Before you start
- Docker, started.
- The GitHub App and its private key as a file. Set up the GitHub App makes both.
- The Factorial source repository. The image builds from it. The repository is private, and the Factorial team gives you access to it. Clone it on your machine.
You do not need Python, Node or other tools on your machine. The image carries them.
Write the .env file
In the root of your clone, copy the example file:
cp .env.example .env
Set these five values. The container stops at start when one of them is empty.
| Variable | What to write |
|---|---|
FACTORIAL_API_TOKEN | Any string that is not empty. The dashboard sends it to the API |
FACTORIAL_MASTER_KEY | 32 random bytes as base64. The command is below |
FACTORIAL_GITHUB__APP_ID | The App ID, from the App settings page |
FACTORIAL_GITHUB__INSTALLATION_ID | The installation ID, from the installation URL |
FACTORIAL_RUNTIME__E2B__DEPLOYMENT | A name for this installation, for example acme-laptop. The container needs it also when you use no E2B runtime. See Codex on the host only |
Make the master key with this command, then copy the output into .env:
python3 -c "import base64,os; print(base64.b64encode(os.urandom(32)).decode())"
Set FACTORIAL_GITHUB__BOT_LOGIN also, to the login of the App, for example acme-software-factory[bot]. The container starts without it, but then every comment condition reads false.
Every other value in .env.example has a working default. The model credentials of the agents are not among them. A run reads its key from its runtime, which you create when you connect a repository.
What the container sets itself
The container exports the values below, and an exported value outranks .env. What you write for them in .env has no effect inside the container.
| Variable | Value in the container |
|---|---|
FACTORIAL_DATABASE_URL | The database that runs beside the API |
FACTORIAL_GITHUB__PRIVATE_KEY_PATH | /run/factorial/app.pem, the copy of your key |
FACTORIAL_CORS_ORIGINS | http://localhost:5173, the dashboard origin |
FACTORIAL_RUNTIME__CLAUDE_CLI__LOCAL_ROOT | /data/runs/claude_cli, on the volume |
FACTORIAL_RUNTIME__E2B__STATE_ROOT | /data/runs/e2b, on the volume |
FACTORIAL_DASHBOARD_DIR | /app/ui/dist, the dashboard the image built |
FACTORIAL_TERMINAL__BIND | 0.0.0.0, in the network of the container only |
Do not export .env. Do not start the container with --env-file .env, and do not pass a setting with -e. Every agent run inherits the environment of the worker. Mount the file instead, as the start command below does. The mount keeps the master key out of the environment of each agent. It does not keep the key from an agent on a host runtime, which runs as the same user and can read the file. Use an E2B runtime for containment. See Security.
Previews on your machine
A step preview URL must use https. A preview on your machine usually uses http. To accept http URLs, set this value in .env:
FACTORIAL_PREVIEW_ALLOW_HTTP=true
Do not set this value on a deployment. The preview must also use a different origin from the dashboard. The dashboard uses http://localhost:5173, so use a different port for the preview. Previews has the rest.
Put the private key in place
The container reads the key from ~/.config/factorial/app.pem on your machine:
mkdir -p ~/.config/factorial
install -m 0600 ~/Downloads/your-app.private-key.pem ~/.config/factorial/app.pem
Keep the mode at 0600. Never write the key into .env, and never commit it.
Build and start the container
Run these commands from the root of the repository, because the second one mounts .env from the current folder:
docker build -t factorial:local .
docker run -d --name factorial --stop-timeout 60 \
-p 127.0.0.1:8000:8000 -p 127.0.0.1:5173:5173 -p 127.0.0.1:8001:8001 \
-v factorial-data:/data \
-v "$PWD/.env:/run/secrets/factorial.env:ro" \
-v "$HOME/.config/factorial/app.pem:/run/secrets/app.pem:ro" \
factorial:local
docker logs -f factorial
The command mounts the two files read-only, maps the three ports to 127.0.0.1 only, and attaches the factorial-data volume.
Open the dashboard at http://localhost:5173. The API answers at http://localhost:8000. The terminal service listens on port 8001. Test that it started:
curl http://localhost:8001/terminal/healthz
The terminal service accepts each origin in FACTORIAL_CORS_ORIGINS as the dashboard. It also accepts the origin of FACTORIAL_AUTH__EXTERNAL_URL. Thus any local development server on one of those origins can open a terminal that types into a run.
At start, the container does these steps in order:
- It copies the two mounts to files that its own users can read.
- It tests your configuration, and stops when something is missing.
- It starts the database, and makes it when the volume is new.
- It applies the migrations.
- It starts the API, the worker, the terminal service and the dashboard.
The Terminal section
The Terminal section of a run's Output drawer needs FACTORIAL_TERMINAL__ENABLED=true in .env. The default is false, and then the service refuses each terminal.
You can open a terminal with the API token alone. Before each connection, the dashboard gets a ticket from the API. A ticket opens one terminal, one time, for 30 seconds. The service records what you type as api-token, because the token is not the name of a person.
To record your email, sign in with Google. To sign in with Google, set the FACTORIAL_AUTH__* values. .env.example tells you how.
When you change FACTORIAL_API_TOKEN, start the container again. The terminal service reads the token only when it starts. Tickets from the old token do not open a terminal.
After it starts
A new volume gives you an empty database. Connect a repository makes a project, gives it credentials and switches admission on. Use http://localhost:8000 as the API, and your FACTORIAL_API_TOKEN as the token.
The API also serves the MCP server. Claude Code and Codex on your machine can connect to it. Connect an MCP client has the steps, with these changes for the container:
- Use
http://localhost:8000/mcp/as the URL. Keep the slash at the end. - The container maps port 8000 to
127.0.0.1only, so you need no other network. - The dashboard on port 5173 does not serve
/mcp/. Do not use port 5173.
When FACTORIAL_AUTH__EXTERNAL_URL is empty, the MCP access page shows this URL. When you set it, the page shows that origin with /mcp/ at the end. Use port 8000 in that case also.
Interactive runs in the container
A claude_cli runtime can run interactive sessions in the container. The image has tmux and the claude version that the adapter pins. Set FACTORIAL_TERMINAL__ENABLED=true in .env. Then turn on Interactive sessions for the runtime on the Runtime page.
A session has a time limit. The limit starts when the session starts, and again each time a person pushes Resume in the dashboard. Work that the agent does in the session does not change the limit. At the limit, the worker stops the session, keeps its workspace, and puts factorial:interrupted on the issue. To continue the session, push Resume in the Terminal section. When you remove the label, the session does not continue.
| Variable | Default | Value |
|---|---|---|
FACTORIAL_TERMINAL__SESSION_CEILING | 4 hours | The limit, as seconds or as an ISO 8601 duration, for example PT4H |
Each interactive run has its own tmux server, apart from the worker. A restart thus has these results:
- When only the worker restarts, the session continues. The next worker finds the session and watches it again. This command restarts only the worker:
docker exec factorial /command/s6-svc -r /run/service/worker - When the container stops, each session stops.
docker stop,docker restartand a new image all stop the container. The run fails with a reason that starts withhost_lost, and the step starts again. The workspace of the run stays on the volume. - When a finish or a stop of the run starts before the container stops, the run keeps that result. Thus a run that you interrupt can continue later.
Codex on the host only
The codex_cli runtime does not run in the container. The image installs codex, but its sandbox does not start in Docker. Keep FACTORIAL_RUNTIME__KIND=claude_cli, and do not name codex_cli in FACTORIAL_RUNTIME__ROUTES. The container refuses to start when you do.
A runtime on E2B runs either CLI from the container, because the agent runs in the E2B sandbox. Create the runtime with the E2B isolation. On the Runtime page, store the key of your E2B team as its E2B_API_KEY. Then choose its template from the builds of that team. If the list is empty, the Factorial team supplies the templates for your E2B team. See Runtimes.
FACTORIAL_RUNTIME__E2B__DEPLOYMENT in .env is the name of this installation. Factorial writes the name on each sandbox, and removes only the sandboxes that have its own name. Thus two installations can use the same E2B team.
When the container stops at once
The startup check writes the reason to the log:
docker logs factorial
| Message | What to do |
|---|---|
is not mounted | Run the start command from the repository root, with .env and the key in place |
FACTORIAL_API_TOKEN is empty | Set it in .env |
FACTORIAL_MASTER_KEY is empty | Set it in .env |
FACTORIAL_RUNTIME__E2B__DEPLOYMENT is empty | Set it in .env |
the GitHub App is not configured | Set the App ID and the installation ID, and mount the key |
settings do not load | A value in .env has the wrong form. The message names it |
codex_cli does not run in this image | Use claude_cli |
Correct .env, remove the stopped container, and run the start command again:
docker rm factorial
Stop it, or delete the data
docker stop factorial # stop it; the data stays
docker rm factorial
docker volume rm factorial-data # delete the database and the agent runs
To start it again on the same data, run the docker run command from Build and start the container again.
Use docker stop, not docker rm -f. The terminal service closes each terminal before it stops, and this can take 45 seconds. The --stop-timeout 60 option lets the container use 60 seconds to stop. When the service cannot close a terminal in an E2B sandbox, that terminal stays open.
The factorial-data volume holds the database and the agent run folders. A container that you build again from a new image keeps the projects and the tickets in that volume.