Production install

Install a production single server

Check readiness on a server using persistent S3

What you will do

Install published images pinned by digest on one Linux server and check /readyz and /docs. This uses persistent AWS S3. This Compose path does not accept custom endpoints. For another S3-compatible endpoint, follow Kubernetes installation. Choose Kubernetes if you need high availability.

Before you begin

ItemRequirement
Operating systemLinux, both x86_64 and arm64
Docker Engine28 or later
Docker Compose2.24.6 or later
DiskDocker data-root on XFS mounted with prjquota. preflight does not check this. Otherwise the scheduler quota check fails during deploy and installation stops. Complete XFS preparation first on ext4 servers. Docker Desktop is unsupported
TimeNTP synchronized. Docker starts automatically after reboot
Toolsgit, jq, curl, openssl, Bun 1.3.14, Docker Buildx plugin, and an account with Docker daemon access
Object storePersistent AWS S3 bucket. Compose test-ops supports AWS S3 only, without custom endpoints. Check protection and permissions in Bucket preparation
External accessDownload images from ghcr.io and Docker Hub without signing in. Sessions connect to api.anthropic.com and github.com

A repository checkout is required because Compose files and scripts/test-ops.sh are read from it. Images are not built from the checkout.

  • Prepare the XFS disk and checkpoint bucket first.
  • Choose an access path before remote bootstrap.
  • Use a separate project, installation id, secret file, and state directory for every installation.

Steps

1. Pin the version and source

Check the changelog and select a published tag supporting the current settings document, model connection, and secret-key contracts. Replace the two placeholders below with an actual tag and its matching source commit from the package contract. An image main tag is not a Git tag; do not assume it is also the checkout reference.

TAG='<published-image-tag>'
SOURCE_REF='<matching-source-commit-or-release-tag>'
for app in control-host worker egress-proxy; do
  echo "$app $(docker buildx imagetools inspect "ghcr.io/jeongjaesoon/werft-$app:$TAG" --format '{{.Manifest.Digest}}')"
done

Continue only after all three image queries succeed and return digests.

git clone https://github.com/JeongJaeSoon/werft.git
cd werft
git checkout "$SOURCE_REF"
bun install --frozen-lockfile

The checkout must match the selected release commit, and dependency installation must succeed. Store the release file outside the checkout to keep the checkout clean.

2. Secrets and settings document

Keep secrets only in an env file outside the repository, readable only by operators. The script rejects group or other permissions.

sudo mkdir -p /etc/werft
sudo install -m 600 -o "$USER" infra/test-ops.env.example /etc/werft/test-ops.env
sudo install -d -m 700 -o "$USER" /var/lib/werft/test-ops

Open /etc/werft/test-ops.env in an editor and fill in the following. Tools such as sed -i create temporary files and fail with Permission denied in /etc/werft; use an editor that writes in place.

VariableValue
POSTGRES_PASSWORDRandom value using only letters, digits, ., _, ~, and -
EGRESS_AUTHORIZER_TOKENRandom value of at least 32 characters
PLATFORM_SECRET_KEY64-character hex from openssl rand -hex 32. Losing it prevents reading stored credentials
BOOTSTRAP_TOKENLeave it commented out. The API generates the first owner's token, and scripts/test-ops.sh bootstrap-token shows it once after deploy in step 4. For a fixed value, uncomment it and enter a random 32–256-character token; the command then prints no token
CONSOLE_ORIGINThe browser console origin (scheme://host[:port]) required to create a GitHub App through the console. Without it, the Create on GitHub button is disabled. The Slack account-link redirect and the Slack events address are computed from it too (without it, Slack linking is off). Add this line yourself; it is absent from the example env file. For an SSH tunnel, use http://127.0.0.1:<tunnel port>. To apply additions or changes after installation, reapply the current release with TEST_OPS_OBJECT_STORE=s3 scripts/test-ops.sh upgrade /var/lib/werft/test-ops/current.json, rather than start

Replace the example LocalStack values with production S3 values. Do not print secrets in command lines or logs.

VariableProduction value
EXECUTION_INSTALLATION_IDAn id unique to this installation. Do not change it after deployment
PLATFORM_SETTINGS_DOCUMENT/etc/werft/settings.json
EXECUTION_WORKSPACE_QUOTAon
AWS_REGION, S3_BUCKETPrepared AWS bucket region and name. Custom endpoints are unsupported
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEYAPI credentials with minimum bucket permissions
EGRESS_CREDENTIAL_ALLOWLISTProvider/Git hosts plus <bucket>.s3.<region>.amazonaws.com:443. For dotted bucket names, use s3.<region>.amazonaws.com:443
EGRESS_CREDENTIAL_PRIVATE_ALLOWLISTEmpty. Remove localstack:4566

Add model keys through console model connections after installation, so the v0.1.2 tag leaves ANTHROPIC_API_KEY empty. On a v0.1.0 tag checkout, however, compose requires it, and preflight exits 1 when it is empty. On that version, put the model key (or a placeholder you will replace later in the console) in the env file. The change that allows an empty value came in with JeongJaeSoon/werft#1262, and v0.1.2 is the first published release that includes it. Back up PLATFORM_SECRET_KEY securely and separately: losing it prevents reading stored credentials.

Put the settings document at /etc/werft/settings.json, listing the GitHub repositories sessions will use. It contains no secrets. Replace only the repository URL with your actual value; this example assumes a public repository. Every repository entry needs "auth": { "kind": "github_app" }. Otherwise import omits it and preflight blocks installation. For private repository authentication, see the API server documentation.

The migrate container must read the file, so use 0644. Create an empty file, then fill in the following using an editor.

sudo install -m 644 -o "$USER" /dev/null /etc/werft/settings.json
{
  "version": 1,
  "connections": {},
  "repositories": {
    "sample-app": {
      "url": "https://github.com/<owner>/<repo>.git",
      "branch": "main",
      "auth": { "kind": "github_app" }
    }
  },
  "mcp_servers": {}
}

Replace the URL with the public repository you will use. For private repositories, prepare CONSOLE_ORIGIN in the step 2 input table, then configure the GitHub App in the installed console. The file contains no secrets and must remain readable by migrate.

3. Generate release input

Run this in the shell retaining the selected TAG. DEPLOYMENT_DIR is an installation-specific directory outside the checkout.

DEPLOYMENT_DIR=/var/lib/werft/test-ops
SETTINGS_DIGEST="$(bun run scripts/lib/test-ops.ts settings-digest /etc/werft/settings.json)"
digest() { docker buildx imagetools inspect "ghcr.io/jeongjaesoon/werft-$1:$TAG" --format '{{.Manifest.Digest}}'; }
jq -n \
  --arg commit "$(git rev-parse HEAD)" \
  --arg control "ghcr.io/jeongjaesoon/werft-control-host@$(digest control-host)" \
  --arg worker "ghcr.io/jeongjaesoon/werft-worker@$(digest worker)" \
  --arg egress "ghcr.io/jeongjaesoon/werft-egress-proxy@$(digest egress-proxy)" \
  --arg settings "$SETTINGS_DIGEST" \
  '{source_commit: $commit,
    images: {control_host: $control, worker: $worker, egress_proxy: $egress},
    settings_digest: $settings}' > "$DEPLOYMENT_DIR/release.json"

