mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 15:08:24 +00:00
Merge #66c9386e: Add portable deployment paths and managed hosting temp…
Add portable deployment paths and managed hosting templates nostr:nevent1qqsxdjfcdmr88n4pe79h4st43kvkwzssekuwepmsl7dt3ze2x0a27scpz3mhxue69uhhyetvv9ujumn8d96zuer9wc5fxfyx PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: Define one durable single-writer deployment contract, then implement it with a non-root Docker image, Compose and Caddy, a generic systemd unit, and separate NixOS, Linux, and Proxmox guides. Add Railway, Render, and Fly.io templates that share the same /data layout, plus bounded public verification and a container replacement test that proves the relay identity survives. The dedicated container e2e workflow expects the ngit-ci KVM runner to opt in to exposing its disposable guest Docker socket; it fails clearly if that prerequisite is absent.
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
.git
|
||||
.direnv
|
||||
.env
|
||||
.relay-owner.nsec
|
||||
data
|
||||
result
|
||||
result-*
|
||||
target
|
||||
work
|
||||
worktrees
|
||||
**/target
|
||||
@@ -0,0 +1,38 @@
|
||||
# ngit-ci currently evaluates push path filters but not pull-request path
|
||||
# filters, so PRs run this workflow unconditionally.
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- ".dockerignore"
|
||||
- "Cargo.lock"
|
||||
- "Cargo.toml"
|
||||
- "Dockerfile"
|
||||
- "build.rs"
|
||||
- "compose*.yaml"
|
||||
- "deploy/**"
|
||||
- "scripts/test-container-deployment.sh"
|
||||
- "src/**"
|
||||
pull_request:
|
||||
|
||||
name: Container deployment e2e
|
||||
|
||||
jobs:
|
||||
container-deployment:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: cachix/install-nix-action@v31
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- name: Require the disposable guest container daemon
|
||||
run: |
|
||||
if [[ ! -S /var/run/docker.sock ]]; then
|
||||
echo "container daemon socket is not available" >&2
|
||||
echo "this workflow requires an ngit-ci operator opt-in" >&2
|
||||
exit 1
|
||||
fi
|
||||
- name: Build, replace, and verify the container
|
||||
run: |
|
||||
nix shell nixpkgs#docker-client nixpkgs#curl nixpkgs#jq \
|
||||
--command scripts/test-container-deployment.sh
|
||||
@@ -59,7 +59,7 @@ forces `ExecStart`, which coerces `src = ../.`; a standalone local path is not
|
||||
Git-filtered and may hash or copy ignored `target/` and worktree data. Use the
|
||||
Git-backed flake module, or a builder stub that ignores all build attributes and
|
||||
keeps `src` lazy. See
|
||||
[`docs/how-to/deploy.md`](docs/how-to/deploy.md#resource-safe-module-validation)
|
||||
[`docs/how-to/deploy-nixos.md`](docs/how-to/deploy-nixos.md#resource-safe-module-validation)
|
||||
for resource-safe validation and deployment.
|
||||
|
||||
### Testing ngit-grasp (Main Project)
|
||||
|
||||
+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"]
|
||||
@@ -406,9 +406,22 @@ This a useful feature of other git servers.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Tagged releases provide a statically linked x86_64 Linux archive accompanied
|
||||
by `SHA256SUMS`. The archive can be installed without a Rust or Nix toolchain;
|
||||
on a Linux flake system, build the same output with `nix build .#static`.
|
||||
For production, start with the [deployment chooser](docs/how-to/deploy.md).
|
||||
The repository ships Docker and Compose configurations, a NixOS module, a
|
||||
hardened systemd unit, and templates for selected managed hosts. All supported
|
||||
paths preserve the same
|
||||
[deployment contract](docs/reference/deployment-contract.md).
|
||||
|
||||
The shortest fresh-VPS path uses Docker Compose and Caddy:
|
||||
|
||||
```bash
|
||||
cp deploy.env.example .env
|
||||
# Set NGIT_DOMAIN in .env and point DNS at this server.
|
||||
docker compose -f compose.yaml -f compose.caddy.yaml up --build -d
|
||||
scripts/verify-deployment.sh https://ngit.example.com
|
||||
```
|
||||
|
||||
For development from source:
|
||||
|
||||
```bash
|
||||
# install ngit
|
||||
@@ -443,7 +456,9 @@ nix develop -c cargo test --lib
|
||||
- Purgatory system activates, ready to hunt for missing git data
|
||||
- Prometheus metrics exposed at `<base-path>/metrics`
|
||||
|
||||
**Don't have Nix?** See [Getting Started Tutorial](docs/tutorials/getting-started.md) for alternative setup methods.
|
||||
**Don't have Nix?** Use the container path above or see the
|
||||
[Getting Started Tutorial](docs/tutorials/getting-started.md) for development
|
||||
alternatives.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
@@ -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,15 @@
|
||||
# Required canonical public hostname, without a scheme or path.
|
||||
NGIT_DOMAIN=ngit.example.com
|
||||
|
||||
NGIT_BASE_PATH=/
|
||||
NGIT_BIND_ADDRESS=127.0.0.1:7334
|
||||
NGIT_GIT_DATA_PATH=/var/lib/ngit-grasp/git
|
||||
NGIT_RELAY_DATA_PATH=/var/lib/ngit-grasp/relay
|
||||
NGIT_DATABASE_BACKEND=lmdb
|
||||
NGIT_LOG_LEVEL=info
|
||||
|
||||
# Optional initial relay. Sync discovers additional relays automatically.
|
||||
# NGIT_SYNC_BOOTSTRAP_RELAY_URL=wss://relay.ngit.dev
|
||||
|
||||
# Prefer /var/lib/ngit-grasp/.relay-owner.nsec or a systemd credential over an
|
||||
# environment secret. Never put NGIT_RELAY_OWNER_NSEC in this example file.
|
||||
@@ -0,0 +1,38 @@
|
||||
[Unit]
|
||||
Description=ngit-grasp GRASP relay
|
||||
Documentation=https://gitworkshop.dev/danconwaydev.com/ngit-grasp
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=ngit-grasp
|
||||
Group=ngit-grasp
|
||||
WorkingDirectory=/var/lib/ngit-grasp
|
||||
EnvironmentFile=/etc/ngit-grasp/ngit-grasp.env
|
||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
||||
ExecStart=/usr/local/bin/ngit-grasp
|
||||
Restart=on-failure
|
||||
RestartSec=10s
|
||||
TimeoutStopSec=300s
|
||||
UMask=0077
|
||||
|
||||
NoNewPrivileges=true
|
||||
PrivateDevices=true
|
||||
PrivateTmp=true
|
||||
ProtectControlGroups=true
|
||||
ProtectHome=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/var/lib/ngit-grasp
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||
RestrictNamespaces=true
|
||||
RestrictRealtime=true
|
||||
RestrictSUIDSGID=true
|
||||
CapabilityBoundingSet=
|
||||
AmbientCapabilities=
|
||||
LockPersonality=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
+4
-3
@@ -36,7 +36,8 @@ WORKING │ How-To │ Reference │
|
||||
**For:** Users with basic knowledge solving real problems
|
||||
**Style:** Practical recipes and solutions
|
||||
|
||||
- **[Deploy ngit-grasp](how-to/deploy.md)** - Production deployment guide
|
||||
- **[Deploy ngit-grasp](how-to/deploy.md)** - Choose Docker, NixOS, Linux, Proxmox, or managed hosting
|
||||
- **[Deployment contract](reference/deployment-contract.md)** - Shared runtime and persistence requirements
|
||||
- **[Configure Nix Flakes](how-to/nix-flakes.md)** - Nix development environment
|
||||
- **[Run Compliance Tests](how-to/test-compliance.md)** - GRASP compliance testing
|
||||
- **[Upgrade nostr-sdk](how-to/upgrade-nostr-sdk.md)** - Handling SDK upgrades
|
||||
@@ -74,8 +75,8 @@ WORKING │ How-To │ Reference │
|
||||
|
||||
### I want to deploy ngit-grasp
|
||||
1. Review [Configuration Reference](reference/configuration.md)
|
||||
2. Follow [Deployment How-To](how-to/deploy.md)
|
||||
3. Set up monitoring and backups
|
||||
2. Choose an environment in [Deploy ngit-grasp](how-to/deploy.md)
|
||||
3. Verify the deployment and test its backup
|
||||
|
||||
### I want to develop on ngit-grasp
|
||||
1. Follow [Getting Started Tutorial](tutorials/getting-started.md)
|
||||
|
||||
@@ -868,47 +868,20 @@ Relax the write policy to accept all repository announcements regardless of clon
|
||||
|
||||
## Deployment
|
||||
|
||||
### Single Binary
|
||||
The runtime remains one binary plus Git, but persistence, identity, proxying,
|
||||
and single-writer behavior are part of the production boundary. The normative
|
||||
[deployment contract](../reference/deployment-contract.md) owns those shared
|
||||
requirements.
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
NGIT_RELAY_OWNER_NSEC=nsec1... \
|
||||
./target/release/ngit-grasp --domain example.com
|
||||
```
|
||||
Supported artifacts are maintained alongside their operating guides:
|
||||
|
||||
### Docker
|
||||
- the root `Dockerfile` and Compose configurations;
|
||||
- `deploy/systemd/ngit-grasp.service` for conventional Linux;
|
||||
- `nix/module.nix` for declarative NixOS instances; and
|
||||
- managed-host templates for single-instance deployments.
|
||||
|
||||
```dockerfile
|
||||
FROM rust:1.75 as builder
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN cargo build --release
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=builder /app/target/release/ngit-grasp /usr/local/bin/
|
||||
EXPOSE 7334
|
||||
CMD ["ngit-grasp"]
|
||||
```
|
||||
|
||||
### Systemd
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=ngit-grasp GRASP server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=git
|
||||
WorkingDirectory=/opt/ngit-grasp
|
||||
EnvironmentFile=/opt/ngit-grasp/.env
|
||||
ExecStart=/usr/local/bin/ngit-grasp
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
See the [deployment chooser](../how-to/deploy.md) rather than copying an
|
||||
illustrative container or service definition from this architecture document.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
|
||||
+12
-12
@@ -24,6 +24,18 @@ How-to guides are **recipes** that show you how to solve specific problems or ac
|
||||
|
||||
## Available How-To Guides
|
||||
|
||||
### [Deploy ngit-grasp](deploy.md)
|
||||
|
||||
**Problem:** Run a durable production relay in a supported hosting environment
|
||||
|
||||
**You'll learn:**
|
||||
|
||||
- Choose Docker, NixOS, systemd Linux, Proxmox, or managed hosting
|
||||
- Preserve the relay identity, Git repositories, and LMDB state
|
||||
- Configure TLS, verify protocols, back up, and upgrade safely
|
||||
|
||||
---
|
||||
|
||||
### [Upgrade from v2 to v3 Git family storage](upgrade-git-family-storage.md)
|
||||
|
||||
**Problem:** Perform the one-way identifier-family storage migration safely
|
||||
@@ -64,18 +76,6 @@ How-to guides are **recipes** that show you how to solve specific problems or ac
|
||||
|
||||
## Planned How-To Guides
|
||||
|
||||
### Deploy ngit-grasp
|
||||
**Status:** 🔜 Planned (waiting for main server)
|
||||
|
||||
**Problem:** Deploy to production
|
||||
**You'll learn:**
|
||||
- Server requirements
|
||||
- Reverse proxy setup (nginx/Caddy)
|
||||
- SSL/TLS configuration
|
||||
- Monitoring and logging
|
||||
|
||||
---
|
||||
|
||||
### Run Compliance Tests
|
||||
**Status:** 🔜 Planned
|
||||
|
||||
|
||||
@@ -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,102 @@
|
||||
# Deploy a static binary with systemd
|
||||
|
||||
Use this path for a conventional Debian, Ubuntu, Fedora, or other systemd Linux
|
||||
server when containers are unnecessary. Build the portable binary on a Nix
|
||||
build machine, then copy only the binary and service files to the server.
|
||||
|
||||
## Build a pinned binary
|
||||
|
||||
From a clean checkout of the reviewed tag or revision:
|
||||
|
||||
```bash
|
||||
nix build .#static
|
||||
file result/bin/ngit-grasp
|
||||
```
|
||||
|
||||
Copy `result/bin/ngit-grasp`, `deploy/systemd/ngit-grasp.service`, and
|
||||
`deploy/systemd/ngit-grasp.env.example` to the server through your normal
|
||||
authenticated deployment channel.
|
||||
|
||||
The deployment host does not need Nix or Rust. It does need Git and trusted CA
|
||||
certificates:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ca-certificates git
|
||||
```
|
||||
|
||||
Use the equivalent packages on non-Debian distributions.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
getent group ngit-grasp >/dev/null || sudo groupadd --system ngit-grasp
|
||||
id -u ngit-grasp >/dev/null 2>&1 || \
|
||||
sudo useradd --system --gid ngit-grasp --home-dir /var/lib/ngit-grasp \
|
||||
--create-home --shell /usr/sbin/nologin ngit-grasp
|
||||
sudo install -Dm755 ngit-grasp /usr/local/bin/ngit-grasp
|
||||
sudo install -Dm644 ngit-grasp.service \
|
||||
/etc/systemd/system/ngit-grasp.service
|
||||
sudo install -Dm640 -o root -g ngit-grasp ngit-grasp.env.example \
|
||||
/etc/ngit-grasp/ngit-grasp.env
|
||||
sudo install -d -m 0750 -o ngit-grasp -g ngit-grasp \
|
||||
/var/lib/ngit-grasp \
|
||||
/var/lib/ngit-grasp/git \
|
||||
/var/lib/ngit-grasp/relay
|
||||
```
|
||||
|
||||
Edit `/etc/ngit-grasp/ngit-grasp.env` and set `NGIT_DOMAIN`.
|
||||
|
||||
If restoring an existing identity, install `.relay-owner.nsec` as mode `0600`
|
||||
owned by `ngit-grasp` under `/var/lib/ngit-grasp`. Otherwise the first start
|
||||
generates it there.
|
||||
|
||||
## Reverse proxy and TLS
|
||||
|
||||
Keep the service bound to `127.0.0.1:7334`. For Caddy, a domain-root virtual
|
||||
host is:
|
||||
|
||||
```caddyfile
|
||||
ngit.example.com {
|
||||
reverse_proxy 127.0.0.1:7334
|
||||
}
|
||||
```
|
||||
|
||||
Point DNS at the server and replace `ngit.example.com` in both Caddy and the
|
||||
ngit-grasp environment file. Caddy preserves WebSocket upgrades automatically.
|
||||
|
||||
## Start and verify
|
||||
|
||||
On the server:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now ngit-grasp
|
||||
sudo systemctl status ngit-grasp --no-pager
|
||||
sudo journalctl -u ngit-grasp -n 50 --no-pager
|
||||
```
|
||||
|
||||
From the repository checkout on the operator workstation:
|
||||
|
||||
```bash
|
||||
scripts/verify-deployment.sh https://ngit.example.com
|
||||
```
|
||||
|
||||
The service unit applies a restrictive umask, filesystem protection, private
|
||||
temporary directory, empty capability set, and bounded five-minute shutdown.
|
||||
|
||||
## Upgrade and rollback
|
||||
|
||||
Build the new pinned binary before touching the server. Read `CHANGELOG.md`,
|
||||
stop the service, snapshot `/var/lib/ngit-grasp`, install the new binary, and
|
||||
start the service:
|
||||
|
||||
```bash
|
||||
sudo systemctl stop ngit-grasp
|
||||
sudo install -Dm755 ngit-grasp /usr/local/bin/ngit-grasp
|
||||
sudo systemctl start ngit-grasp
|
||||
```
|
||||
|
||||
Run the verifier and inspect startup integrity summaries. If the release
|
||||
changed storage, restoring the old binary also requires restoring its matching
|
||||
state snapshot.
|
||||
@@ -0,0 +1,570 @@
|
||||
# Deploy ngit-grasp on NixOS
|
||||
|
||||
**Purpose:** Deploy ngit-grasp to a production NixOS server
|
||||
**Difficulty:** Intermediate
|
||||
**Time:** 30-60 minutes
|
||||
|
||||
This guide implements the shared
|
||||
[deployment contract](../reference/deployment-contract.md) with the repository's
|
||||
NixOS module. For another environment, return to the
|
||||
[deployment chooser](deploy.md).
|
||||
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
You want to:
|
||||
- Deploy ngit-grasp to a NixOS server
|
||||
- Configure it as a systemd service
|
||||
- Set up reverse proxy (Caddy)
|
||||
- Ensure proper security and monitoring
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- NixOS server with SSH access
|
||||
- Flakes enabled on server and local machine
|
||||
- Domain name configured (DNS pointing to server)
|
||||
- Basic knowledge of NixOS configuration
|
||||
|
||||
---
|
||||
|
||||
## Solution
|
||||
|
||||
### Step 1: Add ngit-grasp to Your Server's Flake
|
||||
|
||||
In your server's `flake.nix`, add ngit-grasp as an input:
|
||||
|
||||
```nix
|
||||
{
|
||||
inputs = {
|
||||
# Keep the nixpkgs input already used by this server configuration.
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
ngit-grasp.url =
|
||||
"git+https://gitnostr.com/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, ngit-grasp, ... }@inputs: {
|
||||
nixosConfigurations.your-hostname = nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs = { inherit inputs; };
|
||||
modules = [
|
||||
./configuration.nix
|
||||
# ... other modules
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Create Service Configuration
|
||||
|
||||
Create a new file for your ngit-grasp service (e.g., `services/ngit-grasp.nix`):
|
||||
|
||||
```nix
|
||||
{ inputs, ... }:
|
||||
|
||||
{
|
||||
imports = [ inputs.ngit-grasp.nixosModules.default ];
|
||||
|
||||
services.ngit-grasp.production = {
|
||||
enable = true;
|
||||
domain = "ngit.example.com";
|
||||
|
||||
# Network
|
||||
bindAddress = "127.0.0.1";
|
||||
port = 8082;
|
||||
# Only Caddy can reach the loopback backend, so its forwarded client IP is trusted.
|
||||
trustedProxyCidrs = [ "127.0.0.1/32" ];
|
||||
|
||||
# Storage
|
||||
dataDir = "/persistent/ngit-grasp";
|
||||
|
||||
# Identity
|
||||
relayName = "My GRASP Relay";
|
||||
relayDescription = "A Rust GRASP implementation with proactive sync";
|
||||
relayOwnerNsecFile = "/run/agenix/ngit-grasp-relay-owner-nsec";
|
||||
|
||||
# Sync - bootstrap from relay.ngit.dev
|
||||
syncBootstrapRelayUrl = "wss://relay.ngit.dev";
|
||||
|
||||
# Metrics
|
||||
metricsEnabled = true;
|
||||
|
||||
# Logging
|
||||
logLevel = "info";
|
||||
};
|
||||
|
||||
# Caddy reverse proxy
|
||||
services.caddy.virtualHosts."ngit.example.com" = {
|
||||
extraConfig = ''
|
||||
reverse_proxy 127.0.0.1:8082 {
|
||||
# Caddy manages X-Forwarded-For automatically.
|
||||
header_up X-Real-IP {remote_host}
|
||||
}
|
||||
'';
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Key configuration options:**
|
||||
|
||||
- **Instance name** (`production`): Can be any name. Used for systemd service (`ngit-grasp-production`)
|
||||
- **domain**: Your relay's domain (used in GRASP validation)
|
||||
- **port**: Local port (use reverse proxy for HTTPS)
|
||||
- **trustedProxyCidrs**: Proxy source ranges allowed to supply the client IP
|
||||
- Keep empty for a directly exposed listener
|
||||
- Keep the backend private; trusting a public-facing source range permits spoofed headers
|
||||
- Caddy automatically maintains `X-Forwarded-For`; `header_up`, not `header_down`,
|
||||
changes headers sent to the backend
|
||||
- **dataDir**: Where git repos and database are stored
|
||||
- **relayOwnerNsecFile**: Path to file containing relay owner's nsec
|
||||
- Passed to ngit-grasp as a protected systemd credential, not a process argument
|
||||
- The runtime secret file must already exist (for example through agenix or sops-nix)
|
||||
- Permissions on that external source file remain the operator or secret manager's responsibility
|
||||
- Alternative: `relayOwnerNsec = "nsec1..."` (less secure, in nix store)
|
||||
- If neither option is set, ngit-grasp loads or creates `.relay-owner.nsec` in `dataDir`
|
||||
- **syncBootstrapRelayUrl**: Bootstrap relay to sync from on startup
|
||||
|
||||
See [nix/example-configuration.nix](../../nix/example-configuration.nix) for more examples.
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Import the Service
|
||||
|
||||
Import your service configuration in your main configuration file:
|
||||
|
||||
```nix
|
||||
# In configuration.nix or services/default.nix
|
||||
{
|
||||
imports = [
|
||||
./services/ngit-grasp.nix
|
||||
# ... other services
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Update Flake Lock
|
||||
|
||||
```bash
|
||||
cd /path/to/server/config
|
||||
nix flake update ngit-grasp
|
||||
git add flake.lock
|
||||
git commit -m "Add ngit-grasp and update flake.lock"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 5: Validate Configuration
|
||||
|
||||
Before deploying, validate that the flake evaluates without starting its builds:
|
||||
|
||||
```bash
|
||||
nix flake check --no-build
|
||||
```
|
||||
|
||||
#### Resource-safe module validation
|
||||
|
||||
Nix copies path-valued build inputs into the store when they are forced. A Git
|
||||
flake is materialized from its tracked files first, but a standalone path into a
|
||||
working tree does not inherit that Git filtering.
|
||||
|
||||
This matters when testing ngit-grasp's NixOS module locally. Importing
|
||||
`nix/module.nix` is lazy by itself, but rendering an enabled service forces the
|
||||
module-built package through `ExecStart`. The package's `src = ../.` then
|
||||
resolves relative to that module. If the module was imported directly from a
|
||||
working tree, Nix may recursively hash or copy ignored `target/`, `.git`, and
|
||||
linked-worktree data while it appears to be evaluating the configuration.
|
||||
|
||||
Use `inputs.ngit-grasp.nixosModules.default` from a Git-backed flake input, as
|
||||
shown above. For local module changes, commit them to a temporary Git branch and
|
||||
use that Git source, or replace `buildRustPackage` with a test stub that ignores
|
||||
all build attributes so `src` remains unforced. Do not use a direct
|
||||
working-tree module import for a test that enables an instance.
|
||||
|
||||
Inspect the derivation plan before starting a build:
|
||||
|
||||
```bash
|
||||
nixos-rebuild dry-build --flake .#your-hostname
|
||||
```
|
||||
|
||||
Multiple ngit-grasp instances should normally share one ngit-grasp package
|
||||
derivation. Avoid service-level `ExecStart` overrides that force another flake
|
||||
package or Rust toolchain. If distinct versions are intentional, build them
|
||||
sequentially or on appropriately sized remote builders. For an initial local
|
||||
build, constrain Nix while confirming the plan behaves as expected:
|
||||
|
||||
```bash
|
||||
nixos-rebuild build --flake .#your-hostname --max-jobs 1 --cores 2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 6: Deploy to Server
|
||||
|
||||
Deploy the new configuration to your server:
|
||||
|
||||
```bash
|
||||
# Build and switch in one command (builds on server)
|
||||
nixos-rebuild switch --flake .#your-hostname \
|
||||
--target-host user@server.example.com \
|
||||
--use-remote-sudo \
|
||||
--build-host user@server.example.com
|
||||
```
|
||||
|
||||
**Alternative:** Build locally, then deploy:
|
||||
|
||||
```bash
|
||||
# Build locally
|
||||
nixos-rebuild build --flake .#your-hostname
|
||||
|
||||
# Deploy to server
|
||||
nixos-rebuild switch --flake .#your-hostname \
|
||||
--target-host user@server.example.com \
|
||||
--use-remote-sudo
|
||||
```
|
||||
|
||||
**Note:** Building locally requires your machine to trust the server's nix signing key.
|
||||
|
||||
---
|
||||
|
||||
### Step 7: Verify Deployment
|
||||
|
||||
SSH to the server and check the service:
|
||||
|
||||
```bash
|
||||
ssh user@server.example.com
|
||||
|
||||
# Check service status
|
||||
systemctl status ngit-grasp-production
|
||||
|
||||
# View recent logs
|
||||
journalctl -u ngit-grasp-production -n 50 --no-pager
|
||||
|
||||
# Check if listening on port
|
||||
ss -tlnp | grep 8082
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 8: Test Functionality
|
||||
|
||||
From your local machine, test the relay:
|
||||
|
||||
```bash
|
||||
# Test NIP-11 relay info
|
||||
curl https://ngit.example.com -H "Accept: application/nostr+json" | jq
|
||||
|
||||
# Test WebSocket connection
|
||||
websocat wss://ngit.example.com
|
||||
# Then type: ["REQ","test",{}]
|
||||
# Should receive events
|
||||
|
||||
# Test git clone (if you have repos)
|
||||
git ls-remote https://ngit.example.com/<npub>/<repo>.git
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Required
|
||||
- `enable` - Enable this instance
|
||||
- `domain` - Domain where relay is hosted
|
||||
|
||||
### Network
|
||||
- `basePath` - Public URL mount path (default: `/`)
|
||||
- `bindAddress` - IP to bind to (default: "127.0.0.1")
|
||||
- `port` - Port to listen on (default: 7334)
|
||||
- `trustedProxyCidrs` - Proxy networks allowed to provide the WebSocket client
|
||||
IP (default: empty; forwarded headers ignored)
|
||||
|
||||
### Storage
|
||||
- `dataDir` - Base directory for data (default: /var/lib/ngit-grasp-{name})
|
||||
- `databaseBackend` - "lmdb" | "memory" (default: "lmdb")
|
||||
|
||||
See [Upgrade Git family storage](upgrade-git-family-storage.md) before updating
|
||||
an existing instance to a release that enables identifier-family storage.
|
||||
|
||||
### Identity
|
||||
- `relayName` - Relay name for NIP-11 (default: "{domain} grasp relay")
|
||||
- `relayDescription` - Relay description
|
||||
- `relayOwnerNsecFile` - Runtime secret file loaded as a systemd credential (recommended)
|
||||
- `relayOwnerNsec` - Inline nsec (less secure)
|
||||
|
||||
### Sync
|
||||
- `syncBootstrapRelayUrl` - Bootstrap relay URL (optional)
|
||||
- `syncDisableNegentropy` - Disable NIP-77 negentropy (default: false)
|
||||
- `syncMaxBackoffSecs` - Max backoff for reconnection (default: 3600)
|
||||
- `syncDisconnectCheckIntervalSecs` - Check interval (default: 60)
|
||||
- `syncBaseBackoffSecs` - Base backoff time (default: 5)
|
||||
|
||||
### Metrics
|
||||
- `metricsEnabled` - Enable `/metrics` below the configured base path (default: true)
|
||||
- `metricsConnectionPerIpAbuseThreshold` - Abuse threshold (default: 10)
|
||||
- `metricsTopNRepos` - Number of top repos to track (default: 10)
|
||||
|
||||
### Logging
|
||||
- `logLevel` - "trace" | "debug" | "info" | "warn" | "error" (default: "info")
|
||||
|
||||
### Security
|
||||
- `user` - User to run as (default: "ngit-grasp-{name}")
|
||||
- `group` - Group to run as (default: "ngit-grasp")
|
||||
|
||||
See [nix/module.nix](../../nix/module.nix) for complete option definitions.
|
||||
|
||||
---
|
||||
|
||||
## Systemd Service
|
||||
|
||||
The NixOS module creates a systemd service: `ngit-grasp-{instance-name}`
|
||||
|
||||
```bash
|
||||
# Start/stop/restart
|
||||
systemctl start ngit-grasp-production
|
||||
systemctl stop ngit-grasp-production
|
||||
systemctl restart ngit-grasp-production
|
||||
|
||||
# Enable/disable autostart
|
||||
systemctl enable ngit-grasp-production
|
||||
systemctl disable ngit-grasp-production
|
||||
|
||||
# View logs
|
||||
journalctl -u ngit-grasp-production -f
|
||||
journalctl -u ngit-grasp-production --since "1 hour ago"
|
||||
|
||||
# Check status
|
||||
systemctl status ngit-grasp-production
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Multiple Instances
|
||||
|
||||
You can run multiple instances on the same server:
|
||||
|
||||
```nix
|
||||
services.ngit-grasp = {
|
||||
production = {
|
||||
enable = true;
|
||||
domain = "ngit.example.com";
|
||||
port = 8082;
|
||||
dataDir = "/persistent/ngit-production";
|
||||
};
|
||||
|
||||
staging = {
|
||||
enable = true;
|
||||
domain = "ngit-staging.example.com";
|
||||
port = 8083;
|
||||
dataDir = "/persistent/ngit-staging";
|
||||
logLevel = "debug";
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Each instance:
|
||||
- Runs as separate systemd service: `ngit-grasp-production`, `ngit-grasp-staging`
|
||||
- Has its own user: `ngit-grasp-production`, `ngit-grasp-staging`
|
||||
- Stores data in separate directory
|
||||
- Can have different configuration
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Service won't start
|
||||
|
||||
**Check logs:**
|
||||
```bash
|
||||
journalctl -u ngit-grasp-production -n 50
|
||||
```
|
||||
|
||||
**Common issues:**
|
||||
- Port already in use: Check with `ss -tlnp | grep 8082`
|
||||
- Data directory permissions: Should be owned by service user
|
||||
- Invalid nsec file: Check file exists and contains valid nsec
|
||||
|
||||
### Can't connect via WebSocket
|
||||
|
||||
**Check:**
|
||||
- Service is running: `systemctl status ngit-grasp-production`
|
||||
- Firewall allows connections: `nix run nixpkgs#nmap -- -p 443 ngit.example.com`
|
||||
- Caddy is configured correctly: `systemctl status caddy`
|
||||
- DNS resolves: `dig ngit.example.com`
|
||||
|
||||
### Sync not working
|
||||
|
||||
**Check logs for sync errors:**
|
||||
```bash
|
||||
journalctl -u ngit-grasp-production | grep -i sync
|
||||
```
|
||||
|
||||
**Common issues:**
|
||||
- Bootstrap relay URL incorrect or unreachable
|
||||
- Network connectivity issues
|
||||
- Bootstrap relay doesn't support negentropy (disable with `syncDisableNegentropy = true`)
|
||||
|
||||
### High memory/CPU usage
|
||||
|
||||
**Monitor metrics:**
|
||||
```bash
|
||||
curl http://localhost:8082/metrics
|
||||
```
|
||||
|
||||
**Tune configuration:**
|
||||
- Reduce `metricsTopNRepos`
|
||||
- Increase `syncMaxBackoffSecs`
|
||||
- Tune `syncMaxBackoffSecs` for your network conditions
|
||||
|
||||
---
|
||||
|
||||
## Rollback
|
||||
|
||||
If deployment fails, rollback to previous configuration:
|
||||
|
||||
```bash
|
||||
# On the server
|
||||
nixos-rebuild switch --rollback
|
||||
|
||||
# Or remotely
|
||||
nixos-rebuild switch --rollback \
|
||||
--target-host user@server.example.com \
|
||||
--use-remote-sudo
|
||||
```
|
||||
|
||||
If the release changed on-disk storage, a NixOS generation rollback is not
|
||||
enough. Restore the matching pre-upgrade snapshot of the complete `dataDir`
|
||||
before starting the older service. See the
|
||||
[deployment contract](../reference/deployment-contract.md#upgrade-and-rollback).
|
||||
|
||||
---
|
||||
|
||||
## Upgrading
|
||||
|
||||
To upgrade ngit-grasp:
|
||||
|
||||
```bash
|
||||
# Update flake input
|
||||
nix flake update ngit-grasp
|
||||
|
||||
# Review changes
|
||||
git diff flake.lock
|
||||
|
||||
# Commit
|
||||
git add flake.lock
|
||||
git commit -m "Update ngit-grasp"
|
||||
|
||||
# Deploy
|
||||
nixos-rebuild switch --flake .#your-hostname \
|
||||
--target-host user@server.example.com \
|
||||
--use-remote-sudo \
|
||||
--build-host user@server.example.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Hardening
|
||||
|
||||
The NixOS module includes systemd hardening:
|
||||
|
||||
- `NoNewPrivileges = true` - Prevents privilege escalation
|
||||
- `ProtectSystem = "strict"` - Read-only filesystem except dataDir
|
||||
- `ProtectHome = true` - No access to home directories
|
||||
- `PrivateTmp = true` - Private /tmp
|
||||
- `RestrictAddressFamilies` - Only allow needed network families
|
||||
|
||||
Additional recommendations:
|
||||
|
||||
1. **Use a runtime secret file instead of an inline key:**
|
||||
```nix
|
||||
relayOwnerNsecFile = "/run/agenix/ngit-grasp-relay-owner-nsec";
|
||||
# NOT: relayOwnerNsec = "nsec1..."; # Ends up in nix store!
|
||||
```
|
||||
|
||||
The module exposes the file to ngit-grasp as the `relay_owner_nsec`
|
||||
systemd credential. The key does not appear in `ExecStart` or the process
|
||||
command line. ngit-grasp does not modify the external source file; keep its
|
||||
ownership and permissions restricted through your secret manager.
|
||||
|
||||
2. **Restrict data directory permissions:**
|
||||
```bash
|
||||
chmod 750 /persistent/ngit-grasp
|
||||
chown ngit-grasp-production:ngit-grasp /persistent/ngit-grasp
|
||||
```
|
||||
|
||||
3. **Use HTTPS (reverse proxy required):**
|
||||
- ngit-grasp binds to localhost by default
|
||||
- Use Caddy/nginx for TLS termination
|
||||
- Caddy handles certificates automatically
|
||||
|
||||
4. **Monitor logs regularly:**
|
||||
```bash
|
||||
journalctl -u ngit-grasp-production --since today | grep -i error
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Prometheus Metrics
|
||||
|
||||
ngit-grasp exposes Prometheus metrics at `/metrics`:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8082/metrics
|
||||
```
|
||||
|
||||
See [Prometheus Setup](./prometheus-setup.md) for complete monitoring guide.
|
||||
|
||||
### Basic Health Checks
|
||||
|
||||
```bash
|
||||
# Check if service is running
|
||||
systemctl is-active ngit-grasp-production
|
||||
|
||||
# Check if port is listening
|
||||
nc -zv localhost 8082
|
||||
|
||||
# Check relay info
|
||||
curl https://ngit.example.com -H "Accept: application/nostr+json"
|
||||
|
||||
# Check disk usage
|
||||
du -sh /persistent/ngit-grasp/*
|
||||
```
|
||||
|
||||
## Backup
|
||||
|
||||
Back up the complete `dataDir`, including `.relay-owner.nsec`, `git/`, and
|
||||
`relay/`, from one point in time. For a portable consistent backup, stop the
|
||||
instance before taking the snapshot:
|
||||
|
||||
```bash
|
||||
systemctl stop ngit-grasp-production
|
||||
# Snapshot or back up /persistent/ngit-grasp with the host's storage tooling.
|
||||
systemctl start ngit-grasp-production
|
||||
```
|
||||
|
||||
Keep an off-host copy and test restoration into an isolated, non-public
|
||||
instance. Never start the restored copy alongside production with the same
|
||||
domain and relay identity.
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Configuration Reference](../reference/configuration.md) - All configuration options
|
||||
- [NixOS Module](../../nix/module.nix) - Module source code
|
||||
- [Example Configuration](../../nix/example-configuration.nix) - More examples
|
||||
- [Prometheus Setup](./prometheus-setup.md) - Monitoring guide
|
||||
- [Nix Flakes How-To](./nix-flakes.md) - Nix development environment
|
||||
- [Deployment Contract](../reference/deployment-contract.md) - Shared runtime and persistence rules
|
||||
- [Deployment Chooser](deploy.md) - Other supported environments
|
||||
|
||||
---
|
||||
|
||||
*Part of the [ngit-grasp how-to guides](./)*
|
||||
@@ -0,0 +1,162 @@
|
||||
# Deploy on managed hosting
|
||||
|
||||
The supplied container can run on Railway, Render, and Fly.io when each service
|
||||
has a custom domain, one persistent `/data` volume, and exactly one running
|
||||
instance. These platforms terminate TLS and proxy WebSockets to ngit-grasp.
|
||||
|
||||
Managed hosting is best for a small or moderate relay whose operator accepts
|
||||
brief upgrade downtime. For a large existing relay, use NixOS, systemd, or
|
||||
Compose so startup migrations and storage snapshots remain under direct
|
||||
operator control.
|
||||
|
||||
Read the [deployment contract](../reference/deployment-contract.md) first. In
|
||||
particular, provider replicas do not make ngit-grasp highly available: their
|
||||
local volumes do not replicate application state.
|
||||
|
||||
## Common requirements
|
||||
|
||||
For every provider:
|
||||
|
||||
- set `NGIT_DOMAIN` to the final custom hostname, not the provider hostname;
|
||||
- mount durable storage at `/data` before the first successful start;
|
||||
- keep the instance count at one and disable scale-to-zero;
|
||||
- keep `NGIT_BASE_PATH=/` unless path routing has been designed explicitly;
|
||||
- allow at least five minutes between `SIGTERM` and forced termination; and
|
||||
- take an external backup of `/data`, including `.relay-owner.nsec`.
|
||||
|
||||
The image maps a platform-provided `PORT` to `0.0.0.0:${PORT}`. Do not set
|
||||
`NGIT_BIND_ADDRESS` unless the provider template below does so explicitly.
|
||||
|
||||
## Railway
|
||||
|
||||
`railway.toml` selects the Dockerfile, uses `/` as the health check, disables
|
||||
old/new deployment overlap, and gives shutdown five minutes. Railway storage is
|
||||
configured separately from config-as-code. See Railway's
|
||||
[config-as-code reference](https://docs.railway.com/config-as-code/reference)
|
||||
and [volume guide](https://docs.railway.com/volumes) for the provider-side
|
||||
details.
|
||||
|
||||
From the repository root with the Railway CLI installed:
|
||||
|
||||
```bash
|
||||
railway login
|
||||
railway init
|
||||
railway add --service ngit-grasp
|
||||
railway service ngit-grasp
|
||||
railway variable set NGIT_DOMAIN=ngit.example.com NGIT_BASE_PATH=/
|
||||
railway volume add --mount-path /data
|
||||
railway up
|
||||
railway domain ngit.example.com
|
||||
```
|
||||
|
||||
Add the DNS records returned by `railway domain`, then inspect status and verify
|
||||
the public relay:
|
||||
|
||||
```bash
|
||||
railway domain status ngit.example.com
|
||||
railway deployment list --json
|
||||
scripts/verify-deployment.sh https://ngit.example.com
|
||||
```
|
||||
|
||||
Do not add replicas or multi-region configuration. Railway mounts a new volume
|
||||
as root; the image entrypoint prepares it and drops privileges before starting
|
||||
ngit-grasp.
|
||||
|
||||
Railway volume backups are useful recovery points, but also export or snapshot
|
||||
state outside the platform. During a large storage migration, increase the
|
||||
health-check timeout or move the upgrade to a directly operated host.
|
||||
|
||||
## Render
|
||||
|
||||
`render.yaml` describes a paid `starter` web service with a 10 GB disk mounted
|
||||
at `/data`, a root health check, a five-minute shutdown delay, and automatic
|
||||
deploys disabled. Render prompts for `NGIT_DOMAIN` when the Blueprint is
|
||||
created. The fields follow Render's
|
||||
[Blueprint specification](https://render.com/docs/blueprint-spec); its
|
||||
[persistent disk guide](https://render.com/docs/disks) describes the storage
|
||||
and scaling constraints.
|
||||
|
||||
Render Blueprints require a repository connected through one of Render's
|
||||
supported Git providers. Until ngit-grasp has an authorized mirror or published
|
||||
OCI image, use Render's **Public Git Repository** flow with the canonical URL:
|
||||
|
||||
```text
|
||||
https://gitnostr.com/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
|
||||
```
|
||||
|
||||
Choose Docker and apply the values from `render.yaml` in the service form:
|
||||
|
||||
- environment variable `NGIT_DOMAIN=ngit.example.com`;
|
||||
- persistent disk mounted at `/data`;
|
||||
- health check path `/`;
|
||||
- maximum shutdown delay 300 seconds; and
|
||||
- one instance with automatic deploys disabled.
|
||||
|
||||
Add and verify the custom domain before publishing it in repository
|
||||
announcements. Render services backed by a persistent disk cannot run multiple
|
||||
instances and have brief downtime during deploys, which matches the relay's
|
||||
single-writer requirement.
|
||||
|
||||
After a manual deploy, run:
|
||||
|
||||
```bash
|
||||
scripts/verify-deployment.sh https://ngit.example.com
|
||||
```
|
||||
|
||||
Use Render disk snapshots plus an independent backup. Only `/data` persists;
|
||||
all other container filesystem changes are ephemeral.
|
||||
|
||||
## Fly.io
|
||||
|
||||
Fly volumes are local to one Machine and are not automatically replicated. The
|
||||
template therefore disables autostop, uses an immediate replacement strategy,
|
||||
and must be deployed with high-availability seeding disabled. See Fly's
|
||||
[configuration reference](https://fly.io/docs/reference/configuration/) and
|
||||
[volume overview](https://fly.io/docs/volumes/overview/) for the underlying
|
||||
platform behavior.
|
||||
|
||||
Copy and edit the template:
|
||||
|
||||
```bash
|
||||
cp fly.toml.example fly.toml
|
||||
# Set a unique app name, primary region, and NGIT_DOMAIN in fly.toml.
|
||||
fly config validate --strict --config fly.toml
|
||||
```
|
||||
|
||||
Create the app and one volume in the same region, then deploy one Machine:
|
||||
|
||||
```bash
|
||||
fly apps create replace-with-a-unique-ngit-grasp-name
|
||||
fly volumes create ngit_grasp_data --region ord --size 10
|
||||
fly deploy --ha=false
|
||||
fly scale count 1
|
||||
fly certs add ngit.example.com
|
||||
```
|
||||
|
||||
Use the app name and region selected in `fly.toml`, add the DNS records shown by
|
||||
`fly certs`, then verify:
|
||||
|
||||
```bash
|
||||
fly checks list
|
||||
fly scale show
|
||||
scripts/verify-deployment.sh https://ngit.example.com
|
||||
```
|
||||
|
||||
Never scale above one: Fly creates a separate empty volume for each additional
|
||||
Machine and does not copy ngit-grasp state. A single Machine and volume can be
|
||||
unavailable after host failure, so maintain an external backup rather than
|
||||
relying only on Fly snapshots.
|
||||
|
||||
## Upgrades
|
||||
|
||||
Managed volume deployments cannot provide a safe zero-downtime writer handoff.
|
||||
Expect brief downtime:
|
||||
|
||||
1. disable automatic deploys;
|
||||
2. take a complete provider snapshot and an external backup;
|
||||
3. deploy the pinned revision with one instance;
|
||||
4. inspect startup and integrity logs; and
|
||||
5. run the public verifier.
|
||||
|
||||
For a storage-changing release, restore both the prior executable and its
|
||||
matching pre-upgrade `/data` snapshot when rolling back.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Deploy in a Proxmox LXC container or VM
|
||||
|
||||
ngit-grasp has no kernel-virtualization or nested-container requirement. The
|
||||
preferred Proxmox layout is an unprivileged Debian or Ubuntu LXC running the
|
||||
static binary as a systemd service.
|
||||
|
||||
## Container requirements
|
||||
|
||||
- an unprivileged LXC or ordinary VM with systemd
|
||||
- network access to public HTTPS and WebSocket relays
|
||||
- inbound HTTP/HTTPS through the Proxmox network or an external proxy
|
||||
- durable storage sized for Git repositories, LMDB, holding data, and backups
|
||||
|
||||
Docker nesting is not required for the direct binary path. Leave it disabled
|
||||
unless using the Compose alternative below.
|
||||
|
||||
## Direct systemd path
|
||||
|
||||
Follow [Deploy a static binary with systemd](deploy-linux.md) inside the guest.
|
||||
Keep `/var/lib/ngit-grasp` on storage included in the guest's snapshot and
|
||||
backup policy.
|
||||
|
||||
When the Proxmox host bind-mounts a dataset into an unprivileged LXC, map its
|
||||
ownership to the container's `ngit-grasp` UID/GID before starting the service.
|
||||
Verify this from inside the container:
|
||||
|
||||
```bash
|
||||
sudo -u ngit-grasp test -w /var/lib/ngit-grasp
|
||||
sudo -u ngit-grasp git --version
|
||||
```
|
||||
|
||||
Terminate TLS either inside the guest with Caddy or at an upstream proxy. If
|
||||
the upstream proxy connects directly to ngit-grasp, add only that private
|
||||
source address to `NGIT_TRUSTED_PROXY_CIDRS` and prevent other clients from
|
||||
reaching port 7334.
|
||||
|
||||
## Compose alternative
|
||||
|
||||
If the guest already operates Docker or Podman, follow the
|
||||
[Docker guide](deploy-docker.md). Docker inside LXC generally requires the
|
||||
Proxmox nesting feature; the static binary path avoids that extra layer.
|
||||
|
||||
Do not mount the host Docker socket into the relay container. ngit-grasp needs
|
||||
Git, not a container daemon.
|
||||
|
||||
## Backup and upgrade
|
||||
|
||||
For a simple consistent backup:
|
||||
|
||||
1. stop `ngit-grasp` inside the guest;
|
||||
2. snapshot or back up the guest and its attached state storage;
|
||||
3. start the service; and
|
||||
4. verify the public endpoint.
|
||||
|
||||
Proxmox snapshots are not a substitute for an off-host backup. Before a
|
||||
storage-changing upgrade, confirm that the state volume participates in the
|
||||
snapshot and that the snapshot can be restored without starting a second
|
||||
writer against the production domain.
|
||||
+27
-533
@@ -1,543 +1,37 @@
|
||||
# How-To: Deploy ngit-grasp to Production
|
||||
# Deploy ngit-grasp
|
||||
|
||||
**Purpose:** Deploy ngit-grasp to a production NixOS server
|
||||
**Difficulty:** Intermediate
|
||||
**Time:** 30-60 minutes
|
||||
ngit-grasp supports several single-instance production layouts. Choose the
|
||||
guide matching the host you already operate; every guide implements the same
|
||||
[deployment contract](../reference/deployment-contract.md).
|
||||
|
||||
---
|
||||
| Environment | Start here | Supplied artifact |
|
||||
| --- | --- | --- |
|
||||
| Docker or Podman host | [Docker and Compose](deploy-docker.md) | `Dockerfile`, `compose.yaml`, optional Caddy overlay |
|
||||
| NixOS | [NixOS module](deploy-nixos.md) | `nixosModules.default` |
|
||||
| Debian, Ubuntu, or another systemd Linux | [Static binary and systemd](deploy-linux.md) | Static flake package and service unit |
|
||||
| Proxmox LXC or VM | [Proxmox](deploy-proxmox-lxc.md) | Direct systemd or Compose path |
|
||||
| Railway, Render, or Fly.io | [Managed hosting](deploy-paas.md) | Provider configuration templates |
|
||||
|
||||
## Problem
|
||||
|
||||
You want to:
|
||||
- Deploy ngit-grasp to a NixOS server
|
||||
- Configure it as a systemd service
|
||||
- Set up reverse proxy (Caddy)
|
||||
- Ensure proper security and monitoring
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- NixOS server with SSH access
|
||||
- Flakes enabled on server and local machine
|
||||
- Domain name configured (DNS pointing to server)
|
||||
- Basic knowledge of NixOS configuration
|
||||
|
||||
---
|
||||
|
||||
## Solution
|
||||
|
||||
### Step 1: Add ngit-grasp to Your Server's Flake
|
||||
|
||||
In your server's `flake.nix`, add ngit-grasp as an input:
|
||||
|
||||
```nix
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
|
||||
ngit-grasp.url = "github:DanConwayDev/ngit-grasp";
|
||||
# or use a specific git repository:
|
||||
# ngit-grasp.url = "git+https://git.shakespeare.diy/npub.../ngit-grasp.git";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, ngit-grasp, ... }@inputs: {
|
||||
nixosConfigurations.your-hostname = nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs = { inherit inputs; };
|
||||
modules = [
|
||||
./configuration.nix
|
||||
# ... other modules
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Create Service Configuration
|
||||
|
||||
Create a new file for your ngit-grasp service (e.g., `services/ngit-grasp.nix`):
|
||||
|
||||
```nix
|
||||
{ inputs, ... }:
|
||||
|
||||
{
|
||||
imports = [ inputs.ngit-grasp.nixosModules.default ];
|
||||
|
||||
services.ngit-grasp.production = {
|
||||
enable = true;
|
||||
domain = "ngit.example.com";
|
||||
|
||||
# Network
|
||||
bindAddress = "127.0.0.1";
|
||||
port = 8082;
|
||||
# Only Caddy can reach the loopback backend, so its forwarded client IP is trusted.
|
||||
trustedProxyCidrs = [ "127.0.0.1/32" ];
|
||||
|
||||
# Storage
|
||||
dataDir = "/persistent/ngit-grasp";
|
||||
|
||||
# Identity
|
||||
relayName = "My GRASP Relay";
|
||||
relayDescription = "A Rust GRASP implementation with proactive sync";
|
||||
relayOwnerNsecFile = "/run/agenix/ngit-grasp-relay-owner-nsec";
|
||||
|
||||
# Sync - bootstrap from relay.ngit.dev
|
||||
syncBootstrapRelayUrl = "wss://relay.ngit.dev";
|
||||
|
||||
# Metrics
|
||||
metricsEnabled = true;
|
||||
|
||||
# Logging
|
||||
logLevel = "info";
|
||||
};
|
||||
|
||||
# Caddy reverse proxy
|
||||
services.caddy.virtualHosts."ngit.example.com" = {
|
||||
extraConfig = ''
|
||||
reverse_proxy 127.0.0.1:8082 {
|
||||
# Caddy manages X-Forwarded-For automatically.
|
||||
header_up X-Real-IP {remote_host}
|
||||
}
|
||||
'';
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Key configuration options:**
|
||||
|
||||
- **Instance name** (`production`): Can be any name. Used for systemd service (`ngit-grasp-production`)
|
||||
- **domain**: Your relay's domain (used in GRASP validation)
|
||||
- **port**: Local port (use reverse proxy for HTTPS)
|
||||
- **trustedProxyCidrs**: Proxy source ranges allowed to supply the client IP
|
||||
- Keep empty for a directly exposed listener
|
||||
- Keep the backend private; trusting a public-facing source range permits spoofed headers
|
||||
- Caddy automatically maintains `X-Forwarded-For`; `header_up`, not `header_down`,
|
||||
changes headers sent to the backend
|
||||
- **dataDir**: Where git repos and database are stored
|
||||
- **relayOwnerNsecFile**: Path to file containing relay owner's nsec
|
||||
- Passed to ngit-grasp as a protected systemd credential, not a process argument
|
||||
- The runtime secret file must already exist (for example through agenix or sops-nix)
|
||||
- Permissions on that external source file remain the operator or secret manager's responsibility
|
||||
- Alternative: `relayOwnerNsec = "nsec1..."` (less secure, in nix store)
|
||||
- If neither option is set, ngit-grasp loads or creates `.relay-owner.nsec` in `dataDir`
|
||||
- **syncBootstrapRelayUrl**: Bootstrap relay to sync from on startup
|
||||
|
||||
See [nix/example-configuration.nix](../../nix/example-configuration.nix) for more examples.
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Import the Service
|
||||
|
||||
Import your service configuration in your main configuration file:
|
||||
|
||||
```nix
|
||||
# In configuration.nix or services/default.nix
|
||||
{
|
||||
imports = [
|
||||
./services/ngit-grasp.nix
|
||||
# ... other services
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Update Flake Lock
|
||||
For a fresh internet-facing VPS, the shortest supported path is Docker Compose
|
||||
with the Caddy overlay:
|
||||
|
||||
```bash
|
||||
cd /path/to/server/config
|
||||
nix flake update ngit-grasp
|
||||
git add flake.lock
|
||||
git commit -m "Add ngit-grasp and update flake.lock"
|
||||
cp deploy.env.example .env
|
||||
# Set NGIT_DOMAIN in .env and point its DNS records at this server.
|
||||
docker compose -f compose.yaml -f compose.caddy.yaml up --build -d
|
||||
scripts/verify-deployment.sh https://ngit.example.com
|
||||
```
|
||||
|
||||
---
|
||||
The Caddy path requires ports 80 and 443. If the host already has a reverse
|
||||
proxy, follow the loopback-only path in the Docker guide instead.
|
||||
|
||||
### Step 5: Validate Configuration
|
||||
## Unsupported layouts
|
||||
|
||||
Before deploying, validate that the flake evaluates without starting its builds:
|
||||
Do not deploy ngit-grasp to serverless functions, an ephemeral filesystem, or
|
||||
multiple replicas. It owns long-lived WebSockets, background synchronization,
|
||||
local Git repositories, LMDB state, and a durable relay identity.
|
||||
|
||||
```bash
|
||||
nix flake check --no-build
|
||||
```
|
||||
|
||||
#### Resource-safe module validation
|
||||
|
||||
Nix copies path-valued build inputs into the store when they are forced. A Git
|
||||
flake is materialized from its tracked files first, but a standalone path into a
|
||||
working tree does not inherit that Git filtering.
|
||||
|
||||
This matters when testing ngit-grasp's NixOS module locally. Importing
|
||||
`nix/module.nix` is lazy by itself, but rendering an enabled service forces the
|
||||
module-built package through `ExecStart`. The package's `src = ../.` then
|
||||
resolves relative to that module. If the module was imported directly from a
|
||||
working tree, Nix may recursively hash or copy ignored `target/`, `.git`, and
|
||||
linked-worktree data while it appears to be evaluating the configuration.
|
||||
|
||||
Use `inputs.ngit-grasp.nixosModules.default` from a Git-backed flake input, as
|
||||
shown above. For local module changes, commit them to a temporary Git branch and
|
||||
use that Git source, or replace `buildRustPackage` with a test stub that ignores
|
||||
all build attributes so `src` remains unforced. Do not use a direct
|
||||
working-tree module import for a test that enables an instance.
|
||||
|
||||
Inspect the derivation plan before starting a build:
|
||||
|
||||
```bash
|
||||
nixos-rebuild dry-build --flake .#your-hostname
|
||||
```
|
||||
|
||||
Multiple ngit-grasp instances should normally share one ngit-grasp package
|
||||
derivation. Avoid service-level `ExecStart` overrides that force another flake
|
||||
package or Rust toolchain. If distinct versions are intentional, build them
|
||||
sequentially or on appropriately sized remote builders. For an initial local
|
||||
build, constrain Nix while confirming the plan behaves as expected:
|
||||
|
||||
```bash
|
||||
nixos-rebuild build --flake .#your-hostname --max-jobs 1 --cores 2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 6: Deploy to Server
|
||||
|
||||
Deploy the new configuration to your server:
|
||||
|
||||
```bash
|
||||
# Build and switch in one command (builds on server)
|
||||
nixos-rebuild switch --flake .#your-hostname \
|
||||
--target-host user@server.example.com \
|
||||
--use-remote-sudo \
|
||||
--build-host user@server.example.com
|
||||
```
|
||||
|
||||
**Alternative:** Build locally, then deploy:
|
||||
|
||||
```bash
|
||||
# Build locally
|
||||
nixos-rebuild build --flake .#your-hostname
|
||||
|
||||
# Deploy to server
|
||||
nixos-rebuild switch --flake .#your-hostname \
|
||||
--target-host user@server.example.com \
|
||||
--use-remote-sudo
|
||||
```
|
||||
|
||||
**Note:** Building locally requires your machine to trust the server's nix signing key.
|
||||
|
||||
---
|
||||
|
||||
### Step 7: Verify Deployment
|
||||
|
||||
SSH to the server and check the service:
|
||||
|
||||
```bash
|
||||
ssh user@server.example.com
|
||||
|
||||
# Check service status
|
||||
systemctl status ngit-grasp-production
|
||||
|
||||
# View logs
|
||||
journalctl -u ngit-grasp-production -f
|
||||
|
||||
# Check if listening on port
|
||||
ss -tlnp | grep 8082
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 8: Test Functionality
|
||||
|
||||
From your local machine, test the relay:
|
||||
|
||||
```bash
|
||||
# Test NIP-11 relay info
|
||||
curl https://ngit.example.com -H "Accept: application/nostr+json" | jq
|
||||
|
||||
# Test WebSocket connection
|
||||
websocat wss://ngit.example.com
|
||||
# Then type: ["REQ","test",{}]
|
||||
# Should receive events
|
||||
|
||||
# Test git clone (if you have repos)
|
||||
git ls-remote https://ngit.example.com/<npub>/<repo>.git
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Required
|
||||
- `enable` - Enable this instance
|
||||
- `domain` - Domain where relay is hosted
|
||||
|
||||
### Network
|
||||
- `basePath` - Public URL mount path (default: `/`)
|
||||
- `bindAddress` - IP to bind to (default: "127.0.0.1")
|
||||
- `port` - Port to listen on (default: 7334)
|
||||
- `trustedProxyCidrs` - Proxy networks allowed to provide the WebSocket client
|
||||
IP (default: empty; forwarded headers ignored)
|
||||
|
||||
### Storage
|
||||
- `dataDir` - Base directory for data (default: /var/lib/ngit-grasp-{name})
|
||||
- `databaseBackend` - "lmdb" | "memory" (default: "lmdb")
|
||||
|
||||
See [Upgrade Git family storage](upgrade-git-family-storage.md) before updating
|
||||
an existing instance to a release that enables identifier-family storage.
|
||||
|
||||
### Identity
|
||||
- `relayName` - Relay name for NIP-11 (default: "{domain} grasp relay")
|
||||
- `relayDescription` - Relay description
|
||||
- `relayOwnerNsecFile` - Runtime secret file loaded as a systemd credential (recommended)
|
||||
- `relayOwnerNsec` - Inline nsec (less secure)
|
||||
|
||||
### Sync
|
||||
- `syncBootstrapRelayUrl` - Bootstrap relay URL (optional)
|
||||
- `syncDisableNegentropy` - Disable NIP-77 negentropy (default: false)
|
||||
- `syncMaxBackoffSecs` - Max backoff for reconnection (default: 3600)
|
||||
- `syncDisconnectCheckIntervalSecs` - Check interval (default: 60)
|
||||
- `syncBaseBackoffSecs` - Base backoff time (default: 5)
|
||||
|
||||
### Metrics
|
||||
- `metricsEnabled` - Enable `/metrics` below the configured base path (default: true)
|
||||
- `metricsConnectionPerIpAbuseThreshold` - Abuse threshold (default: 10)
|
||||
- `metricsTopNRepos` - Number of top repos to track (default: 10)
|
||||
|
||||
### Logging
|
||||
- `logLevel` - "trace" | "debug" | "info" | "warn" | "error" (default: "info")
|
||||
|
||||
### Security
|
||||
- `user` - User to run as (default: "ngit-grasp-{name}")
|
||||
- `group` - Group to run as (default: "ngit-grasp")
|
||||
|
||||
See [nix/module.nix](../../nix/module.nix) for complete option definitions.
|
||||
|
||||
---
|
||||
|
||||
## Systemd Service
|
||||
|
||||
The NixOS module creates a systemd service: `ngit-grasp-{instance-name}`
|
||||
|
||||
```bash
|
||||
# Start/stop/restart
|
||||
systemctl start ngit-grasp-production
|
||||
systemctl stop ngit-grasp-production
|
||||
systemctl restart ngit-grasp-production
|
||||
|
||||
# Enable/disable autostart
|
||||
systemctl enable ngit-grasp-production
|
||||
systemctl disable ngit-grasp-production
|
||||
|
||||
# View logs
|
||||
journalctl -u ngit-grasp-production -f
|
||||
journalctl -u ngit-grasp-production --since "1 hour ago"
|
||||
|
||||
# Check status
|
||||
systemctl status ngit-grasp-production
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Multiple Instances
|
||||
|
||||
You can run multiple instances on the same server:
|
||||
|
||||
```nix
|
||||
services.ngit-grasp = {
|
||||
production = {
|
||||
enable = true;
|
||||
domain = "ngit.example.com";
|
||||
port = 8082;
|
||||
dataDir = "/persistent/ngit-production";
|
||||
};
|
||||
|
||||
staging = {
|
||||
enable = true;
|
||||
domain = "ngit-staging.example.com";
|
||||
port = 8083;
|
||||
dataDir = "/persistent/ngit-staging";
|
||||
logLevel = "debug";
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Each instance:
|
||||
- Runs as separate systemd service: `ngit-grasp-production`, `ngit-grasp-staging`
|
||||
- Has its own user: `ngit-grasp-production`, `ngit-grasp-staging`
|
||||
- Stores data in separate directory
|
||||
- Can have different configuration
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Service won't start
|
||||
|
||||
**Check logs:**
|
||||
```bash
|
||||
journalctl -u ngit-grasp-production -n 50
|
||||
```
|
||||
|
||||
**Common issues:**
|
||||
- Port already in use: Check with `ss -tlnp | grep 8082`
|
||||
- Data directory permissions: Should be owned by service user
|
||||
- Invalid nsec file: Check file exists and contains valid nsec
|
||||
|
||||
### Can't connect via WebSocket
|
||||
|
||||
**Check:**
|
||||
- Service is running: `systemctl status ngit-grasp-production`
|
||||
- Firewall allows connections: `nix-shell -p nmap --run "nmap -p 443 ngit.example.com"`
|
||||
- Caddy is configured correctly: `systemctl status caddy`
|
||||
- DNS resolves: `dig ngit.example.com`
|
||||
|
||||
### Sync not working
|
||||
|
||||
**Check logs for sync errors:**
|
||||
```bash
|
||||
journalctl -u ngit-grasp-production | grep -i sync
|
||||
```
|
||||
|
||||
**Common issues:**
|
||||
- Bootstrap relay URL incorrect or unreachable
|
||||
- Network connectivity issues
|
||||
- Bootstrap relay doesn't support negentropy (disable with `syncDisableNegentropy = true`)
|
||||
|
||||
### High memory/CPU usage
|
||||
|
||||
**Monitor metrics:**
|
||||
```bash
|
||||
curl http://localhost:8082/metrics
|
||||
```
|
||||
|
||||
**Tune configuration:**
|
||||
- Reduce `metricsTopNRepos`
|
||||
- Increase `syncMaxBackoffSecs`
|
||||
- Tune `syncMaxBackoffSecs` for your network conditions
|
||||
|
||||
---
|
||||
|
||||
## Rollback
|
||||
|
||||
If deployment fails, rollback to previous configuration:
|
||||
|
||||
```bash
|
||||
# On the server
|
||||
nixos-rebuild switch --rollback
|
||||
|
||||
# Or remotely
|
||||
nixos-rebuild switch --rollback \
|
||||
--target-host user@server.example.com \
|
||||
--use-remote-sudo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Upgrading
|
||||
|
||||
To upgrade ngit-grasp:
|
||||
|
||||
```bash
|
||||
# Update flake input
|
||||
nix flake update ngit-grasp
|
||||
|
||||
# Review changes
|
||||
git diff flake.lock
|
||||
|
||||
# Commit
|
||||
git add flake.lock
|
||||
git commit -m "Update ngit-grasp"
|
||||
|
||||
# Deploy
|
||||
nixos-rebuild switch --flake .#your-hostname \
|
||||
--target-host user@server.example.com \
|
||||
--use-remote-sudo \
|
||||
--build-host user@server.example.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Hardening
|
||||
|
||||
The NixOS module includes systemd hardening:
|
||||
|
||||
- `NoNewPrivileges = true` - Prevents privilege escalation
|
||||
- `ProtectSystem = "strict"` - Read-only filesystem except dataDir
|
||||
- `ProtectHome = true` - No access to home directories
|
||||
- `PrivateTmp = true` - Private /tmp
|
||||
- `RestrictAddressFamilies` - Only allow needed network families
|
||||
- `SystemCallFilter` - Restrict system calls
|
||||
|
||||
Additional recommendations:
|
||||
|
||||
1. **Use a runtime secret file instead of an inline key:**
|
||||
```nix
|
||||
relayOwnerNsecFile = "/run/agenix/ngit-grasp-relay-owner-nsec";
|
||||
# NOT: relayOwnerNsec = "nsec1..."; # Ends up in nix store!
|
||||
```
|
||||
|
||||
The module exposes the file to ngit-grasp as the `relay_owner_nsec`
|
||||
systemd credential. The key does not appear in `ExecStart` or the process
|
||||
command line. ngit-grasp does not modify the external source file; keep its
|
||||
ownership and permissions restricted through your secret manager.
|
||||
|
||||
2. **Restrict data directory permissions:**
|
||||
```bash
|
||||
chmod 750 /persistent/ngit-grasp
|
||||
chown ngit-grasp-production:ngit-grasp /persistent/ngit-grasp
|
||||
```
|
||||
|
||||
3. **Use HTTPS (reverse proxy required):**
|
||||
- ngit-grasp binds to localhost by default
|
||||
- Use Caddy/nginx for TLS termination
|
||||
- Caddy handles certificates automatically
|
||||
|
||||
4. **Monitor logs regularly:**
|
||||
```bash
|
||||
journalctl -u ngit-grasp-production --since today | grep -i error
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Prometheus Metrics
|
||||
|
||||
ngit-grasp exposes Prometheus metrics at `/metrics`:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8082/metrics
|
||||
```
|
||||
|
||||
See [Prometheus Setup](./prometheus-setup.md) for complete monitoring guide.
|
||||
|
||||
### Basic Health Checks
|
||||
|
||||
```bash
|
||||
# Check if service is running
|
||||
systemctl is-active ngit-grasp-production
|
||||
|
||||
# Check if port is listening
|
||||
nc -zv localhost 8082
|
||||
|
||||
# Check relay info
|
||||
curl https://ngit.example.com -H "Accept: application/nostr+json"
|
||||
|
||||
# Check disk usage
|
||||
du -sh /persistent/ngit-grasp/*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Configuration Reference](../reference/configuration.md) - All configuration options
|
||||
- [NixOS Module](../../nix/module.nix) - Module source code
|
||||
- [Example Configuration](../../nix/example-configuration.nix) - More examples
|
||||
- [Prometheus Setup](./prometheus-setup.md) - Monitoring guide
|
||||
- [Nix Flakes How-To](./nix-flakes.md) - Nix development environment
|
||||
|
||||
---
|
||||
|
||||
*Part of the [ngit-grasp how-to guides](./)*
|
||||
Kubernetes can run the container as a one-replica StatefulSet with a
|
||||
ReadWriteOnce volume, but the repository does not yet ship or promise a Helm
|
||||
chart. A container host or systemd service is simpler unless Kubernetes is an
|
||||
existing operational requirement.
|
||||
|
||||
@@ -24,6 +24,13 @@ Reference documentation provides **factual, technical information** that you loo
|
||||
|
||||
## Available Reference Documentation
|
||||
|
||||
### [Deployment Contract](deployment-contract.md)
|
||||
|
||||
Shared process, endpoint, persistence, identity, backup, single-writer, health,
|
||||
and upgrade requirements for every supported hosting environment.
|
||||
|
||||
---
|
||||
|
||||
### [Configuration](configuration.md)
|
||||
**Complete reference for all configuration options**
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Copy this file to fly.toml, then change the app, primary_region, and
|
||||
# NGIT_DOMAIN values before creating the Fly app.
|
||||
app = "replace-with-a-unique-ngit-grasp-name"
|
||||
primary_region = "ord"
|
||||
kill_signal = "SIGTERM"
|
||||
kill_timeout = 300
|
||||
|
||||
[build]
|
||||
dockerfile = "Dockerfile"
|
||||
|
||||
[env]
|
||||
NGIT_DOMAIN = "ngit.example.com"
|
||||
NGIT_BASE_PATH = "/"
|
||||
NGIT_BIND_ADDRESS = "0.0.0.0:7334"
|
||||
NGIT_GIT_DATA_PATH = "/data/git"
|
||||
NGIT_RELAY_DATA_PATH = "/data/relay"
|
||||
NGIT_LOG_LEVEL = "info"
|
||||
|
||||
[deploy]
|
||||
strategy = "immediate"
|
||||
|
||||
[http_service]
|
||||
internal_port = 7334
|
||||
force_https = true
|
||||
auto_stop_machines = "off"
|
||||
auto_start_machines = true
|
||||
min_machines_running = 1
|
||||
|
||||
[[http_service.checks]]
|
||||
grace_period = "30s"
|
||||
interval = "30s"
|
||||
method = "GET"
|
||||
timeout = "5s"
|
||||
path = "/"
|
||||
|
||||
[[mounts]]
|
||||
source = "ngit_grasp_data"
|
||||
destination = "/data"
|
||||
|
||||
[[vm]]
|
||||
size = "shared-cpu-1x"
|
||||
memory = "1gb"
|
||||
@@ -699,7 +699,7 @@ fn git_push(cwd: &Path, url: &str, refname: &str) -> std::io::Result<std::proces
|
||||
///
|
||||
/// Spec-correct rejection looks like:
|
||||
/// - exit non-zero, AND
|
||||
/// - stderr does NOT contain `not found` / `Not Found` / similar 404 hints.
|
||||
/// - stderr does not contain a transport-specific missing-repository message.
|
||||
///
|
||||
/// In practice this means the rejection came from `git-receive-pack` on the
|
||||
/// server side (typically an `ERR` pkt-line such as
|
||||
@@ -717,10 +717,7 @@ fn check_push_rejected_not_404(cwd: &Path, url: &str, refname: &str) -> Result<(
|
||||
}
|
||||
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
// Match git's two common 404 phrasings. `to_lowercase` keeps it robust to
|
||||
// future git releases tweaking capitalisation; the substrings are stable.
|
||||
let lower = stderr.to_lowercase();
|
||||
if lower.contains("not found") || lower.contains("404") {
|
||||
if stderr_indicates_missing_prs_endpoint(&stderr) {
|
||||
return Err(format!(
|
||||
"Push to {} (ref={}) failed with a 404 — the /prs/ endpoint is not implemented. \
|
||||
GRASP-06 06.md line 11 requires the endpoint to be reachable as an empty bare \
|
||||
@@ -734,3 +731,39 @@ fn check_push_rejected_not_404(cwd: &Path, url: &str, refname: &str) -> Result<(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Recognize Git's missing-smart-HTTP-endpoint diagnostics without treating
|
||||
/// arbitrary `404` digits in an echoed URL as an HTTP status.
|
||||
fn stderr_indicates_missing_prs_endpoint(stderr: &str) -> bool {
|
||||
stderr.to_lowercase().lines().any(|line| {
|
||||
let line = line.trim();
|
||||
line.contains("requested url returned error: 404")
|
||||
|| line == "remote: repository not found."
|
||||
|| (line.starts_with("fatal: repository ") && line.ends_with(" not found"))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::stderr_indicates_missing_prs_endpoint;
|
||||
|
||||
#[test]
|
||||
fn missing_endpoint_detection_accepts_git_transport_diagnostics() {
|
||||
for stderr in [
|
||||
"fatal: unable to access 'https://example.test/repo.git/': The requested URL returned error: 404",
|
||||
"fatal: repository 'https://example.test/repo.git/' not found",
|
||||
"remote: Repository not found.",
|
||||
] {
|
||||
assert!(stderr_indicates_missing_prs_endpoint(stderr), "{stderr}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_endpoint_detection_ignores_404_digits_in_rejection_url() {
|
||||
let stderr = "remote: ERR GRASP-06: only pushes to refs/nostr/<event-id> are accepted\n\
|
||||
fatal: the remote end hung up unexpectedly\n\
|
||||
error: failed to push some refs to 'http://127.0.0.1/prs/npub/audit-probe-6218-404b.git'";
|
||||
|
||||
assert!(!stderr_indicates_missing_prs_endpoint(stderr));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
#
|
||||
# Usage:
|
||||
# 1. Add to your server's flake.nix inputs:
|
||||
# inputs.ngit-grasp.url = "github:DanConwayDev/ngit-grasp";
|
||||
# inputs.ngit-grasp.url =
|
||||
# "git+https://gitnostr.com/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git";
|
||||
#
|
||||
# 2. Import the module in your configuration:
|
||||
# imports = [ inputs.ngit-grasp.nixosModules.default ];
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
"$schema" = "https://railway.com/railway.schema.json"
|
||||
|
||||
[build]
|
||||
builder = "DOCKERFILE"
|
||||
dockerfilePath = "Dockerfile"
|
||||
|
||||
[deploy]
|
||||
healthcheckPath = "/"
|
||||
healthcheckTimeout = 300
|
||||
restartPolicyType = "ALWAYS"
|
||||
overlapSeconds = 0
|
||||
drainingSeconds = 300
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
services:
|
||||
- type: web
|
||||
name: ngit-grasp
|
||||
runtime: docker
|
||||
plan: starter
|
||||
autoDeployTrigger: off
|
||||
healthCheckPath: /
|
||||
maxShutdownDelaySeconds: 300
|
||||
disk:
|
||||
name: ngit-grasp-data
|
||||
mountPath: /data
|
||||
sizeGB: 10
|
||||
envVars:
|
||||
- key: NGIT_DOMAIN
|
||||
sync: false
|
||||
- key: NGIT_BASE_PATH
|
||||
value: /
|
||||
- key: NGIT_LOG_LEVEL
|
||||
value: info
|
||||
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