mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 15:08:24 +00:00
feat(deploy): add portable container contract
Production deployment previously depended on an illustrative Docker snippet and did not define which state or identity must survive replacement. Add a non-root runtime image, loopback-only Compose service, optional Caddy TLS overlay, shared /data layout, bounded public verifier, and an identity-persistence container test. Document backup, proxy, single-writer, upgrade, and rollback requirements as the contract for every environment. This assumes one ngit-grasp writer per state directory and a reverse proxy or platform edge for public TLS. Image publication and provider-specific control-plane setup are deliberately left to separate changes. Validated with sh -n, ShellCheck 0.11.0, locked Cargo metadata, YAML parsing, Docker Hub tag lookups, local input-failure checks, and git diff --check. Docker/Podman is unavailable in this VM, so the included end-to-end container test was not run here.
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
.git
|
||||||
|
.direnv
|
||||||
|
.env
|
||||||
|
.relay-owner.nsec
|
||||||
|
data
|
||||||
|
result
|
||||||
|
result-*
|
||||||
|
target
|
||||||
|
work
|
||||||
|
worktrees
|
||||||
|
**/target
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
FROM rust:1.96-bookworm AS builder
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends libssl-dev pkg-config \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ARG NGIT_BUILD_REVISION=unknown
|
||||||
|
ENV NGIT_BUILD_REVISION=${NGIT_BUILD_REVISION}
|
||||||
|
|
||||||
|
RUN cargo build --locked --release -p ngit-grasp
|
||||||
|
|
||||||
|
FROM debian:bookworm-slim AS runtime
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
gosu \
|
||||||
|
libssl3 \
|
||||||
|
tini \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& groupadd --system --gid 10001 ngit-grasp \
|
||||||
|
&& useradd --system --uid 10001 --gid ngit-grasp \
|
||||||
|
--home-dir /data --no-create-home ngit-grasp \
|
||||||
|
&& install -d -m 0750 -o ngit-grasp -g ngit-grasp \
|
||||||
|
/data /data/git /data/relay
|
||||||
|
|
||||||
|
COPY --from=builder /src/target/release/ngit-grasp /usr/local/bin/ngit-grasp
|
||||||
|
COPY deploy/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
|
ARG NGIT_IMAGE_VERSION=dev
|
||||||
|
ARG NGIT_IMAGE_REVISION=unknown
|
||||||
|
LABEL org.opencontainers.image.title="ngit-grasp" \
|
||||||
|
org.opencontainers.image.description="GRASP relay and Git Smart HTTP server" \
|
||||||
|
org.opencontainers.image.licenses="MIT" \
|
||||||
|
org.opencontainers.image.source="https://gitnostr.com/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git" \
|
||||||
|
org.opencontainers.image.version="${NGIT_IMAGE_VERSION}" \
|
||||||
|
org.opencontainers.image.revision="${NGIT_IMAGE_REVISION}"
|
||||||
|
|
||||||
|
ENV HOME=/data \
|
||||||
|
NGIT_GIT_DATA_PATH=/data/git \
|
||||||
|
NGIT_RELAY_DATA_PATH=/data/relay
|
||||||
|
|
||||||
|
WORKDIR /data
|
||||||
|
VOLUME ["/data"]
|
||||||
|
EXPOSE 7334
|
||||||
|
STOPSIGNAL SIGTERM
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/docker-entrypoint.sh"]
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
services:
|
||||||
|
ngit-grasp:
|
||||||
|
environment:
|
||||||
|
NGIT_TRUSTED_PROXY_CIDRS: "${NGIT_CADDY_IP:-172.30.73.2}/32"
|
||||||
|
|
||||||
|
caddy:
|
||||||
|
image: caddy:2-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
ngit-grasp:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
NGIT_DOMAIN: "${NGIT_DOMAIN:?copy deploy.env.example to .env and set NGIT_DOMAIN}"
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
- "443:443/udp"
|
||||||
|
volumes:
|
||||||
|
- ./deploy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
- caddy-data:/data
|
||||||
|
- caddy-config:/config
|
||||||
|
networks:
|
||||||
|
backend:
|
||||||
|
ipv4_address: "${NGIT_CADDY_IP:-172.30.73.2}"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
caddy-data:
|
||||||
|
caddy-config:
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
name: ngit-grasp
|
||||||
|
|
||||||
|
services:
|
||||||
|
ngit-grasp:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
NGIT_BUILD_REVISION: "${NGIT_BUILD_REVISION:-unknown}"
|
||||||
|
NGIT_IMAGE_REVISION: "${NGIT_IMAGE_REVISION:-unknown}"
|
||||||
|
NGIT_IMAGE_VERSION: "${NGIT_IMAGE_VERSION:-dev}"
|
||||||
|
image: "${NGIT_IMAGE:-ngit-grasp:local}"
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- path: ./.env
|
||||||
|
required: false
|
||||||
|
environment:
|
||||||
|
NGIT_DOMAIN: "${NGIT_DOMAIN:?copy deploy.env.example to .env and set NGIT_DOMAIN}"
|
||||||
|
NGIT_BASE_PATH: "${NGIT_BASE_PATH:-/}"
|
||||||
|
NGIT_LOG_LEVEL: "${NGIT_LOG_LEVEL:-info}"
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${NGIT_HOST_PORT:-7334}:7334"
|
||||||
|
volumes:
|
||||||
|
- ngit-grasp-data:/data
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
stop_grace_period: 5m
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD-SHELL
|
||||||
|
- >-
|
||||||
|
curl --fail --silent --show-error
|
||||||
|
--header 'Accept: application/nostr+json'
|
||||||
|
"http://127.0.0.1:7334$${NGIT_BASE_PATH:-/}" >/dev/null
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
ngit-grasp-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
backend:
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: "${NGIT_COMPOSE_SUBNET:-172.30.73.0/24}"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Required: the public hostname, without a URL scheme or path.
|
||||||
|
NGIT_DOMAIN=ngit.example.com
|
||||||
|
|
||||||
|
# Public mount path. The bundled Caddy stack expects a domain-root service.
|
||||||
|
NGIT_BASE_PATH=/
|
||||||
|
|
||||||
|
# Loopback port used by compose.yaml when an existing host proxy is used.
|
||||||
|
NGIT_HOST_PORT=7334
|
||||||
|
|
||||||
|
# Application logging filter.
|
||||||
|
NGIT_LOG_LEVEL=info
|
||||||
|
|
||||||
|
# Optional initial relay. Sync discovers additional relays automatically.
|
||||||
|
# NGIT_SYNC_BOOTSTRAP_RELAY_URL=wss://relay.ngit.dev
|
||||||
|
|
||||||
|
# Compose network settings. Change both only if the default subnet conflicts.
|
||||||
|
NGIT_COMPOSE_SUBNET=172.30.73.0/24
|
||||||
|
NGIT_CADDY_IP=172.30.73.2
|
||||||
|
|
||||||
|
# Do not put NGIT_RELAY_OWNER_NSEC here. By default the service generates
|
||||||
|
# /data/.relay-owner.nsec in the persistent volume. Back up that volume.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{$NGIT_DOMAIN} {
|
||||||
|
reverse_proxy ngit-grasp:7334
|
||||||
|
}
|
||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
data_root=/data
|
||||||
|
git_data_path=${NGIT_GIT_DATA_PATH:-${data_root}/git}
|
||||||
|
relay_data_path=${NGIT_RELAY_DATA_PATH:-${data_root}/relay}
|
||||||
|
|
||||||
|
if [ -z "${NGIT_BIND_ADDRESS:-}" ]; then
|
||||||
|
public_port=${PORT:-7334}
|
||||||
|
case "${public_port}" in
|
||||||
|
''|*[!0-9]*)
|
||||||
|
echo "PORT must be an integer" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if [ "${public_port}" -lt 1 ] || [ "${public_port}" -gt 65535 ]; then
|
||||||
|
echo "PORT must be between 1 and 65535" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
export NGIT_BIND_ADDRESS="0.0.0.0:${public_port}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
export NGIT_GIT_DATA_PATH="${git_data_path}"
|
||||||
|
export NGIT_RELAY_DATA_PATH="${relay_data_path}"
|
||||||
|
|
||||||
|
if [ "$(id -u)" -eq 0 ]; then
|
||||||
|
install -d -m 0750 -o ngit-grasp -g ngit-grasp \
|
||||||
|
"${data_root}" "${git_data_path}" "${relay_data_path}"
|
||||||
|
|
||||||
|
if [ -e "${data_root}/.relay-owner.nsec" ]; then
|
||||||
|
chown ngit-grasp:ngit-grasp "${data_root}/.relay-owner.nsec"
|
||||||
|
chmod 0600 "${data_root}/.relay-owner.nsec"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec gosu ngit-grasp:ngit-grasp ngit-grasp "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec ngit-grasp "$@"
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# Deploy with Docker or Podman
|
||||||
|
|
||||||
|
This is the portable default for a Linux host. The repository ships a
|
||||||
|
multi-stage `Dockerfile`, a loopback-only Compose service, and an optional
|
||||||
|
Caddy overlay for automatic HTTPS.
|
||||||
|
|
||||||
|
The image contains ngit-grasp, Git, CA certificates, and a small init process.
|
||||||
|
It prepares `/data` and then runs ngit-grasp as UID/GID `10001`.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker Engine with Compose v2, or a compatible Podman Compose setup
|
||||||
|
- durable local storage for the named volume
|
||||||
|
- a domain whose DNS points to the host
|
||||||
|
- ports 80 and 443 available when using bundled Caddy
|
||||||
|
|
||||||
|
Review the [deployment contract](../reference/deployment-contract.md) before
|
||||||
|
placing the state volume on remote or managed storage.
|
||||||
|
|
||||||
|
## Fresh VPS with automatic HTTPS
|
||||||
|
|
||||||
|
Create the small deployment environment file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp deploy.env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `NGIT_DOMAIN` in `.env`, then start the relay and Caddy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f compose.yaml -f compose.caddy.yaml up --build -d
|
||||||
|
docker compose -f compose.yaml -f compose.caddy.yaml ps
|
||||||
|
scripts/verify-deployment.sh https://ngit.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Caddy obtains and renews the certificate. The relay is also published on
|
||||||
|
`127.0.0.1:7334` for local diagnostics, but it is not directly reachable from
|
||||||
|
the network.
|
||||||
|
|
||||||
|
The bundled Caddy configuration serves ngit-grasp at the domain root. To share
|
||||||
|
a hostname using `NGIT_BASE_PATH`, use an existing reverse proxy and configure
|
||||||
|
its path routing explicitly.
|
||||||
|
|
||||||
|
## Existing reverse proxy
|
||||||
|
|
||||||
|
Start only the relay service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up --build -d
|
||||||
|
curl -H 'Accept: application/nostr+json' http://127.0.0.1:7334
|
||||||
|
```
|
||||||
|
|
||||||
|
Proxy the public HTTPS hostname to `http://127.0.0.1:7334`. Preserve WebSocket
|
||||||
|
upgrades, request methods, bodies, and query strings. Forwarded client IP
|
||||||
|
headers are ignored by default; configure `NGIT_TRUSTED_PROXY_CIDRS` only after
|
||||||
|
identifying the exact container-visible proxy address and keeping the backend
|
||||||
|
private.
|
||||||
|
|
||||||
|
## State and identity
|
||||||
|
|
||||||
|
The `ngit-grasp-data` named volume is mounted at `/data` and contains all
|
||||||
|
durable state. The first start creates `/data/.relay-owner.nsec`; retaining the
|
||||||
|
volume retains the public identity advertised in NIP-11.
|
||||||
|
|
||||||
|
Inspect the resolved volume without guessing its Compose prefix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker volume inspect ngit-grasp_ngit-grasp-data
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not use `docker compose down --volumes` in production. It deletes the relay
|
||||||
|
identity, events, and Git repositories.
|
||||||
|
|
||||||
|
## Operations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Logs
|
||||||
|
docker compose logs --follow ngit-grasp
|
||||||
|
|
||||||
|
# Controlled restart
|
||||||
|
docker compose restart ngit-grasp
|
||||||
|
|
||||||
|
# Stop while retaining state
|
||||||
|
docker compose stop
|
||||||
|
|
||||||
|
# Start again
|
||||||
|
docker compose start
|
||||||
|
```
|
||||||
|
|
||||||
|
When the Caddy overlay is active, include both `-f` arguments for commands that
|
||||||
|
must operate on the complete project.
|
||||||
|
|
||||||
|
## Backup
|
||||||
|
|
||||||
|
Stop the writer, archive the complete volume, and start it again:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose stop ngit-grasp
|
||||||
|
docker run --rm \
|
||||||
|
--volume ngit-grasp_ngit-grasp-data:/data:ro \
|
||||||
|
--volume "$PWD:/backup" \
|
||||||
|
alpine:3.22 \
|
||||||
|
tar -czf /backup/ngit-grasp-data.tar.gz -C /data .
|
||||||
|
docker compose start ngit-grasp
|
||||||
|
```
|
||||||
|
|
||||||
|
Store the archive away from the Docker host. Test restoration into a separate,
|
||||||
|
non-public deployment before relying on it.
|
||||||
|
|
||||||
|
## Upgrade
|
||||||
|
|
||||||
|
Pin or check out the intended tag, read `CHANGELOG.md`, take a backup, and
|
||||||
|
rebuild without overlapping the old and new writers:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose stop ngit-grasp
|
||||||
|
docker compose build --pull ngit-grasp
|
||||||
|
docker compose up -d ngit-grasp
|
||||||
|
scripts/verify-deployment.sh https://ngit.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
For storage-changing releases, follow the linked migration guide and restore
|
||||||
|
the pre-upgrade volume snapshot before attempting a binary rollback.
|
||||||
|
|
||||||
|
## Validate the image locally
|
||||||
|
|
||||||
|
The repository includes a destructive-to-test-resources-only integration
|
||||||
|
check. It builds the image, creates uniquely named temporary container and
|
||||||
|
volume resources, replaces the container, and confirms the NIP-11 pubkey did
|
||||||
|
not change:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/test-container-deployment.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `CONTAINER_ENGINE=podman` to exercise a compatible Podman CLI.
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# Deployment contract
|
||||||
|
|
||||||
|
This reference defines the runtime assumptions shared by every supported
|
||||||
|
ngit-grasp deployment. Environment-specific guides should provide these
|
||||||
|
properties rather than inventing a different layout.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
ngit-grasp is one long-running process. It serves Git Smart HTTP, Nostr
|
||||||
|
WebSockets, NIP-11, landing pages, and Prometheus metrics from one HTTP port.
|
||||||
|
The process must:
|
||||||
|
|
||||||
|
- have `git` and trusted CA certificates available at runtime;
|
||||||
|
- receive `SIGTERM` during a controlled stop;
|
||||||
|
- have enough shutdown time to persist state and clean up temporary refs; and
|
||||||
|
- run as a non-root user after writable storage has been prepared.
|
||||||
|
|
||||||
|
## Required configuration
|
||||||
|
|
||||||
|
`NGIT_DOMAIN` is the only required application setting. It is the canonical
|
||||||
|
public hostname, without a URL scheme or path:
|
||||||
|
|
||||||
|
```text
|
||||||
|
NGIT_DOMAIN=ngit.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
When the relay is mounted below a shared hostname, set `NGIT_BASE_PATH` to the
|
||||||
|
public path. Otherwise keep the default `/`.
|
||||||
|
|
||||||
|
The listener defaults to `127.0.0.1:7334`. A container or managed platform must
|
||||||
|
override it to an externally reachable container address. The supplied image
|
||||||
|
does this automatically, using `0.0.0.0:${PORT}` when the platform provides
|
||||||
|
`PORT` and `0.0.0.0:7334` otherwise.
|
||||||
|
|
||||||
|
See the [configuration reference](configuration.md) for optional policy,
|
||||||
|
sync, curation, resource-limit, and private-service settings.
|
||||||
|
|
||||||
|
## Public endpoint
|
||||||
|
|
||||||
|
The public endpoint must preserve HTTP methods, request bodies, query strings,
|
||||||
|
and WebSocket upgrades. TLS normally terminates at Caddy, nginx, an ingress,
|
||||||
|
or the hosting platform.
|
||||||
|
|
||||||
|
Configure `NGIT_TRUSTED_PROXY_CIDRS` only when all of the following are true:
|
||||||
|
|
||||||
|
- the backend is unreachable except through the named proxies;
|
||||||
|
- the proxy overwrites or safely appends forwarding headers; and
|
||||||
|
- every trusted hop is represented by an exact address or narrow CIDR.
|
||||||
|
|
||||||
|
Leaving the setting empty is safe. The relay then ignores forwarding headers
|
||||||
|
and records the proxy address instead of the original client address.
|
||||||
|
|
||||||
|
## Durable state
|
||||||
|
|
||||||
|
Every production deployment must persist these three items together:
|
||||||
|
|
||||||
|
| Path | Contents |
|
||||||
|
| --- | --- |
|
||||||
|
| `.relay-owner.nsec` | Generated relay identity, unless an external credential supplies it |
|
||||||
|
| `git/` | Git object families, repository views, holding data, and maintenance queues |
|
||||||
|
| `relay/` | LMDB relay events, lifecycle metadata, cursors, and rejected-event state |
|
||||||
|
|
||||||
|
The container contract places them below `/data`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/data/.relay-owner.nsec
|
||||||
|
/data/git/
|
||||||
|
/data/relay/
|
||||||
|
```
|
||||||
|
|
||||||
|
The NixOS and generic systemd deployments use the same relative layout below
|
||||||
|
their configured state directory.
|
||||||
|
|
||||||
|
Back up the entire state directory from one point in time. A simple portable
|
||||||
|
backup should stop the service first; a storage-level snapshot may instead
|
||||||
|
quiesce or atomically snapshot the filesystem. A backup that omits the owner
|
||||||
|
key changes the relay identity on restore. A backup that captures Git and LMDB
|
||||||
|
at unrelated times can restore an inconsistent authorization view.
|
||||||
|
|
||||||
|
## One writer
|
||||||
|
|
||||||
|
An ngit-grasp state directory has exactly one writer. Do not:
|
||||||
|
|
||||||
|
- run multiple replicas against one filesystem or network volume;
|
||||||
|
- attach independent local volumes to replicas and load-balance between them;
|
||||||
|
- use rolling or blue/green replacement that overlaps two writers; or
|
||||||
|
- scale the service to zero when that can discard or detach its durable volume.
|
||||||
|
|
||||||
|
Run one instance and scale it vertically. Separate instances are supported
|
||||||
|
only when each has its own domain, relay identity, and state directory.
|
||||||
|
|
||||||
|
## Identity and secrets
|
||||||
|
|
||||||
|
The default is intentionally low ceremony: on first start, ngit-grasp creates
|
||||||
|
`.relay-owner.nsec` with mode `0600` in its working directory and reuses it on
|
||||||
|
later starts. Protect and back up that file.
|
||||||
|
|
||||||
|
To supply an existing identity:
|
||||||
|
|
||||||
|
- use the `relay_owner_nsec` systemd credential where available;
|
||||||
|
- mount `.relay-owner.nsec` into the persistent state directory; or
|
||||||
|
- as a less preferred container fallback, use the
|
||||||
|
`NGIT_RELAY_OWNER_NSEC` secret environment variable.
|
||||||
|
|
||||||
|
Never put an nsec in a command-line argument, image, Compose file, Nix store,
|
||||||
|
checked-in environment file, or hosting template.
|
||||||
|
|
||||||
|
## Health and verification
|
||||||
|
|
||||||
|
The HTTP listener starts only after configuration validation, storage
|
||||||
|
initialization, migrations, and startup integrity work. Until a dedicated
|
||||||
|
health route exists, platform health checks should request the configured base
|
||||||
|
path and accept any `2xx` response.
|
||||||
|
|
||||||
|
After deployment, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/verify-deployment.sh https://ngit.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
The verifier checks NIP-11, an HTTP/1.1 WebSocket upgrade, and Prometheus
|
||||||
|
metrics with bounded network deadlines. Set `VERIFY_METRICS=false` only when
|
||||||
|
metrics were deliberately disabled.
|
||||||
|
|
||||||
|
## Upgrade and rollback
|
||||||
|
|
||||||
|
Pin deployments to a reviewed tag or revision. Before changing versions:
|
||||||
|
|
||||||
|
1. read `CHANGELOG.md` and any linked migration guide;
|
||||||
|
2. take a restorable snapshot of the complete state directory;
|
||||||
|
3. stop the old writer before starting the new one;
|
||||||
|
4. wait for startup migrations and integrity summaries; and
|
||||||
|
5. run the deployment verifier.
|
||||||
|
|
||||||
|
A binary or image rollback is not necessarily a data rollback. When a release
|
||||||
|
changes storage, restore the matching pre-upgrade state snapshot before
|
||||||
|
starting the older version. The
|
||||||
|
[Git family storage upgrade guide](../how-to/upgrade-git-family-storage.md)
|
||||||
|
documents the current one-way migration boundary.
|
||||||
Executable
+95
@@ -0,0 +1,95 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
container_engine=${CONTAINER_ENGINE:-docker}
|
||||||
|
test_suffix=$$
|
||||||
|
container_name=ngit-grasp-deployment-test-${test_suffix}
|
||||||
|
volume_name=ngit-grasp-deployment-test-${test_suffix}
|
||||||
|
if [ -n "${NGIT_TEST_IMAGE:-}" ]; then
|
||||||
|
image_name=${NGIT_TEST_IMAGE}
|
||||||
|
remove_test_image=false
|
||||||
|
else
|
||||||
|
image_name=ngit-grasp:deployment-test-${test_suffix}
|
||||||
|
remove_test_image=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${container_name}" in
|
||||||
|
ngit-grasp-deployment-test-[0-9]*) ;;
|
||||||
|
*)
|
||||||
|
echo "refusing to use unexpected test resource name" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if ! command -v "${container_engine}" >/dev/null 2>&1; then
|
||||||
|
echo "missing container engine: ${container_engine}" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
for required_command in curl jq; do
|
||||||
|
if ! command -v "${required_command}" >/dev/null 2>&1; then
|
||||||
|
echo "missing required command: ${required_command}" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
"${container_engine}" rm --force "${container_name}" >/dev/null 2>&1 || true
|
||||||
|
"${container_engine}" volume rm "${volume_name}" >/dev/null 2>&1 || true
|
||||||
|
if [ "${remove_test_image}" = true ]; then
|
||||||
|
"${container_engine}" image rm "${image_name}" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT HUP INT TERM
|
||||||
|
|
||||||
|
start_container() {
|
||||||
|
"${container_engine}" run \
|
||||||
|
--detach \
|
||||||
|
--name "${container_name}" \
|
||||||
|
--publish 127.0.0.1::7334 \
|
||||||
|
--volume "${volume_name}:/data" \
|
||||||
|
--env NGIT_DOMAIN=localhost \
|
||||||
|
"${image_name}" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
relay_url() {
|
||||||
|
published_port=$("${container_engine}" port "${container_name}" 7334/tcp)
|
||||||
|
published_port=${published_port##*:}
|
||||||
|
printf 'http://127.0.0.1:%s' "${published_port}"
|
||||||
|
}
|
||||||
|
|
||||||
|
relay_pubkey() {
|
||||||
|
curl \
|
||||||
|
--fail \
|
||||||
|
--silent \
|
||||||
|
--show-error \
|
||||||
|
--connect-timeout 5 \
|
||||||
|
--max-time 15 \
|
||||||
|
--retry 30 \
|
||||||
|
--retry-all-errors \
|
||||||
|
--retry-max-time 180 \
|
||||||
|
--header 'Accept: application/nostr+json' \
|
||||||
|
"$1" | jq -er '.pubkey'
|
||||||
|
}
|
||||||
|
|
||||||
|
"${container_engine}" build --tag "${image_name}" .
|
||||||
|
"${container_engine}" volume create "${volume_name}" >/dev/null
|
||||||
|
|
||||||
|
start_container
|
||||||
|
first_url=$(relay_url)
|
||||||
|
first_pubkey=$(relay_pubkey "${first_url}")
|
||||||
|
"${container_engine}" exec "${container_name}" \
|
||||||
|
test -s /data/.relay-owner.nsec
|
||||||
|
|
||||||
|
"${container_engine}" stop --time 300 "${container_name}" >/dev/null
|
||||||
|
"${container_engine}" rm "${container_name}" >/dev/null
|
||||||
|
|
||||||
|
start_container
|
||||||
|
second_url=$(relay_url)
|
||||||
|
second_pubkey=$(relay_pubkey "${second_url}")
|
||||||
|
|
||||||
|
if [ "${first_pubkey}" != "${second_pubkey}" ]; then
|
||||||
|
echo "relay identity changed after container replacement" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "container deployment verified; relay identity persisted"
|
||||||
Executable
+99
@@ -0,0 +1,99 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: $0 <public-base-url>" >&2
|
||||||
|
echo "Example: $0 https://ngit.example.com" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$#" -ne 1 ]; then
|
||||||
|
usage
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
base_url=${1%/}
|
||||||
|
case "${base_url}" in
|
||||||
|
http://*|https://*) ;;
|
||||||
|
*)
|
||||||
|
echo "public base URL must start with http:// or https://" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
for required_command in curl grep mktemp; do
|
||||||
|
if ! command -v "${required_command}" >/dev/null 2>&1; then
|
||||||
|
echo "missing required command: ${required_command}" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
temporary_directory=$(mktemp -d)
|
||||||
|
cleanup() {
|
||||||
|
rm -rf -- "${temporary_directory}"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT HUP INT TERM
|
||||||
|
|
||||||
|
nip11_body=${temporary_directory}/nip11.json
|
||||||
|
websocket_headers=${temporary_directory}/websocket.headers
|
||||||
|
|
||||||
|
curl \
|
||||||
|
--fail \
|
||||||
|
--silent \
|
||||||
|
--show-error \
|
||||||
|
--connect-timeout 5 \
|
||||||
|
--max-time 15 \
|
||||||
|
--retry 20 \
|
||||||
|
--retry-all-errors \
|
||||||
|
--retry-max-time 120 \
|
||||||
|
--header 'Accept: application/nostr+json' \
|
||||||
|
--output "${nip11_body}" \
|
||||||
|
"${base_url}"
|
||||||
|
|
||||||
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
jq -e '
|
||||||
|
(.pubkey | type == "string" and length == 64) and
|
||||||
|
(.supported_nips | type == "array")
|
||||||
|
' "${nip11_body}" >/dev/null
|
||||||
|
else
|
||||||
|
grep -q '"pubkey"' "${nip11_body}"
|
||||||
|
grep -q '"supported_nips"' "${nip11_body}"
|
||||||
|
fi
|
||||||
|
echo "ok: NIP-11 relay information"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
curl \
|
||||||
|
--http1.1 \
|
||||||
|
--silent \
|
||||||
|
--show-error \
|
||||||
|
--connect-timeout 5 \
|
||||||
|
--max-time 5 \
|
||||||
|
--dump-header "${websocket_headers}" \
|
||||||
|
--output /dev/null \
|
||||||
|
--header 'Connection: Upgrade' \
|
||||||
|
--header 'Upgrade: websocket' \
|
||||||
|
--header 'Sec-WebSocket-Version: 13' \
|
||||||
|
--header 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
|
||||||
|
"${base_url}"
|
||||||
|
websocket_curl_status=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if ! grep -Eq '^HTTP/[0-9.]+ 101([[:space:]]|$)' "${websocket_headers}"; then
|
||||||
|
echo "WebSocket upgrade failed (curl status ${websocket_curl_status})" >&2
|
||||||
|
cat "${websocket_headers}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "ok: WebSocket upgrade"
|
||||||
|
|
||||||
|
if [ "${VERIFY_METRICS:-true}" = true ]; then
|
||||||
|
curl \
|
||||||
|
--fail \
|
||||||
|
--silent \
|
||||||
|
--show-error \
|
||||||
|
--connect-timeout 5 \
|
||||||
|
--max-time 15 \
|
||||||
|
--output /dev/null \
|
||||||
|
"${base_url}/metrics"
|
||||||
|
echo "ok: Prometheus metrics"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "deployment verified: ${base_url}"
|
||||||
Reference in New Issue
Block a user