README: install and update from object storage

This commit is contained in:
svesnav
2026-07-27 13:04:23 +03:00
parent 3faa4927cf
commit 0ae771786a
2 changed files with 70 additions and 32 deletions
+11 -14
View File
@@ -17,15 +17,11 @@ One command sets up a host. Run it as root; it asks what the host should be
`/opt/runix`, and wires up systemd. `/opt/runix`, and wires up systemd.
```sh ```sh
curl -fsSL https://vcs.astra-dev.online/svesnav/runix/raw/branch/main/install.sh | sudo sh curl -fsSL https://runix-releases.s3-alpha-web.astra-dev.online/install.sh | sudo sh
``` ```
Prefer to pin a version, or your instance still requires sign-in to read raw Binaries are served from object storage, so this needs no account and no
files? Use the release asset instead — it downloads without a login: token. To pin a version, add `--version v0.5.0`.
```sh
curl -fsSL https://vcs.astra-dev.online/svesnav/runix/releases/download/v0.5.0/install.sh | sudo sh -s -- --role all-in-one
```
### Roles ### Roles
@@ -56,16 +52,15 @@ services restarted, while your configuration (above all the JWT and
encryption secrets) is preserved: encryption secrets) is preserved:
```sh ```sh
curl -fsSL https://vcs.astra-dev.online/svesnav/runix/raw/branch/main/install.sh | sudo sh -- -y curl -fsSL https://runix-releases.s3-alpha-web.astra-dev.online/install.sh | sudo sh -s -- -y
``` ```
Or update from the UI: an administrator sees an **Updates** page that checks Or update from the UI: an administrator sees an **Updates** page that checks
this repository for the latest release and updates the control plane and each object storage for the latest release and updates the control plane and each
agent with one click. Point a control plane at this repository with: agent with one click. Point a control plane at this repository with:
``` ```
RUNIX_UPDATE_API_BASE=https://vcs.astra-dev.online/api/v1 RUNIX_UPDATE_MANIFEST=https://runix-releases.s3-alpha-web.astra-dev.online/latest.json
RUNIX_UPDATE_REPO=svesnav/runix
``` ```
## Releases ## Releases
@@ -78,11 +73,13 @@ Each release publishes, for `linux/amd64` and `linux/arm64`:
- `SHA256SUMS` — checksums; the installer verifies every download against it - `SHA256SUMS` — checksums; the installer verifies every download against it
Binaries are built automatically from source on tag by the CI runner and Binaries are built automatically from source on tag by the CI runner and
published here and to the source repository. published to object storage as well as to this repository. `latest.json`
beside them names the newest version and every asset's checksum — that is
what the installer and the in-app updater read.
## Verify a download by hand ## Verify a download by hand
```sh ```sh
curl -fsSLO https://vcs.astra-dev.online/svesnav/runix/releases/download/v0.5.0/runix-server_linux_amd64 curl -fsSLO https://runix-releases.s3-alpha-web.astra-dev.online/v0.5.0/runix-server_linux_amd64
curl -fsSL https://vcs.astra-dev.online/svesnav/runix/releases/download/v0.5.0/SHA256SUMS | sha256sum -c --ignore-missing curl -fsSL https://runix-releases.s3-alpha-web.astra-dev.online/v0.5.0/SHA256SUMS | sha256sum -c --ignore-missing
``` ```
+59 -18
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# Runix installer — the one command that sets a host up. # Runix installer — the one command that sets a host up.
# #
# curl -fsSL https://vcs.astra-dev.online/svesnav/runix/raw/branch/main/install.sh | sudo sh # curl -fsSL https://runix-releases.s3-alpha-web.astra-dev.online/install.sh | sudo sh
# #
# Asks what this host should be (control plane, agent, or both), checks # Asks what this host should be (control plane, agent, or both), checks
# the prerequisites, installs under /opt/runix and wires up systemd. # the prerequisites, installs under /opt/runix and wires up systemd.
@@ -19,9 +19,15 @@
set -eu set -eu
PREFIX=${RUNIX_PREFIX:-/opt/runix} PREFIX=${RUNIX_PREFIX:-/opt/runix}
# Releases are served from a Gitea instance by default. Point GITEA at a # Releases are served from object storage by default: it needs no account,
# GitHub-style host by clearing it (RUNIX_GITEA=) and setting a download # so a plain `curl … | sh` works on a fresh box. The layout is
# base, if you mirror the release elsewhere. #
# <base>/latest.json names the newest version
# <base>/<version>/<asset> the binaries, install.sh and SHA256SUMS
#
# Clear it (RUNIX_S3=) to fall back to the git forge below.
S3_BASE=${RUNIX_S3:-https://runix-releases.s3-alpha-web.astra-dev.online}
# The forge is the fallback source, used when S3_BASE is empty.
GITEA=${RUNIX_GITEA:-https://vcs.astra-dev.online} GITEA=${RUNIX_GITEA:-https://vcs.astra-dev.online}
REPO=${RUNIX_REPO:-svesnav/runix} REPO=${RUNIX_REPO:-svesnav/runix}
VERSION=${RUNIX_VERSION:-latest} VERSION=${RUNIX_VERSION:-latest}
@@ -86,8 +92,9 @@ Options:
--server-binary PATH Install a local control-plane build --server-binary PATH Install a local control-plane build
--agent-binary PATH Install a local agent build --agent-binary PATH Install a local agent build
--version VERSION Release to install (default: latest) --version VERSION Release to install (default: latest)
--repo OWNER/NAME Release repository (default: $REPO) --s3 URL Object store serving releases (default: $S3_BASE)
--gitea URL Gitea instance serving releases (default: $GITEA) --repo OWNER/NAME Release repository, when not using --s3
--gitea URL Gitea instance serving releases (fallback source)
--repo-token TOK Read token, if the instance requires sign-in --repo-token TOK Read token, if the instance requires sign-in
--prefix PATH Install root (default: $PREFIX) --prefix PATH Install root (default: $PREFIX)
--no-start Install and configure, but do not start services --no-start Install and configure, but do not start services
@@ -95,7 +102,7 @@ Options:
-h, --help Show this help -h, --help Show this help
Environment equivalents: RUNIX_PREFIX, RUNIX_VERSION, RUNIX_REPO, Environment equivalents: RUNIX_PREFIX, RUNIX_VERSION, RUNIX_REPO,
RUNIX_GITEA, RUNIX_TOKEN, RUNIX_DATABASE_DSN, RUNIX_ADMIN_PASSWORD, RUNIX_S3, RUNIX_GITEA, RUNIX_TOKEN, RUNIX_DATABASE_DSN, RUNIX_ADMIN_PASSWORD,
RUNIX_HTTP_HOST, RUNIX_AGENT_SERVER_URL, RUNIX_AGENT_TOKEN, RUNIX_HTTP_HOST, RUNIX_AGENT_SERVER_URL, RUNIX_AGENT_TOKEN,
RUNIX_POSTGRES_PORT. RUNIX_POSTGRES_PORT.
@@ -125,7 +132,8 @@ while [ $# -gt 0 ]; do
--agent-binary) AGENT_BIN="$2"; shift 2 ;; --agent-binary) AGENT_BIN="$2"; shift 2 ;;
--version) VERSION="$2"; shift 2 ;; --version) VERSION="$2"; shift 2 ;;
--repo) REPO="$2"; shift 2 ;; --repo) REPO="$2"; shift 2 ;;
--gitea) GITEA="$2"; shift 2 ;; --s3) S3_BASE="$2"; shift 2 ;;
--gitea) GITEA="$2"; S3_BASE=""; shift 2 ;;
--repo-token|--github-token) DL_TOKEN="$2"; shift 2 ;; --repo-token|--github-token) DL_TOKEN="$2"; shift 2 ;;
--prefix) PREFIX="$2"; shift 2 ;; --prefix) PREFIX="$2"; shift 2 ;;
--no-start) NO_START=1; shift ;; --no-start) NO_START=1; shift ;;
@@ -136,7 +144,9 @@ while [ $# -gt 0 ]; do
done done
if [ -z "$DOWNLOAD_BASE" ]; then if [ -z "$DOWNLOAD_BASE" ]; then
if [ -n "$GITEA" ]; then if [ -n "$S3_BASE" ]; then
DOWNLOAD_BASE="$S3_BASE"
elif [ -n "$GITEA" ]; then
DOWNLOAD_BASE="$GITEA/$REPO/releases" DOWNLOAD_BASE="$GITEA/$REPO/releases"
else else
DOWNLOAD_BASE="https://github.com/$REPO/releases" DOWNLOAD_BASE="https://github.com/$REPO/releases"
@@ -802,6 +812,16 @@ RESOLVED_TAG=""
resolve_tag() { resolve_tag() {
if [ "$VERSION" != latest ]; then RESOLVED_TAG="$VERSION"; return 0; fi if [ "$VERSION" != latest ]; then RESOLVED_TAG="$VERSION"; return 0; fi
[ -n "$RESOLVED_TAG" ] && return 0 [ -n "$RESOLVED_TAG" ] && return 0
# Object storage serves no API, so the newest version is named in a
# small manifest published beside the binaries.
if [ -n "$S3_BASE" ]; then
_man=$(mktemp)
if fetch "$S3_BASE/latest.json" "$_man" "application/json"; then
RESOLVED_TAG=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$_man" | head -n1)
fi
rm -f "$_man"
[ -n "$RESOLVED_TAG" ] && return 0
fi
if [ -n "$GITEA" ]; then if [ -n "$GITEA" ]; then
_loc=$(redirect_of "$DOWNLOAD_BASE/latest" 2>/dev/null || true) _loc=$(redirect_of "$DOWNLOAD_BASE/latest" 2>/dev/null || true)
RESOLVED_TAG=$(printf '%s' "$_loc" | sed -n 's#.*/releases/tag/##p') RESOLVED_TAG=$(printf '%s' "$_loc" | sed -n 's#.*/releases/tag/##p')
@@ -843,19 +863,26 @@ asset_id() {
# try_download NAME DEST — quiet, returns non-zero if the asset is absent. # try_download NAME DEST — quiet, returns non-zero if the asset is absent.
try_download() { try_download() {
_name=$1; _dest=$2 # Distinct variable names: POSIX sh has no locals, and resolve_tag
if [ -n "$GITEA" ]; then # below calls fetch, which would otherwise overwrite the _dest we were
# handed — sending the binary to the manifest's temp file instead of
# where the caller expects it.
_tdname=$1; _tddest=$2
if [ -n "$S3_BASE" ]; then
resolve_tag || return 1
fetch "$S3_BASE/$RESOLVED_TAG/$_tdname" "$_tddest" || return 1
elif [ -n "$GITEA" ]; then
# Gitea assets download by tag; the token (if any) rides in fetch. # Gitea assets download by tag; the token (if any) rides in fetch.
resolve_tag || return 1 resolve_tag || return 1
fetch "$DOWNLOAD_BASE/download/$RESOLVED_TAG/$_name" "$_dest" || return 1 fetch "$DOWNLOAD_BASE/download/$RESOLVED_TAG/$_tdname" "$_tddest" || return 1
elif [ -n "$DL_TOKEN" ]; then elif [ -n "$DL_TOKEN" ]; then
_id=$(asset_id "$_name") || return 1 _id=$(asset_id "$_tdname") || return 1
fetch "https://api.github.com/repos/$REPO/releases/assets/$_id" \ fetch "https://api.github.com/repos/$REPO/releases/assets/$_id" \
"$_dest" "application/octet-stream" || return 1 "$_tddest" "application/octet-stream" || return 1
elif [ "$VERSION" = latest ]; then elif [ "$VERSION" = latest ]; then
fetch "$DOWNLOAD_BASE/latest/download/$_name" "$_dest" || return 1 fetch "$DOWNLOAD_BASE/latest/download/$_tdname" "$_tddest" || return 1
else else
fetch "$DOWNLOAD_BASE/download/$VERSION/$_name" "$_dest" || return 1 fetch "$DOWNLOAD_BASE/download/$VERSION/$_tdname" "$_tddest" || return 1
fi fi
} }
@@ -1016,6 +1043,14 @@ install_server() {
mkdir -p "$PREFIX" mkdir -p "$PREFIX"
install_binary runix-server "$SERVER_BIN" install_binary runix-server "$SERVER_BIN"
# Applying an update from the UI replaces the binary in place, which
# means writing a new file into this directory and renaming it over the
# old one — so the service user has to own the directory, not just the
# file. Without this the update fails with a bare permission error long
# after the download has succeeded.
chown "$SERVER_USER" "$BIN_DIR" 2>/dev/null || true
chown "$SERVER_USER" "$BIN_DIR/runix-server" 2>/dev/null || true
[ "$DB_MODE" = docker ] && provision_postgres [ "$DB_MODE" = docker ] && provision_postgres
[ -n "$DSN" ] || fail "no database configured" [ -n "$DSN" ] || fail "no database configured"
@@ -1094,7 +1129,11 @@ NoNewPrivileges=yes
PrivateTmp=yes PrivateTmp=yes
ProtectSystem=strict ProtectSystem=strict
ProtectHome=yes ProtectHome=yes
ReadWritePaths=$CONFIG_DIR # The binary directory is writable so the control plane can replace its own
# binary when an administrator applies an update from the UI. That is the
# price of in-app updates: the service can rewrite the file it runs from.
# Drop \$BIN_DIR here to forbid it, and upgrade by re-running this script.
ReadWritePaths=$CONFIG_DIR $BIN_DIR
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
@@ -1376,7 +1415,9 @@ fi
if [ "$ROLE" = server ]; then if [ "$ROLE" = server ]; then
echo echo
echo " Add hosts from the UI (Servers → Add server), then run on each:" echo " Add hosts from the UI (Servers → Add server), then run on each:"
if [ -n "$GITEA" ]; then if [ -n "$S3_BASE" ]; then
say " curl -fsSL $S3_BASE/install.sh | sudo sh -s -- \\"
elif [ -n "$GITEA" ]; then
say " curl -fsSL $GITEA/$REPO/raw/branch/main/install.sh | sudo sh -s -- \\" say " curl -fsSL $GITEA/$REPO/raw/branch/main/install.sh | sudo sh -s -- \\"
else else
say " curl -fsSL $DOWNLOAD_BASE/latest/download/install.sh | sudo sh -s -- \\" say " curl -fsSL $DOWNLOAD_BASE/latest/download/install.sh | sudo sh -s -- \\"