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