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
| Item | Requirement |
|---|---|
| Operating system | Linux, both x86_64 and arm64 |
| Docker Engine | 28 or later |
| Docker Compose | 2.24.6 or later |
| Disk | Docker 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 |
| Time | NTP synchronized. Docker starts automatically after reboot |
| Tools | git, jq, curl, openssl, Bun 1.3.14, Docker Buildx plugin, and an account with Docker daemon access |
| Object store | Persistent AWS S3 bucket. Compose test-ops supports AWS S3 only, without custom endpoints. Check protection and permissions in Bucket preparation |
| External access | Download 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}}')"
doneContinue 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-lockfileThe 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-opsOpen /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.
| Variable | Value |
|---|---|
POSTGRES_PASSWORD | Random value using only letters, digits, ., _, ~, and - |
EGRESS_AUTHORIZER_TOKEN | Random value of at least 32 characters |
PLATFORM_SECRET_KEY | 64-character hex from openssl rand -hex 32. Losing it prevents reading stored credentials |
BOOTSTRAP_TOKEN | Leave 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_ORIGIN | The 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.
| Variable | Production value |
|---|---|
EXECUTION_INSTALLATION_ID | An id unique to this installation. Do not change it after deployment |
PLATFORM_SETTINGS_DOCUMENT | /etc/werft/settings.json |
EXECUTION_WORKSPACE_QUOTA | on |
AWS_REGION, S3_BUCKET | Prepared AWS bucket region and name. Custom endpoints are unsupported |
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY | API credentials with minimum bucket permissions |
EGRESS_CREDENTIAL_ALLOWLIST | Provider/Git hosts plus <bucket>.s3.<region>.amazonaws.com:443. For dotted bucket names, use s3.<region>.amazonaws.com:443 |
EGRESS_CREDENTIAL_PRIVATE_ALLOWLIST | Empty. 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-tokenPaste 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/docsCheck 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-orphansFix 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.