Use this when upgrading an existing Keycast deployment to the hardened runtime. This is not a blank install path.
- Containers now run as a non-root user and use a read-only root filesystem.
master.keyis mounted from the host instead of being copied into the image.ALLOWED_PUBKEYSis enforced by the API, not just the browser.docker-compose.prod.ymlcan pull the publishedghcr.io/marmot-protocol/keycastimage instead of building Rust and Bun on the server.- The Nostr Rust stack moved to current crates.io releases.
- Migration
0002_normalize_allowed_kinds_permissions.sqlnormalizes oldallowed_kindspermission JSON from{"sign":[...]}to{"allowed_kinds":[...]}.
- Stop the running containers and make a cold backup of the database and key. SQLite runs in WAL
mode, so prefer SQLite's backup command when
sqlite3is available:
mkdir -p backups
ts="$(date +%Y%m%d-%H%M%S)"
docker compose stop keycast-api keycast-signer keycast-web
sqlite3 database/keycast.db ".backup 'backups/keycast.${ts}.db'"
cp master.key "backups/master.${ts}.key"If sqlite3 is not available on the host, copy the database sidecar files too when they exist:
mkdir -p backups
ts="$(date +%Y%m%d-%H%M%S)"
docker compose stop keycast-api keycast-signer keycast-web
cp database/keycast.db "backups/keycast.${ts}.db"
[ ! -f database/keycast.db-wal ] || cp database/keycast.db-wal "backups/keycast.${ts}.db-wal"
[ ! -f database/keycast.db-shm ] || cp database/keycast.db-shm "backups/keycast.${ts}.db-shm"
cp master.key "backups/master.${ts}.key"- Do not generate a new
master.keyfor an existing install. If the host file is missing but the old container still has/app/master.key, recover it before deploying:
docker cp keycast-api:/app/master.key ./master.key
chmod 600 master.key- Set the server allowlist before starting the new API:
ALLOWED_PUBKEYS=hexpubkey1,hexpubkey2Every admin pubkey that needs to use the app after the upgrade must be included.
- Run the preflight:
scripts/upgrade_preflight.sh
sudo scripts/upgrade_preflight.sh --fix-permissionsThe second command is needed when the old root-running containers created root-owned database files.
The preflight also checks that database/migrations/0002_normalize_allowed_kinds_permissions.sql is
present in the checkout. The API and signer read migrations from the bind-mounted database/
directory at startup.
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
docker compose -f docker-compose.prod.yml psProduction Compose pulls ghcr.io/marmot-protocol/keycast by default. If pull returns
unauthorized, change the GitHub Packages visibility to public or log the deployment host in to
GHCR before retrying.
The API and signer run SQLx migrations on startup. The new migration only normalizes old permission JSON. It does not rotate keys, change stored-key ciphertext, or invalidate existing bunker connection strings.
By default, production Compose tracks the moving master tag. To pin a specific build, set
KEYCAST_IMAGE_TAG=sha-<commit> in .env, then run the same pull and up commands.
curl -f https://your-domain.example/health
curl -f https://your-domain.example/api/health
docker compose -f docker-compose.prod.yml logs --tail=100 keycast-api keycast-signer keycast-webThen sign in with an allowlisted pubkey, open an existing team, inspect an existing policy, and test one existing NIP-46 authorization. Existing bunker connection strings should keep working because the stored keys, bunker keys, and connection secrets are unchanged.
If you need to roll back code after migration 0002 has run, restore the database backup too. The old
application may not understand the normalized allowed_kinds JSON.
docker compose -f docker-compose.prod.yml down
cp backups/keycast.YYYYMMDD-HHMMSS.db database/keycast.db
cp backups/master.YYYYMMDD-HHMMSS.key master.key
KEYCAST_IMAGE_TAG=sha-previous docker compose -f docker-compose.prod.yml up -dDo not roll back with a different master.key; stored private keys and bunker keys will not decrypt.