Confirm all three image digests are nonempty.

4. Explicitly select S3, check, and install

TEST_OPS_OBJECT_STORE=s3 scripts/test-ops.sh preflight "$DEPLOYMENT_DIR/release.json"
TEST_OPS_OBJECT_STORE=s3 scripts/test-ops.sh deploy "$DEPLOYMENT_DIR/release.json"

preflight checks bucket protection and scratch version writes, holds, reads, hold release, and deletion. The scheduler checks quota during deploy. Both must succeed and services must become healthy. The object store mode cannot change after the first deploy.

If the env file sets no BOOTSTRAP_TOKEN, get the first owner's token after deploy. It waits in an owner-only file inside the API container, not in the container log, and this command deletes it as it prints it.

scripts/test-ops.sh bootstrap-token

Paste the value into /bootstrap (first owner and keys). Running the command again prints no token and exits 1. If you missed the value and no owner exists yet, run scripts/test-ops.sh stop and start to restart the API, which mints a new token (the old one stops working). An installation that already sets BOOTSTRAP_TOKEN is unchanged.

Verify

scripts/test-ops.sh status
curl --max-time 10 -fsS -w '\n%{http_code}\n' http://127.0.0.1:3000/readyz
curl --max-time 10 -fsS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/docs

Check the ready response body and 200, docs 200, and healthy status for each service. PostgreSQL must remain loopback-only, and the Docker TCP socket must be inaccessible externally. The complete production AWS S3 installation and restoration with data have not been verified by execution in this tutorial. An empty LocalStack checkpoint comparison and ready 200 do not substitute for them.

Troubleshooting

If the first deploy fails, started containers remain and stop is unavailable because current.json does not exist. Inspect failure logs for quota, network, or Secret issues. Confirm this installation's project name, then bring it down as follows without -v.

PROJECT='<this-installation-project>'
printf '%s\n' '{"services":{"x":{"image":"alpine:3"}},"networks":{"default":{}}}' \
  | docker compose --project-name "$PROJECT" -f - down --remove-orphans

Fix the cause, then rerun deploy with the same release file and environment. Use start to restart an installation that has already succeeded.

For quota errors, see XFS preparation; for bucket protection/permission errors, see Bucket preparation. For env permissions, check step 2 file modes; for settings digest errors, check step 3 digest and release generation. A failed first deploy does not imply a successful deployment state file exists.

Roll back

For a healthy installation, preserve data through stop and restart. After a failed first deploy, inspect the cause and resources this project created first. Do not use reset or volume deletion as default cleanup. Follow a separate removal procedure only after confirming no required data exists.

Next steps

Access → First owner and keys → Model connections. Set up backup and restoration before starting operations.

Previous URLs

Find the current guide for an older section

Production single-server installation

Prepare an XFS disk

Deployment package contract

Build images for your registry

Create the first owner and issue user keys

Configure model connections and profiles

Stop, restart, and remove Compose

Configure user access

Prepare a checkpoint bucket

Back up and rehearse restoration

On this page