Keel self-hosted deploy SOP
Operations manual: deploy a self-hosted Keel instance (Update server +
Cloud Build server + frontend CDN) from scratch. Target users:
ops / DevOps for self-hosted Keel backends — not regular Keel users
(regular users just run npx @keel-ai/cli ...).
Estimated time: ~60 minutes (excluding approval waits).
0. Prerequisites
| Domain | 1 root domain like mycorp.dev, with subdomains update.mycorp.dev + cdn.mycorp.dev. |
| SSL cert | Let’s Encrypt or Aliyun free cert. One cert per subdomain. |
| Server | 1 × 4C8G+ ECS / VPS running Update server + Cloud Build server. Or split across two hosts. |
| Aliyun account | OSS bucket + CDN domain. |
| Disk | Update server metadata uses SQLite (default); a Postgres backend is planned. |
1. Aliyun OSS bucket
# 1.1 Create bucket (East-1 / North-2; recommend same region as the server)
aliyun oss mb oss://keel-bundles --acl=public-read \
--region=cn-hangzhou
# 1.2 Create a RAM user + grant OSS-write
aliyun ram CreateUser --UserName keel-update-uploader
aliyun ram CreateAccessKey --UserName keel-update-uploader
# ↑ Record AccessKeyId + AccessKeySecret; fill into KEEL_UPDATE_S3_* below
# 1.3 Bind OSS-Read-Write policy to this user (scoped to this bucket)
cat > /tmp/keel-policy.json <<'EOF'
{
"Version": "1",
"Statement": [{
"Effect": "Allow",
"Action": ["oss:PutObject", "oss:GetObject", "oss:DeleteObject"],
"Resource": ["acs:oss:*:*:keel-bundles/*"]
}]
}
EOF
aliyun ram CreatePolicy --PolicyName keel-bundles-rw \
--PolicyDocument file:///tmp/keel-policy.json
aliyun ram AttachPolicyToUser --PolicyName keel-bundles-rw \
--PolicyType Custom --UserName keel-update-uploader
2. Aliyun CDN
# 2.1 Create CDN domain pointing at the OSS bucket
aliyun cdn AddCdnDomain \
--DomainName cdn.mycorp.dev \
--CdnType download \
--Sources '[{"content":"keel-bundles.oss-cn-hangzhou.aliyuncs.com","type":"oss","port":80,"priority":20,"weight":15}]'
# 2.2 Upload SSL cert + enable HTTPS
aliyun cdn SetDomainServerCertificate \
--DomainName cdn.mycorp.dev \
--ServerCertificateStatus on \
--CertName keel-cdn-cert \
--ServerCertificate "$(cat fullchain.pem)" \
--PrivateKey "$(cat privkey.pem)"
# 2.3 DNS (in your DNS console):
# CNAME cdn.mycorp.dev → cdn.mycorp.dev.w.alikunlun.com (target from CDN console)
3. SQLite data directory
# Update server defaults to SQLite for manifest + patch metadata.
# Single instance + local SSD handles ~100k device polling traffic.
# Postgres backend is on the roadmap; only mem / sqlite:<path> for now.
mkdir -p /var/lib/keel
chown keel:keel /var/lib/keel
4. Update server deployment
# 4.1 Clone + build on the server
git clone https://github.com/liamxujia/appunvs.git
cd appunvs/keel
go build -o /usr/local/bin/keel-update-server ./cmd/update-server
# 4.2 systemd unit
cat > /etc/systemd/system/keel-update.service <<'EOF'
[Unit]
Description=KAS Update Server
After=network.target
[Service]
EnvironmentFile=/etc/keel/update.env
ExecStart=/usr/local/bin/keel-update-server
Restart=always
RestartSec=5
User=keel
[Install]
WantedBy=multi-user.target
EOF
# 4.3 env file
mkdir -p /etc/keel
cat > /etc/keel/update.env <<EOF
KEEL_UPDATE_LISTEN=:8090
KEEL_UPDATE_DB=sqlite:/var/lib/keel/update.db
KEEL_UPDATE_BLOB_STORE=s3
KEEL_UPDATE_S3_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
KEEL_UPDATE_S3_REGION=cn-hangzhou
KEEL_UPDATE_S3_BUCKET=keel-bundles
KEEL_UPDATE_S3_ACCESS_KEY=<from step 1.2>
KEEL_UPDATE_S3_SECRET_KEY=<from step 1.2>
KEEL_UPDATE_PUBLIC_URL_BASE=https://cdn.mycorp.dev
EOF
chmod 600 /etc/keel/update.env
# 4.4 Start
systemctl enable --now keel-update
systemctl status keel-update
5. nginx / reverse proxy
server {
listen 443 ssl http2;
server_name update.mycorp.dev;
ssl_certificate /etc/letsencrypt/live/update.mycorp.dev/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/update.mycorp.dev/privkey.pem;
# Bundle uploads can be tens of MB — raise default limits.
client_max_body_size 128m;
proxy_request_buffering off;
proxy_read_timeout 300s;
location / {
proxy_pass http://127.0.0.1:8090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
6. Cloud Build server (optional)
If you only use OTA and don’t need server-side bundle builds, skip
this section. Your RN developers build locally with
keel build --local and run keel publish directly.
If you want Cloud Build to run (remote bundle building, similar to EAS Build):
go build -o /usr/local/bin/keel-build-server ./cmd/build-server
# Cloud Build runs metro + Hermes in a Docker container; needs docker.
apt-get install -y docker-ce docker-ce-cli containerd.io
systemctl enable --now docker
# Build the metro+hermes builder image
bash cloud/packaging/build-cloud-build.sh
# systemd unit + nginx similar to Update server, listening on :8091.
7. Smoke test
# 7.1 Health check
curl https://update.mycorp.dev/health
# {"status":"ok"}
# 7.2 Upload a bundle (multipart mode)
curl -X POST https://update.mycorp.dev/v1/myproj/update/bundles \
-F 'metadata={"version":"1.0.0","content_hash":"sha256:abc...","platform":"ios","channel":"production","runtime_version":"1.0","size_bytes":1234};type=application/json' \
-F 'bundle=@dist/index.ios.bundle;type=application/octet-stream'
# 7.3 Promote + fetch manifest
curl -X POST https://update.mycorp.dev/v1/myproj/update/bundles/abc.../promote \
-H 'Content-Type: application/json' \
-d '{"platform":"ios","channel":"production","runtime_version":"1.0"}'
curl 'https://update.mycorp.dev/v1/myproj/update/manifest?platform=ios&channel=production&runtime_version=1.0'
# {"current":{"hash":"sha256:abc...","url":"https://cdn.mycorp.dev/...",...},"patches":[]}
8. Staged rollout
# 50% traffic
curl -X POST https://update.mycorp.dev/v1/myproj/update/bundles/<hash>/promote \
-d '{"platform":"ios","channel":"production","runtime_version":"1.0","rollout":50}'
# Watch metrics, no crash → push to 100%
curl -X POST https://update.mycorp.dev/v1/myproj/update/bundles/<hash>/promote \
-d '{"platform":"ios","channel":"production","runtime_version":"1.0","rollout":100}'
# Incident → rollback to the previous hash (a bundle that was previously promoted)
keel rollback --to=<previous-hash>
9. Submit driver credentials
If you use KAS Submit for automated store submission (not just OTA),
you need credentials per market. See keel/cloud/internal/submit/drivers/<market>/’s
Cred* constants for each driver’s specifics.
# Example: Apple App Store
keel submit configure ios_appstore \
--issuer_id="..." --key_id="..." --p8_path="./AuthKey_XXX.p8" \
--bundle_id="com.mycorp.app"
# Example: Huawei AGC
keel submit configure huawei \
--app_id="..." --client_id="..." --client_secret="..." \
--project_id="..."
Credentials live in ~/.keel/credentials.yml with permissions 0600.
For CI pipelines, override via env variables (each driver’s README
lists them).
10. Monitoring + DR
| Item | Approach |
|---|---|
| Health monitoring | UptimeRobot 5-min ping update.mycorp.dev/health |
| Logs | systemd-journald → ELK / Aliyun SLS (see Mortar’s SLS integration) |
| Postgres backup | Daily full + 1h binlog; Aliyun RDS includes this by default |
| OSS backup | Enable OSS cross-region replication; multi-region sync |
| CDN failure | DNS fallback to the OSS direct domain |
11. Upgrades
cd /opt/keel
git pull
go build -o /usr/local/bin/keel-update-server ./cmd/update-server
systemctl restart keel-update
Update server runs DB migrations automatically (scans
internal/update/ schema files on start).
Troubleshooting checklist
| Symptom | Likely cause |
|---|---|
/health returns 503 | DB unreachable / OSS endpoint wrong / credentials expired |
| Manifest 200 but bundle URL 404 | OSS bucket not public-read or CDN not pulling origin |
| Patch flow not effective | Client current_hash not in patches → client falls back to full download (normal) |
| Submit returns 401 | Credentials expired or client_id wrong |
| Cloud Build job stuck | docker daemon down / disk full / metro cache corrupted |
References
architecture— 5-layer product overviewupdate— Update protocol detailssubmit— Submit 5-market comparisoninternal/update/— Update server sourceinternal/submit/— Submit driver source