# Dedicated Gitea Runtime Builder Runner > Goal: build API/Worker runtime image tar on a machine that does not host production services. ## Required machine Minimum recommended spec: - 2 vCPU minimum, 4 vCPU preferred. - 4GiB RAM minimum, 8GiB preferred. - 40GiB disk minimum. - Docker Engine installed. - Network access to Gitea and production SSH. Do not install this runner on the current production host. ## Runner label The production runtime image workflow targets: ```yaml runs-on: runtime-builder ``` Register the new runner with a label named `runtime-builder`. Keep the existing production/staging runner separate. ## Required Gitea secrets The workflow can use either Gitea secrets or the dedicated runner-local upload key. Preferred repository or organization secrets: - `PRODUCTION_SSH_HOST` - `PRODUCTION_SSH_USER` - `PRODUCTION_SSH_KEY` If these secrets are absent, the workflow falls back to: - host: `47.98.113.167` - user: `root` - key path on the dedicated builder: `/root/.ssh/xiaoxia_runtime_builder` The key must allow uploading to: ```text /var/lib/xiaoxia-saas-production/runtime-images-.tar ``` Do not store SSH private keys in the repository. ## Workflow behavior On `v*` tag push: 1. `build-production-runtime-images` runs on `runtime-builder`. 2. Checkout downloads the repository archive using the explicit `${{ github.token }}` environment binding; runner shell environments must not assume `GITHUB_TOKEN` is implicitly set. 3. It executes `scripts/build_release_images.sh `. 4. It builds `apps/web/dist` in a Node Docker container, not on production. 5. It packages `release-.tar.gz` with the prebuilt Web dist. 6. It uploads both `runtime-images-.tar` and `release-.tar.gz` to production. 7. `deploy-production` runs after the build job succeeds, on `runtime-builder`, and drives production over SSH. 8. `deploy-production` extracts the prebuilt release artifact on production, loads `/var/lib/xiaoxia-saas-production/runtime-images-.tar`, and restarts API/Worker/Web. If either the release tar or runtime image tar is missing, production deploy must fail. The production deploy job intentionally also runs on `runtime-builder` because Gitea `needs` scheduling previously skipped the downstream `ubuntu-latest` deploy job after a successful cross-runner build. ## Preflight checks on runner Run on the new runner before registration: ```bash docker version docker ps --format '{{.Names}}' | grep -Eq '^(xiaoxia-(api|web|worker|postgres|redis)-production|gitea)$' && exit 1 || true ``` The second command must not find production containers. ## Validation release After runner registration: 1. Push a test tag only after `develop` is green. 2. Confirm runtime image build job runs on the dedicated runner. 3. Confirm production deploy waits for the image job. 4. Run: ```bash python scripts/smoke_public_auth_flow.py python scripts/smoke_public_upload_flow.py ``` 5. Record release result in the Phase progress document.