aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-18 10:35:11 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-18 10:35:11 +0200
commit1e5b4927e5ae1d542d51158eb995d4f4b5a8b1c5 (patch)
tree5d2b414abbe6550eaf403c6aa4b5f75eb703fa01
parent7a0771215d9bd52dc9d6913abee56a9f62893d3e (diff)
downloadbokf-1e5b4927e5ae1d542d51158eb995d4f4b5a8b1c5.tar.gz
bokf-1e5b4927e5ae1d542d51158eb995d4f4b5a8b1c5.zip
deploy: --dev hot-reloads the daemon via docker cp + SIGHUPv0.1.14
- bokfd re-execs its own binary on SIGHUP, closing listeners/db first - scripts/deploy.sh --dev cross-builds and copies binaries into the running container, then verifies the reported version
-rw-r--r--docs/DEPLOY.md17
-rw-r--r--docs/PROTOCOL.md5
-rw-r--r--docs/STATE.md4
-rwxr-xr-xscripts/deploy.sh116
-rw-r--r--src/bokfd.c20
5 files changed, 130 insertions, 32 deletions
diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md
index 8862fab..2964a19 100644
--- a/docs/DEPLOY.md
+++ b/docs/DEPLOY.md
@@ -50,6 +50,23 @@ to touch an already initialized database. Subsequent `scripts/deploy.sh`
runs update the image, tag and compose file, restart the daemon and wait for
the healthcheck.
+## Fast iteration (`--dev`)
+
+Normal deploys build an image and recreate the container. While developing,
+`scripts/deploy.sh --dev` skips the image entirely:
+
+- the gate (`make` + `make test`) still runs locally,
+- on an architecture mismatch the binaries are cross-compiled here
+ (`deploy/Dockerfile.cross`, ~20 s on a PC),
+- the binaries are copied into the running container with `docker cp`,
+- the daemon is reloaded with `SIGHUP`, which re-execs the binary in place
+ (in-memory sessions are lost, clients reconnect),
+- the version the daemon reports is verified against the tag.
+
+No image is built and the container is not recreated; a later normal deploy
+replaces the copied binaries. Use a descriptive tag, e.g.
+`scripts/deploy.sh --dev v0.2.0-rc1`.
+
## Deploying upgrades
```sh
diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md
index f282691..1baf607 100644
--- a/docs/PROTOCOL.md
+++ b/docs/PROTOCOL.md
@@ -488,4 +488,7 @@ beyond the session and calls nothing but public commands.
| `allow_org_create` | `true` | any user may create an org |
Container deployments mount the socket directory, database directory, backup
-and export directories as volumes; the daemon is otherwise stateless.
+and export directories as volumes; the daemon is otherwise stateless. On
+`SIGHUP` the daemon closes its listeners and database and re-executes its own
+binary in place (used by `scripts/deploy.sh --dev`); in-memory sessions are
+reset and clients reconnect.
diff --git a/docs/STATE.md b/docs/STATE.md
index 3cbe448..458833e 100644
--- a/docs/STATE.md
+++ b/docs/STATE.md
@@ -65,7 +65,9 @@ server/protocol/ledger only.
come from a lego sidecar using INWX DNS-01 (`compose.yaml`). Externals
get accounts/roles/tokens, never VPN access. `scripts/deploy.sh` builds
locally and ships over SSH, or builds on the host when architectures
- differ.
+ differ. `scripts/deploy.sh --dev` cross-compiles the binaries here and
+ hot-reloads the daemon (SIGHUP re-exec via `docker cp`), skipping the
+ image build and container recreate.
## Pending decisions
diff --git a/scripts/deploy.sh b/scripts/deploy.sh
index c345a21..79d731f 100755
--- a/scripts/deploy.sh
+++ b/scripts/deploy.sh
@@ -1,7 +1,12 @@
#!/usr/bin/env bash
# Deploy a locally built bokf image to BOKF_HOST over SSH. See docs/DEPLOY.md.
#
-# scripts/deploy.sh [tag]
+# scripts/deploy.sh [--dev] [tag]
+#
+# Normal mode builds an image (cross-compiled on this machine when the target
+# architecture differs) and recreates the container. --dev skips the image:
+# it copies the fresh binaries into the running container and reloads the
+# daemon with SIGHUP, which re-execs them in place.
#
# BOKF_HOST (user@host) and BOKF_REMOTE_DIR are read from the environment or
# from .env in the repository root. The host needs only Docker and Compose.
@@ -23,6 +28,12 @@ fi
BOKF_REMOTE_DIR="${BOKF_REMOTE_DIR:-/srv/bokf}"
SSH=(ssh "$BOKF_HOST")
+DEV=0
+if [ "${1:-}" = "--dev" ]; then
+ DEV=1
+ shift
+fi
+
TAG="${1:-}"
if [ -z "$TAG" ]; then
TAG="$(git describe --tags --always --dirty 2>/dev/null || echo dev)"
@@ -36,11 +47,41 @@ esac
remote() { "${SSH[@]}" "$1"; }
+wait_healthy() {
+ for _ in $(seq 1 30); do
+ cid="$(remote "cd '$BOKF_REMOTE_DIR' && docker compose ps -q bokfd" 2>/dev/null || true)"
+ if [ -n "$cid" ]; then
+ st="$(remote "docker inspect --format '{{.State.Health.Status}}' '$cid'" 2>/dev/null || true)"
+ if [ "$st" = "healthy" ]; then
+ return 0
+ fi
+ fi
+ sleep 2
+ done
+ return 1
+}
+
+cross_build() {
+ echo "deploy: cross-compiling $host_arch binaries on this machine"
+ rm -rf .prebuilt
+ mkdir -p .prebuilt
+ docker build -q -f deploy/Dockerfile.cross -t bokf-cross deploy/ >/dev/null
+ docker run --rm -e "VERSION=$TAG" -v "$PWD":/src:ro \
+ -v "$PWD/.prebuilt":/out bokf-cross
+ if command -v file >/dev/null 2>&1 &&
+ ! file .prebuilt/bokfd | grep -q aarch64; then
+ echo "deploy: cross-build produced no arm64 binary" >&2
+ rm -rf .prebuilt
+ exit 1
+ fi
+}
+
echo "deploy: gate: build + tests"
make -j"$(nproc)"
make test
BOKF_BUILD="${BOKF_BUILD:-auto}"
+host_arch="$(uname -m)"
case "$BOKF_BUILD" in
auto)
host_arch="$(remote 'uname -m' 2>/dev/null || echo unknown)"
@@ -51,31 +92,64 @@ case "$BOKF_BUILD" in
BOKF_BUILD=remote
fi
;;
- local | remote) ;;
+ local | remote)
+ host_arch="$(remote 'uname -m' 2>/dev/null || echo unknown)"
+ ;;
*)
echo "deploy: BOKF_BUILD must be auto, local or remote" >&2
exit 1
;;
esac
+if [ "$DEV" = 1 ]; then
+ if ! remote "test -f '$BOKF_REMOTE_DIR/var/db/bokfd.db'"; then
+ echo "deploy: no database on $BOKF_HOST; run a normal deploy first" >&2
+ exit 1
+ fi
+ if [ "$BOKF_BUILD" = local ]; then
+ prebin="build"
+ echo "deploy: dev: using the locally built binaries"
+ else
+ cross_build
+ prebin=".prebuilt"
+ fi
+ echo "deploy: dev: copying binaries into the running container"
+ remote "mkdir -p /tmp/bokf-dev"
+ scp -q "$prebin/bokfd" "$prebin/bokfctl" "$prebin/bokftui" \
+ "$BOKF_HOST:/tmp/bokf-dev/"
+ remote "cd '$BOKF_REMOTE_DIR'
+ cid=\$(docker compose ps -q bokfd)
+ test -n \"\$cid\" || { echo 'deploy: bokfd is not running' >&2; exit 1; }
+ docker cp /tmp/bokf-dev/bokfd \"\$cid:/usr/local/bin/bokfd\"
+ docker cp /tmp/bokf-dev/bokfctl \"\$cid:/usr/local/bin/bokfctl\"
+ docker cp /tmp/bokf-dev/bokftui \"\$cid:/usr/local/bin/bokftui\"
+ docker kill --signal=HUP \"\$cid\""
+ if [ "$prebin" = ".prebuilt" ]; then
+ rm -rf .prebuilt
+ fi
+ ver=""
+ for _ in $(seq 1 15); do
+ ver="$(remote "cd '$BOKF_REMOTE_DIR' && docker exec \$(docker compose ps -q bokfd) bokfctl meta 2>/dev/null" |
+ sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' || true)"
+ [ "$ver" = "$TAG" ] && break
+ sleep 1
+ done
+ if [ "$ver" = "$TAG" ]; then
+ echo "deploy: dev reloaded, daemon reports $ver"
+ exit 0
+ fi
+ echo "deploy: dev reload did not come back with $TAG (got '${ver:-nothing}')" >&2
+ remote "cd '$BOKF_REMOTE_DIR' && docker compose logs --tail=30 bokfd" >&2 || true
+ exit 1
+fi
+
if [ "$BOKF_BUILD" = local ]; then
echo "deploy: building image bokf:$TAG locally"
docker build --build-arg "VERSION=$TAG" -t "bokf:$TAG" .
echo "deploy: shipping image to $BOKF_HOST"
docker save "bokf:$TAG" | gzip | "${SSH[@]}" 'gunzip | docker load'
else
- echo "deploy: cross-compiling $host_arch binaries on this machine"
- rm -rf .prebuilt
- mkdir -p .prebuilt
- docker build -q -f deploy/Dockerfile.cross -t bokf-cross deploy/ >/dev/null
- docker run --rm -e "VERSION=$TAG" -v "$PWD":/src:ro \
- -v "$PWD/.prebuilt":/out bokf-cross
- if command -v file >/dev/null 2>&1 &&
- ! file .prebuilt/bokfd | grep -q aarch64; then
- echo "deploy: cross-build produced no arm64 binary" >&2
- rm -rf .prebuilt
- exit 1
- fi
+ cross_build
echo "deploy: assembling image bokf:$TAG on $BOKF_HOST"
tar -cf - \
--exclude=./.git --exclude=./build --exclude=./var --exclude=./.env \
@@ -106,20 +180,6 @@ fi
echo "deploy: starting containers"
remote "cd '$BOKF_REMOTE_DIR' && docker compose up -d --no-build"
-wait_healthy() {
- for _ in $(seq 1 30); do
- cid="$(remote "cd '$BOKF_REMOTE_DIR' && docker compose ps -q bokfd" 2>/dev/null || true)"
- if [ -n "$cid" ]; then
- st="$(remote "docker inspect --format '{{.State.Health.Status}}' '$cid'" 2>/dev/null || true)"
- if [ "$st" = "healthy" ]; then
- return 0
- fi
- fi
- sleep 2
- done
- return 1
-}
-
if wait_healthy; then
echo "deploy: bokf:$TAG is healthy on $BOKF_HOST"
exit 0
diff --git a/src/bokfd.c b/src/bokfd.c
index 944e910..933b274 100644
--- a/src/bokfd.c
+++ b/src/bokfd.c
@@ -38,6 +38,7 @@ struct conn {
};
static volatile sig_atomic_t g_stop = 0;
+static volatile sig_atomic_t g_reload = 0;
static size_t g_line_limit = 1024 * 1024;
static SSL_CTX *g_tls_ctx = NULL;
static time_t g_tls_cert_mtime = 0;
@@ -50,6 +51,12 @@ static void on_signal(int sig)
g_stop = 1;
}
+static void on_reload(int sig)
+{
+ (void)sig;
+ g_reload = 1;
+}
+
static int mkdir_p(const char *path, mode_t mode)
{
char tmp[4096];
@@ -582,6 +589,7 @@ int main(int argc, char **argv)
signal(SIGINT, on_signal);
signal(SIGTERM, on_signal);
+ signal(SIGHUP, on_reload);
signal(SIGPIPE, SIG_IGN);
sessions_init(g_cfg.session_ttl);
@@ -599,7 +607,7 @@ int main(int argc, char **argv)
int lfds[3];
int ltls[3];
- while (!g_stop) {
+ while (!g_stop && !g_reload) {
int n = 0;
int nlisteners = 0;
lfds[nlisteners] = lfd;
@@ -670,7 +678,10 @@ int main(int argc, char **argv)
nconns = keep;
}
- log_info("shutting down");
+ if (g_reload)
+ log_info("reloading on SIGHUP");
+ else
+ log_info("shutting down");
for (size_t i = 0; i < nconns; i++) {
if (conns[i].ssl)
SSL_free(conns[i].ssl);
@@ -691,5 +702,10 @@ int main(int argc, char **argv)
sessions_free_all();
sqlite3_close(db);
config_free();
+ if (g_reload) {
+ execv(argv[0], argv);
+ log_error("re-exec %s failed: %s", argv[0], strerror(errno));
+ return 1;
+ }
return 0;
}