Skip to content

Self-hosting

Use the single-server Compose deployment guide for published service images, HTTPS, PostgreSQL, and S3 artifact storage. GitHub App repository import and automatic syncing are available. The deployment has been tested with GitHub.com and on a gVisor host, including sandbox isolation, resource limits and fail-closed startup. Its guide includes tested backup, restore and update procedures. To repeat the gVisor, upgrade and restore tests, run the production end-to-end test.

For a local trial, use secondshift up as described in the quick start.

See GitHub App repository access to configure repository import and automatic syncing.

Install and start SecondShift using the quickstart. The beta is distributed through GitHub Releases until it is available on PyPI.

Upgrading from an alpha? This beta resets database migrations and requires a fresh database. Read the beta 1 release notes first.

Once started, the console shows:

SecondShift is running at http://127.0.0.1:8000
Sign in with this link. It works once, within 15 minutes:
http://127.0.0.1:8000/-/sign-in/<one-time-token>/
Run `secondshift login` for a new link. Press Ctrl+C to stop.

Self-host SecondShift to run committed skills against trusted repositories with your own model account and deliver reports to team channels. You can move to a deployment with stronger isolation without changing the skills.

  • fake: records requests without executing repository code. For tests only.
  • local-docker: hardened Docker for development and trusted, single-tenant self-hosting.
  • isolated-runner: a separate Linux runner using runsc for hosted or untrusted workloads.

There is no normal no-sandbox backend. Backends are deployment choices with the same functional runner contract, not job features. Production settings fail closed unless the configured isolation policy is available. SecondShift never accepts agent drivers, runtime flags, images, capabilities, networks or mounts from a repository manifest.

Skill commands run in Docker’s official python:3.13-slim image, pinned by digest in secondshift/config/constants.py. The runner pulls it when it starts, so there is nothing to build or host.

Operators can set SECONDSHIFT_RUNNER_IMAGE to their own image, for example one built FROM python:3.13-slim with Node or Go added for skills that need them. Repositories can never choose the image. A custom image must:

  • provide sleep, find, cat and grep (any Debian or Alpine base does)
  • work as an unprivileged user with a read-only root filesystem, no network, HOME=/tmp and the repository mounted at /workspace
  • contain no credentials, Docker client or SecondShift code.

Git is not needed in the image: the runner checks out the commit itself. Pin custom images by digest (name@sha256:…) so a pushed tag can’t change what runs.

Self-hosting the control plane does not by itself keep repository content local: the configured model provider receives the context sent to it. An OpenAI-compatible local provider can be used where the entire execution path must remain within the operator’s environment.

A workspace owner connects model providers in Connections. Anthropic, OpenAI and OpenRouter need only an API key. Any other provider with an OpenAI-compatible chat completions API works as an OpenAI-compatible connection with its base URL.

Provider Connection Base URL
Anthropic Anthropic Preset
OpenAI OpenAI Preset
OpenRouter OpenRouter Preset
Fireworks AI OpenAI-compatible https://api.fireworks.ai/inference/v1
Together AI OpenAI-compatible https://api.together.xyz/v1
Baseten OpenAI-compatible https://inference.baseten.co/v1

Runs call tools, so choose a model that supports tool calling. Amazon SageMaker endpoints use AWS request signing, not an API key. Put an OpenAI-compatible proxy such as LiteLLM in front of them.

Set SECONDSHIFT_PUBLIC_URL to the HTTPS address people use to open the dashboard. Production refuses to start without it. OAuth sign-in callbacks and run links use it.

MCP and HTTP API calls leave from the worker, never from a sandbox. In production they reach only public HTTPS addresses. Local mode also allows local addresses for testing. Each address is checked again when the connection opens, so a host cannot pass the check and then resolve to a private address.

Sign-up is closed in production unless SECONDSHIFT_SIGNUP_ENABLED is set. People join through invite links instead.

Create the first workspace on the server:

Terminal window
secondshift create_workspace "Acme"

It prints a one-time link. Whoever opens it can sign in or sign up, even with sign-up closed, and becomes the workspace’s owner after confirming. The link works for seven days. If it expires unused, run the command again for a new one. Once the workspace has an owner, the command refuses to make another.

Staff accounts can also create workspaces from the dashboard, with the workspace menu under the logo. They own each workspace they create. The account secondshift up makes is staff. With sign-up open, anyone can create workspaces this way, up to three each. Staff have no limit. With sign-up closed, people who are not staff can only join through invites.

People in more than one workspace switch between them with the same menu.

Owners invite everyone else from the Settings page. Each invite has a role:

  • Owner connects services, approves access, invites people and manages their roles.
  • Admin enables, runs and schedules approved jobs, manages repositories and changes run limits.
  • Member can view.

