Machine-translated draft — terminology + flow still being reviewed.

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

Domain1 root domain like mycorp.dev, with subdomains update.mycorp.dev + cdn.mycorp.dev.
SSL certLet’s Encrypt or Aliyun free cert. One cert per subdomain.
Server1 × 4C8G+ ECS / VPS running Update server + Cloud Build server. Or split across two hosts.
Aliyun accountOSS bucket + CDN domain.
DiskUpdate 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

ItemApproach
Health monitoringUptimeRobot 5-min ping update.mycorp.dev/health
Logssystemd-journald → ELK / Aliyun SLS (see Mortar’s SLS integration)
Postgres backupDaily full + 1h binlog; Aliyun RDS includes this by default
OSS backupEnable OSS cross-region replication; multi-region sync
CDN failureDNS 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

SymptomLikely cause
/health returns 503DB unreachable / OSS endpoint wrong / credentials expired
Manifest 200 but bundle URL 404OSS bucket not public-read or CDN not pulling origin
Patch flow not effectiveClient current_hash not in patches → client falls back to full download (normal)
Submit returns 401Credentials expired or client_id wrong
Cloud Build job stuckdocker daemon down / disk full / metro cache corrupted

References

  • architecture — 5-layer product overview
  • update — Update protocol details
  • submit — Submit 5-market comparison
  • internal/update/ — Update server source
  • internal/submit/ — Submit driver source