diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 71760f801f1f86606cb6bdc80028ead87d8c3584..5137f77b92a1b5fc039e640e30a10ea0a4c3cb6c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -23,23 +23,36 @@
       - run: cargo build --locked --release
 
   release:
+    name: release (${{ matrix.platform.target }})
     strategy:
       fail-fast: false
       matrix:
-        os: [ubuntu-latest, macos-latest]
-    runs-on: ${{ matrix.os }}
+        platform:
+          - runner: ubuntu-24.04
+            target: x86_64-unknown-linux-gnu
+          - runner: ubuntu-24.04-arm
+            target: aarch64-unknown-linux-gnu
+          - runner: macos-15-intel
+            target: x86_64-apple-darwin
+          - runner: macos-15
+            target: aarch64-apple-darwin
+    runs-on: ${{ matrix.platform.runner }}
     steps:
       - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
         with:
           persist-credentials: false
       - run: cargo --version
+      - name: Verify the Rust host target
+        run: |
+          test "$(rustc -vV | awk '/^host: / { print $2 }')" = \
+            "${{ matrix.platform.target }}"
       - run: cargo test --locked --all-targets --all-features
       - run: cargo test --locked --release --lib
       - run: cargo test --locked --release --lib git_reads_tests::measures_bounded_search_without_an_index -- --ignored --nocapture
       - run: "cargo test --locked --release --lib sqlite_workload_tests:: -- --ignored --nocapture"
       - run: cargo build --locked --release
       - run: wc -c target/release/tit