People can sign in with GitHub when GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET are set, or with an email and password. Set SECONDSHIFT_EMAIL_URL (for example smtp://user:[email protected]:587) to send address confirmations and password resets. Without it SecondShift sends no email, does not ask people to confirm their address, and cannot reset passwords.

An invite link works once and is shown only when it is created. The server stores a hash of it, not the link itself. Opening a link never adds anyone by itself. The person confirms in the dashboard while signed in. Owners can revoke pending invites from the Settings page.

Owners also change roles and remove people on the Settings page. A workspace always keeps at least one owner, so the last owner cannot step down or leave.

Role Controls
Repository contributors Propose skills, schedules, and requested capabilities through Git changes.
Repository maintainers Review and merge those changes through the repository’s normal process.
Workspace owners Connect model providers and services, approve each job’s access, add local repositories, and invite people and manage their roles.
Workspace admins Enable, run and schedule approved jobs, manage repositories, and change run limits.
Workspace members View repositories, jobs and runs.
Server operators Define sandbox images, runtimes, resource limits, and network policy.

Workspace owners can also perform admin and member actions. Admins can also perform member actions. The full list of actions and required roles is POLICY in secondshift/workspaces/access.py.

A committed skill does not grant itself access. Repository configuration cannot choose credentials, provider endpoints, sandbox images, mounts, or network settings. Expanding requested capabilities requires renewed approval.

Skills run in isolated sandboxes. Every run is tied to a Git commit.

Full run output is stored outside Postgres in private storage, under runs/<workspace>/<YYYY>/<MM>/<DD>/<run>/output.log. Choose the backend with SECONDSHIFT_ARTIFACT_STORAGE:

  • filesystem (default): files under SECONDSHIFT_ARTIFACT_ROOT. Mount a persistent volume there in production.
  • s3: any S3-compatible service. Configure the SECONDSHIFT_ARTIFACT_* variables in the environment reference. To try it locally, set COMPOSE_PROFILES=s3 and SECONDSHIFT_ARTIFACT_STORAGE=s3. Run process-compose up to start RustFS and let bootstrap_dev create the bucket.

Finished runs older than SECONDSHIFT_RUN_RETENTION_DAYS (default 90) are pruned daily by the worker, together with their output and DBOS records.

A job with history starts each run with its earlier reports. SECONDSHIFT_JOB_HISTORY_MAX (default 5) is the most a manifest can ask for. Lowering it also applies to jobs that already ask for more. SECONDSHIFT_RUN_HISTORY_MAX_CHARS (default 65,536) caps their total length. The newest report is included first. What does not fit is cut or left out.

Each run has four limits. Each has a default and a maximum:

  • SECONDSHIFT_RUN_TIMEOUT_SECONDS (default 900, so 15 minutes) limits time. The clock starts when the run starts, so time in the queue does not count.
  • SECONDSHIFT_AGENT_MAX_TURNS (default 24) counts model requests, not tool actions. One request can ask for several tool calls.
  • SECONDSHIFT_AGENT_MAX_TOOL_CALLS (default 64) counts file reads, searches and commands.
  • SECONDSHIFT_MODEL_RUN_TOKEN_BUDGET (default 2,000,000) counts input and output tokens. The model gateway enforces it.

The workspace owner can change these limits on the Settings page for the whole workspace, and on a job’s page for that job. A job’s limit wins over the workspace’s, and the workspace’s wins over the default. No value can go above its maximum. Set the maximum with the same variable plus _CEILING, for example SECONDSHIFT_AGENT_MAX_TURNS_CEILING. Each maximum defaults to its limit, so owners can only lower a limit until you raise the maximum. Lowering a maximum later also caps values owners already chose. .secondshift.yaml cannot set limits.

A run’s limits are fixed when it is created and stored in its execution plan. Changing a limit affects the next run, not one that is queued or running.

The run checks the time limit before each model request and each tool call, and its model access ends at the limit. A run that passes it fails with the reason Timed out and keeps what the agent wrote so far as a partial report. Each sandbox command also has its own limit, SECONDSHIFT_RUNNER_COMMAND_TIMEOUT_SECONDS (default 120).

When the agent reaches its last model request, or has no tool calls left, its tools are taken away and it is asked to write the report with what it has. The run then succeeds and its timeline notes that the budget ran out. A run that still goes past a limit, or hits the token budget, fails with the reason Budget used up. It names the limit and keeps what the agent wrote so far as a partial report. The run page links to the job’s limits, and runs show their token use against the budget, for example 12.3k / 2m tok.