-      - run: ./scripts/package-release target/release/tit dist
+      - run: ./scripts/package-release target/release/tit dist ${{ matrix.platform.target }}
       - name: Verify the release artifact
         run: |
           set -- dist/*.tar.gz
@@ -51,7 +64,52 @@
       - name: Store the release artifact
         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
         with:
-          name: tit-${{ runner.os }}-${{ runner.arch }}
+          name: tit-${{ matrix.platform.target }}
+          path: dist/*
+          if-no-files-found: error
+
+  linux-armv7:
+    name: release (armv7-unknown-linux-gnueabihf)
+    runs-on: ubuntu-24.04
+    timeout-minutes: 90
+    env:
+      CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER: arm-linux-gnueabihf-gcc
+      CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_RUNNER: scripts/run-linux-armv7
+      QEMU_LD_PREFIX: /usr/arm-linux-gnueabihf
+      TARGET: armv7-unknown-linux-gnueabihf
+      TIT_RELEASE_RUNNER: ${{ github.workspace }}/scripts/run-linux-armv7
+    steps:
+      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+        with:
+          persist-credentials: false
+      - name: Install the ARMv7 tools
+        run: |
+          sudo apt-get update
+          sudo apt-get install --no-install-recommends \
+            gcc-arm-linux-gnueabihf \
+            libc6-dev-armhf-cross \
+            qemu-user-binfmt
+          sudo update-binfmts --enable qemu-arm
+          rustup target add "$TARGET"
+      - run: cargo --version
+      - run: cargo test --locked --all-targets --all-features --target "$TARGET"
+      - run: cargo test --locked --release --lib --target "$TARGET"
+      - run: cargo build --locked --release --target "$TARGET"
+      - run: wc -c "target/$TARGET/release/tit"
+      - run: ./scripts/package-release "target/$TARGET/release/tit" dist "$TARGET"
+      - name: Verify the release artifact
+        run: |
+          set -- dist/*.tar.gz
+          ./scripts/verify-release-artifact "$1" "$1.sha256"
+          mkdir release-test
+          tar -xzf "$1" -C release-test
+          set -- release-test/tit-*/bin/tit
+          TIT_RELEASE_BINARY="$(pwd)/$1" \
+            cargo test --locked --release --target "$TARGET" --test serve
+      - name: Store the release artifact
+        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+        with:
+          name: tit-armv7-unknown-linux-gnueabihf
           path: dist/*
           if-no-files-found: error
 
@@ -67,16 +125,19 @@
           - name: FreeBSD 15.1
             os: freebsd
             version: '15.1'
+            target: x86_64-unknown-freebsd
             install: sudo pkg install -y git rust
             experimental: false
           - name: OpenBSD 7.9
             os: openbsd
             version: '7.9'
+            target: x86_64-unknown-openbsd
             install: sudo pkg_add git rust
             experimental: true
           - name: NetBSD 10.1
             os: netbsd
             version: '10.1'
+            target: x86_64-unknown-netbsd
             install: sudo pkgin -y install git-base rust
             experimental: false
     env:
@@ -99,10 +160,14 @@
       - name: Install build and test tools
         run: ${{ matrix.platform.install }}
       - run: cargo --version
+      - name: Verify the Rust host target
+        run: |
+          test "$(rustc -vV | awk '/^host: / { print $2 }')" = \
+            "${{ matrix.platform.target }}"
       - run: cargo test --locked --release --all-targets --all-features
       - run: cargo build --locked --release
       - run: wc -c target/release/tit
-      - run: ./scripts/package-release target/release/tit dist
+      - run: ./scripts/package-release target/release/tit dist ${{ matrix.platform.target }}
       - name: Verify the release artifact
         run: |
           set -- dist/*.tar.gz
@@ -110,13 +175,13 @@
       - name: Store the release artifact
         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
         with:
-          name: tit-${{ matrix.platform.os }}-X64
+          name: tit-${{ matrix.platform.target }}
           path: dist/*
           if-no-files-found: error
 
   publish:
     if: startsWith(github.ref, 'refs/tags/v')
-    needs: [quality, release, bsd]
+    needs: [quality, release, linux-armv7, bsd]
     runs-on: ubuntu-latest
     permissions:
       contents: write
@@ -129,7 +194,7 @@
         env:
           GH_TOKEN: ${{ github.token }}
         run: |
-          set -- release-artifacts/tit-Linux-*/*.tar.gz
+          set -- release-artifacts/tit-x86_64-unknown-linux-gnu/*.tar.gz
           case "$(basename "$1")" in
             "tit-${GITHUB_REF_NAME#v}-"*) ;;
             *) echo "The tag does not match the package version." >&2; exit 1 ;;
diff --git a/README.md b/README.md
index 0614f38501ef4584901d2b37f8a27dd1699610d3..6668d38d7c9a4f6a29b104b245923173c52803de 100644
--- a/README.md
+++ b/README.md
@@ -250,6 +250,19 @@
 completions, the manual page, the example configuration, this README, and the
 license.
 
+The release workflow publishes packages for these Rust targets:
+
+- `x86_64-unknown-linux-gnu`;
+- `aarch64-unknown-linux-gnu`;
+- `armv7-unknown-linux-gnueabihf`;
+- `x86_64-apple-darwin`;
+- `aarch64-apple-darwin`;
+- `x86_64-unknown-freebsd`;
+- `x86_64-unknown-netbsd`.
+
+The workflow also builds an experimental package for
+`x86_64-unknown-openbsd`.
+
 ## Development
 
 Read [CONTRIBUTING.md](CONTRIBUTING.md) before you change code. Run the quality
diff --git a/scripts/package-release b/scripts/package-release
index 004ba92e9d7a78ba3ea6df06b1c82c17da0063a9..a73b543597d167948f75a9fc1f6afdc81d59131c 100755
--- a/scripts/package-release
+++ b/scripts/package-release
@@ -4,6 +4,8 @@
 project_dir=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
 binary=${1:-"$project_dir/target/release/tit"}
 output_dir=${2:-"$project_dir/dist"}
+target=${3:-}
+release_runner=${TIT_RELEASE_RUNNER:-}
 
 case "$binary" in
     /*) ;;
@@ -15,7 +17,11 @@
     exit 1
 fi
 
-set -- $("$binary" --version)
+if [ -n "$release_runner" ]; then
+    set -- $("$release_runner" "$binary" --version)
+else
+    set -- $("$binary" --version)
+fi
 if [ "$#" -ne 2 ] || [ "$1" != "tit" ]; then
     echo "tit: the release executable has invalid version output" >&2
     exit 1
@@ -25,9 +31,11 @@
     *[!0-9A-Za-z.-]* | "") echo "tit: the release version is not valid" >&2; exit 1 ;;
 esac
 
-target=$(rustc -vV | awk '/^host: / { print $2 }')
+if [ -z "$target" ]; then
+    target=$(rustc -vV | awk '/^host: / { print $2 }')
+fi
 case "$target" in
-    *[!0-9A-Za-z_.-]* | "") echo "tit: the Rust host target is not valid" >&2; exit 1 ;;
+    *[!0-9A-Za-z_.-]* | "") echo "tit: the Rust target is not valid" >&2; exit 1 ;;
 esac
 
 mkdir -p "$output_dir"
diff --git a/scripts/run-linux-armv7 b/scripts/run-linux-armv7
new file mode 100755
index 0000000000000000000000000000000000000000..368201bfbd2ef3b2404c445ea6d0e34e09aa5472
--- /dev/null
+++ b/scripts/run-linux-armv7
@@ -1,0 +1,4 @@
+#!/bin/sh
+set -eu
+
+exec /usr/bin/qemu-arm -L /usr/arm-linux-gnueabihf "$@"
diff --git a/scripts/verify-release-artifact b/scripts/verify-release-artifact
index 47ddac347cc06ddb5315cb77e5fbabf6fd3ce6dd..8a8e7a9b214324a5705c1165487c78c92b996a26 100755
--- a/scripts/verify-release-artifact
+++ b/scripts/verify-release-artifact
@@ -8,6 +8,7 @@
 
 archive=$1
 checksum=$2
+release_runner=${TIT_RELEASE_RUNNER:-}
 archive_dir=$(CDPATH= cd "$(dirname "$archive")" && pwd)
 archive_name=$(basename "$archive")
 checksum_name=$(basename "$checksum")
@@ -53,6 +54,15 @@
 package="$work/$root"
 binary="$package/bin/tit"
 
+run_binary()
+{
+    if [ -n "$release_runner" ]; then
+        "$release_runner" "$binary" "$@"
+    else
+        "$binary" "$@"
+    fi
+}
+
 for required in \
     "$binary" \
     "$package/LICENSE" \
@@ -96,15 +106,30 @@
 EOF
 chmod 600 "$config"
 
+run_clean_binary()
+{
+    if [ -n "$release_runner" ]; then
+        env -i PATH="$empty_path" HOME="$home" TMPDIR="$work" \
+            "$release_runner" "$binary" "$@"
+    else
+        env -i PATH="$empty_path" HOME="$home" TMPDIR="$work" \
+            "$binary" "$@"
+    fi
+}
+
 key='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJgOfb9397gwZRfsz2A6DN/VVw/+bdjMdVsJ89JDLAC0 tit-release-verification'
 setup_output="$work/setup.out"
-env -i PATH="$empty_path" HOME="$home" TMPDIR="$work" \
-    "$binary" --config "$config" setup admin release-test "$key" >"$setup_output"
+run_clean_binary --config "$config" setup admin release-test "$key" >"$setup_output"
 grep -q '^Recovery code: tit-recovery-v1:' "$setup_output"
 
 server_log="$work/server.log"
-env -i PATH="$empty_path" HOME="$home" TMPDIR="$work" \
-    "$binary" --config "$config" serve 2>"$server_log" &
+if [ -n "$release_runner" ]; then
+    env -i PATH="$empty_path" HOME="$home" TMPDIR="$work" \
+        "$release_runner" "$binary" --config "$config" serve 2>"$server_log" &
+else
+    env -i PATH="$empty_path" HOME="$home" TMPDIR="$work" \
+        "$binary" --config "$config" serve 2>"$server_log" &
+fi
 server_pid=$!
 sleep 1
 if ! kill -0 "$server_pid" 2>/dev/null; then
@@ -116,26 +141,20 @@
 kill -TERM "$server_pid"
 wait "$server_pid"
 
-env -i PATH="$empty_path" HOME="$home" TMPDIR="$work" \
-    "$binary" --config "$config" doctor
+run_clean_binary --config "$config" doctor
 backup="$work/tit-backup.tar"
-env -i PATH="$empty_path" HOME="$home" TMPDIR="$work" \
-    "$binary" --config "$config" backup "$backup"
-env -i PATH="$empty_path" HOME="$home" TMPDIR="$work" \
-    "$binary" --config "$config" doctor --backup "$backup"
+run_clean_binary --config "$config" backup "$backup"
+run_clean_binary --config "$config" doctor --backup "$backup"
 
 : >"$instance/tit.sqlite3"
-if env -i PATH="$empty_path" HOME="$home" TMPDIR="$work" \
-    "$binary" --config "$config" doctor >/dev/null 2>&1
+if run_clean_binary --config "$config" doctor >/dev/null 2>&1
 then
     echo "tit: doctor accepted the damaged disposable instance" >&2
     exit 1
 fi
 
-env -i PATH="$empty_path" HOME="$home" TMPDIR="$work" \
-    "$binary" restore "$backup" "$restored"
-env -i PATH="$empty_path" HOME="$home" TMPDIR="$work" \
-    "$binary" --config "$restored/config.toml" doctor
+run_clean_binary restore "$backup" "$restored"
+run_clean_binary --config "$restored/config.toml" doctor
 
 recovery=$(sed -n 's/^Recovery code: //p' "$setup_output")
 if grep -F "$recovery" "$server_log" >/dev/null; then
@@ -143,5 +162,5 @@
     exit 1
 fi
 
-version=$("$binary" --version)
+version=$(run_binary --version)
 printf 'verified %s from %s without commands in PATH\n' "$version" "$archive_name"
