From 00f19e8d24318c4fa468e4665f79a9da5a657860 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:57:31 -0400 Subject: [PATCH 01/59] Add multi-stage Docker appliance image --- Dockerfile | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dff6f72 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,110 @@ +# syntax=docker/dockerfile:1.7 + +ARG DOTNET_VERSION=10.0 +ARG KUBO_VERSION=v0.42.0 +ARG TRUTHGATE_UID=1000 +ARG TRUTHGATE_GID=1000 + +FROM ipfs/kubo:${KUBO_VERSION} AS kubo + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-resolute AS build +WORKDIR /src + +COPY TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj TruthGate-Web/TruthGate-Web/ +COPY TruthGate-Web/TruthGate-Web.Client/TruthGate-Web.Client.csproj TruthGate-Web/TruthGate-Web.Client/ +RUN dotnet restore TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj + +COPY . . +RUN dotnet publish TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj \ + --configuration Release \ + --no-restore \ + --output /out \ + /p:UseAppHost=false + +FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION}-resolute AS runtime-base +ARG TRUTHGATE_UID +ARG TRUTHGATE_GID + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + gosu \ + tini \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd --gid "${TRUTHGATE_GID}" truthgate \ + && useradd --uid "${TRUTHGATE_UID}" --gid truthgate --create-home --shell /usr/sbin/nologin truthgate \ + && mkdir -p /home/truthgate/.aspnet \ + && ln -s /data/truthgate/secrets/data-protection-keys /home/truthgate/.aspnet/DataProtection-Keys + +COPY --from=kubo /usr/local/bin/ipfs /usr/local/bin/ipfs +COPY --chmod=0755 docker/entrypoint.sh /usr/local/bin/truthgate-entrypoint +COPY --chmod=0755 docker/healthcheck.sh /usr/local/bin/truthgate-healthcheck + +ENV ASPNETCORE_ENVIRONMENT=Production \ + DOTNET_ENVIRONMENT=Production \ + DOTNET_NOLOGO=true \ + DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true \ + HOME=/home/truthgate \ + IPFS_PATH=/data/ipfs/repo \ + TMPDIR=/run/truthgate \ + TRUTHGATE_CERT_PATH=/data/truthgate/certificates \ + TRUTHGATE_CONFIG_PATH=/data/truthgate/config/config.json \ + TRUTHGATE_DATABASE_PATH=/data/truthgate/database \ + TRUTHGATE_STATE_PATH=/data/truthgate/state \ + TRUTHGATE_HEALTH_URL=https://127.0.0.1:443/ + +EXPOSE 80 443 4001/tcp 4001/udp + +ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/truthgate-entrypoint"] +HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \ + CMD ["/usr/local/bin/truthgate-healthcheck"] + +FROM runtime-base AS production +WORKDIR /app +COPY --from=build /out . +CMD ["production"] + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-resolute AS development +ARG TRUTHGATE_UID +ARG TRUTHGATE_GID + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + git \ + gosu \ + tini \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd --gid "${TRUTHGATE_GID}" truthgate \ + && useradd --uid "${TRUTHGATE_UID}" --gid truthgate --create-home --shell /bin/bash truthgate \ + && mkdir -p /workspace /home/truthgate/.aspnet \ + && chown truthgate:truthgate /workspace \ + && ln -s /data/truthgate/secrets/data-protection-keys /home/truthgate/.aspnet/DataProtection-Keys + +COPY --from=kubo /usr/local/bin/ipfs /usr/local/bin/ipfs +COPY --chmod=0755 docker/entrypoint.sh /usr/local/bin/truthgate-entrypoint +COPY --chmod=0755 docker/healthcheck.sh /usr/local/bin/truthgate-healthcheck + +ENV ASPNETCORE_ENVIRONMENT=Development \ + DOTNET_ENVIRONMENT=Development \ + DOTNET_CLI_HOME=/tmp/dotnet \ + DOTNET_NOLOGO=true \ + DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true \ + HOME=/home/truthgate \ + IPFS_PATH=/data/ipfs/repo \ + TMPDIR=/run/truthgate \ + TRUTHGATE_CERT_PATH=/data/truthgate/certificates \ + TRUTHGATE_CONFIG_PATH=/data/truthgate/config/config.json \ + TRUTHGATE_DATABASE_PATH=/data/truthgate/database \ + TRUTHGATE_DEV_PROJECT=/workspace/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj \ + TRUTHGATE_STATE_PATH=/data/truthgate/state \ + TRUTHGATE_HEALTH_URL=http://127.0.0.1:80/ + +WORKDIR /workspace +EXPOSE 80 4001/tcp 4001/udp +ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/truthgate-entrypoint"] +HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=3 \ + CMD ["/usr/local/bin/truthgate-healthcheck"] +CMD ["development"] From 56e8041955626a018db49c0a6f25a161bfbd74d2 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:57:48 -0400 Subject: [PATCH 02/59] Add appliance process supervisor --- docker/entrypoint.sh | 173 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 docker/entrypoint.sh diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..ed09d1e --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +mode="${1:-production}" +truthgate_user="truthgate" +truthgate_group="truthgate" + +log() { + printf '[truthgate] %s\n' "$*" +} + +fail() { + printf '[truthgate] ERROR: %s\n' "$*" >&2 + exit 1 +} + +as_truthgate() { + gosu "${truthgate_user}:${truthgate_group}" "$@" +} + +require_absolute_path() { + local name="$1" + local value="$2" + [[ "${value}" = /* ]] || fail "${name} must be an absolute path; received '${value}'." +} + +: "${IPFS_PATH:=/data/ipfs/repo}" +: "${TRUTHGATE_CONFIG_PATH:=/data/truthgate/config/config.json}" +: "${TRUTHGATE_DATABASE_PATH:=/data/truthgate/database}" +: "${TRUTHGATE_CERT_PATH:=/data/truthgate/certificates}" +: "${TRUTHGATE_STATE_PATH:=/data/truthgate/state}" +: "${TMPDIR:=/run/truthgate}" + +export IPFS_PATH TRUTHGATE_CONFIG_PATH TRUTHGATE_DATABASE_PATH +export TRUTHGATE_CERT_PATH TRUTHGATE_STATE_PATH TMPDIR + +require_absolute_path IPFS_PATH "${IPFS_PATH}" +require_absolute_path TRUTHGATE_CONFIG_PATH "${TRUTHGATE_CONFIG_PATH}" +require_absolute_path TRUTHGATE_DATABASE_PATH "${TRUTHGATE_DATABASE_PATH}" +require_absolute_path TRUTHGATE_CERT_PATH "${TRUTHGATE_CERT_PATH}" +require_absolute_path TRUTHGATE_STATE_PATH "${TRUTHGATE_STATE_PATH}" +require_absolute_path TMPDIR "${TMPDIR}" + +config_directory="$(dirname "${TRUTHGATE_CONFIG_PATH}")" +blocks_directory="${IPFS_PATH}/blocks" +data_protection_directory="/data/truthgate/secrets/data-protection-keys" + +install -d -m 0750 -o "${truthgate_user}" -g "${truthgate_group}" \ + "${config_directory}" \ + "${TRUTHGATE_DATABASE_PATH}" \ + "${TRUTHGATE_CERT_PATH}" \ + "${TRUTHGATE_STATE_PATH}" \ + "${data_protection_directory}" \ + "${IPFS_PATH}" \ + "${blocks_directory}" \ + "${TMPDIR}" + +# The default Compose layout mounts the repo and blockstore separately. Changing +# ownership on the mount roots is cheap and avoids recursively walking a large +# existing blockstore on every container start. +chown "${truthgate_user}:${truthgate_group}" \ + "${IPFS_PATH}" \ + "${blocks_directory}" \ + "${config_directory}" \ + "${TRUTHGATE_DATABASE_PATH}" \ + "${TRUTHGATE_CERT_PATH}" \ + "${TRUTHGATE_STATE_PATH}" \ + "${data_protection_directory}" \ + "${TMPDIR}" + +bootstrap_password_file="${TRUTHGATE_STATE_PATH}/bootstrap-admin-password" +if [[ ! -s "${TRUTHGATE_CONFIG_PATH}" ]]; then + if [[ -n "${TRUTHGATE_BOOTSTRAP_ADMIN_PASSWORD:-}" ]]; then + bootstrap_password="${TRUTHGATE_BOOTSTRAP_ADMIN_PASSWORD}" + elif [[ -s "${bootstrap_password_file}" ]]; then + bootstrap_password="$(<"${bootstrap_password_file}")" + else + bootstrap_password="$(od -An -N24 -tx1 /dev/urandom | tr -d ' \n')" + umask 077 + printf '%s' "${bootstrap_password}" > "${bootstrap_password_file}" + chown "${truthgate_user}:${truthgate_group}" "${bootstrap_password_file}" + fi + + export TRUTHGATE_BOOTSTRAP_ADMIN_PASSWORD="${bootstrap_password}" + log "First-run administrator account: admin" + log "First-run administrator password: ${bootstrap_password}" + log "The password is retained at ${bootstrap_password_file} until the TruthGate config is persisted." +else + rm -f "${bootstrap_password_file}" + unset TRUTHGATE_BOOTSTRAP_ADMIN_PASSWORD || true +fi + +if [[ ! -s "${IPFS_PATH}/config" ]]; then + log "Initializing a new Kubo repository at ${IPFS_PATH}." + as_truthgate ipfs init +else + log "Using the existing Kubo repository at ${IPFS_PATH}." +fi + +# TruthGate talks to Kubo over loopback. The RPC API is intentionally not +# published by Compose; the gateway is likewise kept private behind TruthGate. +as_truthgate ipfs config Addresses.API /ip4/127.0.0.1/tcp/5001 +as_truthgate ipfs config Addresses.Gateway /ip4/127.0.0.1/tcp/9010 + +log "Starting Kubo with automatic repository migrations enabled." +as_truthgate ipfs daemon --migrate=true & +ipfs_pid=$! + +stop_children() { + trap - TERM INT + + if [[ -n "${app_pid:-}" ]] && kill -0 "${app_pid}" 2>/dev/null; then + kill -TERM "${app_pid}" 2>/dev/null || true + fi + + if kill -0 "${ipfs_pid}" 2>/dev/null; then + kill -TERM "${ipfs_pid}" 2>/dev/null || true + fi + + [[ -z "${app_pid:-}" ]] || wait "${app_pid}" 2>/dev/null || true + wait "${ipfs_pid}" 2>/dev/null || true +} + +trap 'stop_children; exit 143' TERM +trap 'stop_children; exit 130' INT + +kubo_ready=false +for _ in $(seq 1 120); do + if as_truthgate ipfs --api=/ip4/127.0.0.1/tcp/5001 id >/dev/null 2>&1; then + kubo_ready=true + break + fi + + if ! kill -0 "${ipfs_pid}" 2>/dev/null; then + wait "${ipfs_pid}" || true + fail "Kubo exited before its RPC API became ready." + fi + + sleep 1 +done + +[[ "${kubo_ready}" == true ]] || fail "Kubo did not become ready within 120 seconds." +log "Kubo is ready." + +case "${mode}" in + production) + [[ -f /app/TruthGate-Web.dll ]] || fail "Production application was not found at /app/TruthGate-Web.dll." + log "Starting TruthGate in production mode." + as_truthgate dotnet /app/TruthGate-Web.dll & + ;; + development) + project="${TRUTHGATE_DEV_PROJECT:-/workspace/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj}" + [[ -f "${project}" ]] || fail "Development project was not found at ${project}. Is the repository mounted at /workspace?" + export ASPNETCORE_URLS="${ASPNETCORE_URLS:-http://0.0.0.0:80}" + export DOTNET_USE_POLLING_FILE_WATCHER="${DOTNET_USE_POLLING_FILE_WATCHER:-1}" + log "Starting TruthGate with dotnet watch (${project})." + as_truthgate dotnet watch --project "${project}" run --no-launch-profile --urls "${ASPNETCORE_URLS}" & + ;; + *) + fail "Unknown run mode '${mode}'. Expected 'production' or 'development'." + ;; +esac + +app_pid=$! + +set +e +wait -n "${ipfs_pid}" "${app_pid}" +exit_code=$? +set -e + +log "A managed process exited with status ${exit_code}; stopping the remaining process." +stop_children +exit "${exit_code}" From c74db8ad685c4246ad99697aeb3e5381bfe63644 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:57:55 -0400 Subject: [PATCH 03/59] Add container health check --- docker/healthcheck.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 docker/healthcheck.sh diff --git a/docker/healthcheck.sh b/docker/healthcheck.sh new file mode 100644 index 0000000..4172a47 --- /dev/null +++ b/docker/healthcheck.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +ipfs --api=/ip4/127.0.0.1/tcp/5001 diag healthy >/dev/null + +health_url="${TRUTHGATE_HEALTH_URL:-https://127.0.0.1:443/}" +http_status="$(curl \ + --silent \ + --show-error \ + --insecure \ + --max-time 5 \ + --output /dev/null \ + --write-out '%{http_code}' \ + "${health_url}")" + +[[ "${http_status}" =~ ^[0-9]{3}$ ]] +(( 10#${http_status} >= 200 && 10#${http_status} < 500 )) From 806dfd6496f5819ae370963ce6b2dfddc034d3e1 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:58:05 -0400 Subject: [PATCH 04/59] Add production Compose appliance --- compose.yaml | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 compose.yaml diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..dfee427 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,58 @@ +name: truthgate + +services: + truthgate: + image: ${TRUTHGATE_IMAGE:-ghcr.io/magiccodingman/truthgate-ipfs:master} + build: + context: . + dockerfile: Dockerfile + target: production + args: + DOTNET_VERSION: ${DOTNET_VERSION:-10.0} + KUBO_VERSION: ${KUBO_VERSION:-v0.42.0} + TRUTHGATE_UID: ${TRUTHGATE_UID:-1000} + TRUTHGATE_GID: ${TRUTHGATE_GID:-1000} + container_name: ${TRUTHGATE_CONTAINER_NAME:-truthgate} + restart: unless-stopped + read_only: true + security_opt: + - no-new-privileges:true + sysctls: + net.ipv4.ip_unprivileged_port_start: "0" + environment: + ASPNETCORE_ENVIRONMENT: Production + DOTNET_ENVIRONMENT: Production + IPFS_PATH: /data/ipfs/repo + TMPDIR: /run/truthgate + TRUTHGATE_ACME_STAGING: ${TRUTHGATE_ACME_STAGING:-false} + TRUTHGATE_CERT_IPS: ${TRUTHGATE_CERT_IPS:-} + TRUTHGATE_CERT_PATH: /data/truthgate/certificates + TRUTHGATE_CONFIG_PATH: /data/truthgate/config/config.json + TRUTHGATE_DATABASE_PATH: /data/truthgate/database + TRUTHGATE_STATE_PATH: /data/truthgate/state + TRUTHGATE_HEALTH_URL: https://127.0.0.1:443/ + ports: + - "${TRUTHGATE_HTTP_PORT:-80}:80/tcp" + - "${TRUTHGATE_HTTPS_PORT:-443}:443/tcp" + - "${IPFS_SWARM_PORT:-4001}:4001/tcp" + - "${IPFS_SWARM_PORT:-4001}:4001/udp" + volumes: + - type: bind + source: ${TRUTHGATE_DATA_HOST_PATH:-./data/truthgate} + target: /data/truthgate + - type: bind + source: ${IPFS_REPO_HOST_PATH:-./data/ipfs/repo} + target: /data/ipfs/repo + - type: bind + source: ${IPFS_BLOCKS_HOST_PATH:-./data/ipfs/blocks} + target: /data/ipfs/repo/blocks + tmpfs: + - /tmp:rw,noexec,nosuid,size=256m + - /run/truthgate:rw,noexec,nosuid,size=64m + healthcheck: + test: ["CMD", "/usr/local/bin/truthgate-healthcheck"] + interval: 30s + timeout: 10s + start_period: 90s + retries: 3 + stop_grace_period: 45s From 82a214298c5666ccae447425517faeb9272b18e6 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:58:13 -0400 Subject: [PATCH 05/59] Add Docker development override --- compose.dev.yaml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 compose.dev.yaml diff --git a/compose.dev.yaml b/compose.dev.yaml new file mode 100644 index 0000000..b1a9952 --- /dev/null +++ b/compose.dev.yaml @@ -0,0 +1,30 @@ +services: + truthgate: + image: truthgate-ipfs:dev + build: + target: development + command: ["development"] + restart: "no" + read_only: false + working_dir: /workspace + environment: + ASPNETCORE_ENVIRONMENT: Development + DOTNET_ENVIRONMENT: Development + ASPNETCORE_URLS: http://0.0.0.0:80 + DOTNET_USE_POLLING_FILE_WATCHER: "1" + TRUTHGATE_DEV_PROJECT: /workspace/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj + TRUTHGATE_HEALTH_URL: http://127.0.0.1:80/ + ports: !override + - "${TRUTHGATE_DEV_HTTP_PORT:-8080}:80/tcp" + - "${IPFS_SWARM_PORT:-4001}:4001/tcp" + - "${IPFS_SWARM_PORT:-4001}:4001/udp" + volumes: + - type: bind + source: . + target: /workspace + - type: volume + source: truthgate_nuget + target: /home/truthgate/.nuget/packages + +volumes: + truthgate_nuget: From 983a8019431343ef8e515c4c5ba38eab77028d2c Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:58:21 -0400 Subject: [PATCH 06/59] Add Docker environment template --- .env.example | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..659b47c --- /dev/null +++ b/.env.example @@ -0,0 +1,25 @@ +# Published image. Local builds use the same Compose file and can override this. +TRUTHGATE_IMAGE=ghcr.io/magiccodingman/truthgate-ipfs:master + +# Build inputs. Both official base images are multi-platform. +DOTNET_VERSION=10.0 +KUBO_VERSION=v0.42.0 +TRUTHGATE_UID=1000 +TRUTHGATE_GID=1000 + +# Host ports. +TRUTHGATE_HTTP_PORT=80 +TRUTHGATE_HTTPS_PORT=443 +TRUTHGATE_DEV_HTTP_PORT=8080 +IPFS_SWARM_PORT=4001 + +# Persistent host paths. The blockstore is always separate so it can later be +# moved to different storage without changing the container's Kubo layout. +TRUTHGATE_DATA_HOST_PATH=./data/truthgate +IPFS_REPO_HOST_PATH=./data/ipfs/repo +IPFS_BLOCKS_HOST_PATH=./data/ipfs/blocks + +# Certificate behavior. +TRUTHGATE_ACME_STAGING=false +# Optional comma-separated IP addresses for the self-signed fallback cert. +TRUTHGATE_CERT_IPS= From 1c6c54033c23c668b315b612d930bb6dc0b89067 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:58:28 -0400 Subject: [PATCH 07/59] Add Docker build exclusions --- .dockerignore | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b354484 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +.git +.github +.idea +.vs +.env +.env.* +!.env.example +**/bin +**/obj +**/TestResults +**/*.user +**/*.suo +coverage +artifacts +data +Test.TruthGate +TruthGate-Web/TruthGate-Web.Tests From 5d8a1a0cd860af71177e73fa7e0f61f4b1878e08 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:58:33 -0400 Subject: [PATCH 08/59] Ignore local appliance state --- data/.gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 data/.gitignore diff --git a/data/.gitignore b/data/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore From a4615dd32078076db458aca5aa27c0dc95b08a59 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:58:44 -0400 Subject: [PATCH 09/59] Document Docker appliance architecture --- DOCKER.md | 130 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 DOCKER.md diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..c02383e --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,130 @@ +# TruthGate Docker appliance + +TruthGate ships as one container containing the TruthGate ASP.NET application, +Kubo, and the matching `ipfs` CLI. Docker owns the software lifecycle; mounted +state survives image replacement. + +## Requirements + +- Docker Engine with the Compose v2 plugin +- Docker Compose 2.24.4 or newer for the development override's `!override` + merge directive +- TCP ports 80, 443, and 4001 plus UDP port 4001 available by default + +## Production quick start + +```bash +cp .env.example .env +docker compose up --build -d +``` + +Open `https://localhost` (the first connection uses TruthGate's self-signed +fallback certificate unless a configured domain has an issued certificate). + +On the first boot, retrieve the generated administrator password with: + +```bash +docker compose logs truthgate +``` + +The username is `admin`. Change its password in the TruthGate UI. Until the +configuration is persisted, the bootstrap password is also retained at +`data/truthgate/state/bootstrap-admin-password` with restrictive permissions. + +## Pulling the published image + +After the multi-platform image has been published by GitHub Actions: + +```bash +docker compose pull +docker compose up -d +``` + +The same `master` tag resolves to the correct `linux/amd64` or `linux/arm64` +image automatically. Replacing the container does not replace mounted state. + +## Persistent storage contract + +The default deployment keeps three host paths separate: + +```text +data/ +├── truthgate/ # config, database, certificates, secrets, app state +└── ipfs/ + ├── repo/ # Kubo identity, config, datastore, keystore, repo version + └── blocks/ # Kubo block files +``` + +They are mounted as: + +```text +/data/truthgate +/data/ipfs/repo +/data/ipfs/repo/blocks +``` + +Kubo still sees a conventional `$IPFS_PATH/blocks` directory, while the host +can later relocate only the blockstore by changing `IPFS_BLOCKS_HOST_PATH`. +No network filesystem, FUSE, JuiceFS, mount management, or mount health logic +is included in this initial implementation. + +The software inside the image is disposable. Backups should target the mounted +state, not the .NET runtime, Kubo binary, application binaries, or build output. + +## Kubo startup and migrations + +The entrypoint initializes Kubo only when `/data/ipfs/repo/config` is absent. +Existing repositories are preserved. Every start uses: + +```bash +ipfs daemon --migrate=true +``` + +This lets Kubo perform supported repository migrations when a newer image is +started against older persistent data. The RPC API listens only on container +loopback at port 5001, and the Kubo HTTP gateway listens only on loopback at +port 9010. Neither is published to the host. + +## Development + +Development uses the same Dockerfile and base Compose service: + +```bash +docker compose -f compose.yaml -f compose.dev.yaml up --build +``` + +The override changes the image target to the .NET SDK development stage, +bind-mounts the repository at `/workspace`, adds a persistent NuGet cache, and +runs the web project with `dotnet watch`. TruthGate is available at +`http://localhost:8080` by default. + +Kubo initialization, repository migrations, persistent paths, startup order, +and process supervision are shared with production. This prevents development +from silently using a different node layout. + +Rider can attach to the running development container or use the container's +SDK. The source directory is mounted directly, so normal edits trigger +`dotnet watch`. + +## Configuration + +All ordinary host-facing settings live in `.env`. Important values include: + +- `TRUTHGATE_IMAGE` +- `KUBO_VERSION` +- `TRUTHGATE_HTTP_PORT` +- `TRUTHGATE_HTTPS_PORT` +- `IPFS_SWARM_PORT` +- `TRUTHGATE_DATA_HOST_PATH` +- `IPFS_REPO_HOST_PATH` +- `IPFS_BLOCKS_HOST_PATH` + +Use absolute host paths for advanced deployments. Relative defaults are rooted +at the directory containing `compose.yaml`. + +## Process model + +`tini` is PID 1. The entrypoint starts Kubo first, waits for its RPC API, then +starts TruthGate. If either process exits, the entrypoint terminates the other +and exits so Docker's restart policy can recover the appliance. SIGTERM and +SIGINT are forwarded to both processes for graceful shutdown. From 1769e83f8b7612059340ff40b8d8ca184fe553b4 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:58:54 -0400 Subject: [PATCH 10/59] Document Docker quick start --- README.md | 58 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index bb26024..ef04aca 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # TruthGate - > The Secure, Self-Hosted Edge Gateway IPFS Always Needed, with Logins, API Keys, GUI Control, and Web3 Site Publishing. > **[truthgate.io](https://truthgate.io)** for full docs, guides, and live demos. @@ -19,17 +18,48 @@ --- +## Docker quick start + +TruthGate, Kubo, and the matching `ipfs` CLI run together as one appliance +container. Application state, Kubo repository metadata, and Kubo blocks are +persisted separately by default. + +```bash +cp .env.example .env +docker compose up --build -d +docker compose logs truthgate +``` + +The first-start logs contain a generated password for the `admin` account. +Open `https://localhost`, accept the temporary self-signed fallback certificate, +and change the password. + +For the full persistence contract, image update flow, ARM64/AMD64 publishing, +and Docker-based development setup, see **[DOCKER.md](DOCKER.md)**. + +Development with hot reload uses the production definition plus a small +override: + +```bash +docker compose -f compose.yaml -f compose.dev.yaml up --build +``` + +Then open `http://localhost:8080`. + +--- + ## What Is It? TruthGate is a **secure edge layer for IPFS nodes**. -Think **Netlify, but for IPFS**, self-hosted, login-protected, and actually yours. +Think **Netlify, but for IPFS**, self-hosted, login-protected, and actually yours. + +It solves the problems developers hit with IPFS: -It solves the problems developers hit with IPFS: -- Node exposure risks -- SSL/domain linking headaches -- CLI-only publishing -- Zero protection for `/api` or `/webui` routes -- Reliance on public gateways +- Node exposure risks +- SSL/domain linking headaches +- CLI-only publishing +- Zero protection for `/api` or `/webui` routes +- Reliance on public gateways [See how it works](https://truthgate.io) @@ -37,15 +67,15 @@ It solves the problems developers hit with IPFS: ## Get Started -Head to **[truthgate.io](https://truthgate.io)** for installation, configuration, and publishing guides. +Head to **[truthgate.io](https://truthgate.io)** for installation, configuration, and publishing guides. --- ## A Note from the Creator -I built this out of frustration. +I built this out of frustration. -I wanted a way to serve Web3-native apps that *actually worked*, securely, reliably, and without selling my soul to centralized hosts. Now it’s real. +I wanted a way to serve Web3-native apps that *actually worked*, securely, reliably, and without selling my soul to centralized hosts. Now it’s real. If you’ve ever wrestled with IPFS routing, SSL certs, or gateway hacks just to get your site online, **TruthGate is for you.** @@ -55,11 +85,11 @@ If you’ve ever wrestled with IPFS routing, SSL certs, or gateway hacks just to Pull requests welcome. Stars encourage me. -Issues are sacred. +Issues are sacred. -Let’s fix decentralized hosting together. +Let’s fix decentralized hosting together. -[truthgate.io](https://truthgate.io) +[truthgate.io](https://truthgate.io) --- From 539a64a05cb307d272603607ef5f2a6412f6d32d Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:59:08 -0400 Subject: [PATCH 11/59] Generate a secure Docker bootstrap admin password --- TruthGate-Web/TruthGate-Web/Models/Config.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Models/Config.cs b/TruthGate-Web/TruthGate-Web/Models/Config.cs index 4a88011..f5e6608 100644 --- a/TruthGate-Web/TruthGate-Web/Models/Config.cs +++ b/TruthGate-Web/TruthGate-Web/Models/Config.cs @@ -1,8 +1,12 @@ -namespace TruthGate_Web.Models +using TruthGate_Web.Utils; + +namespace TruthGate_Web.Models { public class Config { public const string DefaultAdminHash = "/EUxvODjrpkTKnal6nVEAh2m+52H4OgXGEBLcE3xilcgZ8gbeE5ay/CfzYr9PCJ0"; + public const string BootstrapAdminPasswordEnvironmentVariable = "TRUTHGATE_BOOTSTRAP_ADMIN_PASSWORD"; + private List? _users; public IpnsWildCardSubDomain IpnsWildCardSubDomain { get; set; } public List Domains { get; set; } = new List(); @@ -29,7 +33,7 @@ public List Users _users.Add(new UserAccount { UserName = "admin", - PasswordHashed = DefaultAdminHash + PasswordHashed = CreateInitialAdminHash() }); } @@ -41,6 +45,14 @@ public List Users _users = value is null ? new List() : new List(value); } } + + private static string CreateInitialAdminHash() + { + var bootstrapPassword = Environment.GetEnvironmentVariable(BootstrapAdminPasswordEnvironmentVariable); + return string.IsNullOrWhiteSpace(bootstrapPassword) + ? DefaultAdminHash + : StringHasher.HashString(bootstrapPassword); + } } public class IpnsWildCardSubDomain @@ -49,7 +61,7 @@ public class IpnsWildCardSubDomain public string UseSSL { get; set; } = "true"; } - public class EdgeDomain + public class EdgeDomain { public string Domain { get; set; } = ""; public string UseSSL { get; set; } = "false"; @@ -75,6 +87,7 @@ public class ApiKey public string Name { get; set; } public string KeyHashed { get; set; } } + public class UserAccount { public string UserName { get; set; } From e691336a822a25a8d3a93247bcf9ee77b689b0f0 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:59:16 -0400 Subject: [PATCH 12/59] Separate persistent database storage --- .../Security/RateLimiterRegistration.cs | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Security/RateLimiterRegistration.cs b/TruthGate-Web/TruthGate-Web/Security/RateLimiterRegistration.cs index 7da9060..d9141a8 100644 --- a/TruthGate-Web/TruthGate-Web/Security/RateLimiterRegistration.cs +++ b/TruthGate-Web/TruthGate-Web/Security/RateLimiterRegistration.cs @@ -1,4 +1,4 @@ -using TruthGate_Web.Middleware; +using TruthGate_Web.Middleware; using TruthGate_Web.Services; using Microsoft.EntityFrameworkCore; @@ -12,20 +12,38 @@ public static IServiceCollection AddTruthGateRateLimiter( { services.AddDbContextFactory((sp, b) => { - // Safe to resolve dependencies here var cfg = sp.GetRequiredService(); - var tet = cfg.Get(); - // Figure out SQLite location from cfg.ConfigPath (directory portion) - var dir = Path.GetDirectoryName(cfg.ConfigPath) ?? AppContext.BaseDirectory; - Directory.CreateDirectory(dir); - var dbPath = Path.Combine(dir, "ratelimiter.db"); + var configuredDatabasePath = Environment.GetEnvironmentVariable("TRUTHGATE_DATABASE_PATH"); + + string dbPath; + if (string.IsNullOrWhiteSpace(configuredDatabasePath)) + { + // Preserve the historical behavior for non-container installations. + var configDirectory = Path.GetDirectoryName(cfg.ConfigPath) ?? AppContext.BaseDirectory; + Directory.CreateDirectory(configDirectory); + dbPath = Path.Combine(configDirectory, "ratelimiter.db"); + } + else + { + var resolved = Path.GetFullPath(configuredDatabasePath); + if (string.Equals(Path.GetExtension(resolved), ".db", StringComparison.OrdinalIgnoreCase)) + { + Directory.CreateDirectory(Path.GetDirectoryName(resolved)!); + dbPath = resolved; + } + else + { + Directory.CreateDirectory(resolved); + dbPath = Path.Combine(resolved, "ratelimiter.db"); + } + } + var cs = connectionString ?? $"Data Source={dbPath};Cache=Shared"; b.UseSqlite(cs); b.EnableSensitiveDataLogging(false); }); - services.AddSingleton(); services.AddHostedService(); services.AddHostedService(); @@ -37,5 +55,4 @@ public static IServiceCollection AddTruthGateRateLimiter( public static IApplicationBuilder UseTruthGateRateLimiter(this IApplicationBuilder app) => app.UseMiddleware(); } - } From e847ca57d157ec89729fcefcb334f9bee92b6291 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:59:30 -0400 Subject: [PATCH 13/59] Add multi-platform Docker CI and publishing --- .github/workflows/docker.yml | 123 +++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 .github/workflows/docker.yml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..44be8e6 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,123 @@ +name: Docker appliance + +on: + pull_request: + push: + branches: + - master + tags: + - "v*" + workflow_dispatch: + +permissions: + contents: read + packages: write + +concurrency: + group: docker-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + IMAGE_NAME: ghcr.io/magiccodingman/truthgate-ipfs + +jobs: + validate-compose: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Validate production Compose + run: docker compose -f compose.yaml config + - name: Validate development override + run: docker compose -f compose.yaml -f compose.dev.yaml config + + smoke-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build production image (AMD64) + run: docker build --target production --tag truthgate-ipfs:test . + + - name: Build development image (AMD64) + run: docker build --target development --tag truthgate-ipfs:dev-test . + + - name: Start appliance and wait for health + shell: bash + run: | + set -Eeuo pipefail + trap 'docker compose logs --no-color || true; docker compose down --remove-orphans || true' EXIT + + mkdir -p .ci-data/truthgate .ci-data/ipfs/repo .ci-data/ipfs/blocks + + TRUTHGATE_IMAGE=truthgate-ipfs:test \ + TRUTHGATE_HTTP_PORT=18080 \ + TRUTHGATE_HTTPS_PORT=18443 \ + IPFS_SWARM_PORT=14001 \ + TRUTHGATE_DATA_HOST_PATH=./.ci-data/truthgate \ + IPFS_REPO_HOST_PATH=./.ci-data/ipfs/repo \ + IPFS_BLOCKS_HOST_PATH=./.ci-data/ipfs/blocks \ + docker compose up --detach --no-build + + for attempt in $(seq 1 60); do + status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' truthgate)" + if [[ "${status}" == "healthy" ]]; then + docker compose logs --no-color + exit 0 + fi + + if [[ "${status}" == "unhealthy" ]]; then + echo "TruthGate became unhealthy." + exit 1 + fi + + sleep 5 + done + + echo "TruthGate did not become healthy in time." + exit 1 + + build-and-publish: + needs: + - validate-compose + - smoke-test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-qemu-action@v3 + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate image metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_NAME }} + tags: | + type=raw,value=master,enable=${{ github.ref == 'refs/heads/master' }} + type=sha,prefix=sha- + type=ref,event=tag + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + + - name: Build and optionally publish AMD64 + ARM64 + uses: docker/build-push-action@v6 + with: + context: . + target: production + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: mode=max + sbom: true From 58cd02042aa3f4c6f37c89efa837bf8ea4d910fe Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:02:21 -0400 Subject: [PATCH 14/59] Preserve Docker build diagnostics --- .github/workflows/docker.yml | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 44be8e6..c1fd7d5 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -36,10 +36,34 @@ jobs: - uses: actions/checkout@v4 - name: Build production image (AMD64) - run: docker build --target production --tag truthgate-ipfs:test . + shell: bash + run: | + set -o pipefail + docker build --progress=plain --target production --tag truthgate-ipfs:test . 2>&1 \ + | tee production-image-build.log + + - name: Upload production build diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: production-image-build + path: production-image-build.log + if-no-files-found: error - name: Build development image (AMD64) - run: docker build --target development --tag truthgate-ipfs:dev-test . + shell: bash + run: | + set -o pipefail + docker build --progress=plain --target development --tag truthgate-ipfs:dev-test . 2>&1 \ + | tee development-image-build.log + + - name: Upload development build diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: development-image-build + path: development-image-build.log + if-no-files-found: ignore - name: Start appliance and wait for health shell: bash From 4ca64b417c2da63fa3ea3a7ecd0a7766ea56660f Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:03:51 -0400 Subject: [PATCH 15/59] Reuse existing base image groups safely --- Dockerfile | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index dff6f72..d739dd0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,8 +32,12 @@ RUN apt-get update \ gosu \ tini \ && rm -rf /var/lib/apt/lists/* \ - && groupadd --gid "${TRUTHGATE_GID}" truthgate \ - && useradd --uid "${TRUTHGATE_UID}" --gid truthgate --create-home --shell /usr/sbin/nologin truthgate \ + && if ! getent group "${TRUTHGATE_GID}" >/dev/null; then groupadd --gid "${TRUTHGATE_GID}" truthgate; fi \ + && if existing_user="$(getent passwd "${TRUTHGATE_UID}" | cut -d: -f1)" && [ -n "${existing_user}" ]; then \ + usermod --login truthgate --home /home/truthgate --move-home --shell /usr/sbin/nologin "${existing_user}"; \ + else \ + useradd --uid "${TRUTHGATE_UID}" --gid "${TRUTHGATE_GID}" --create-home --shell /usr/sbin/nologin truthgate; \ + fi \ && mkdir -p /home/truthgate/.aspnet \ && ln -s /data/truthgate/secrets/data-protection-keys /home/truthgate/.aspnet/DataProtection-Keys @@ -77,10 +81,14 @@ RUN apt-get update \ gosu \ tini \ && rm -rf /var/lib/apt/lists/* \ - && groupadd --gid "${TRUTHGATE_GID}" truthgate \ - && useradd --uid "${TRUTHGATE_UID}" --gid truthgate --create-home --shell /bin/bash truthgate \ + && if ! getent group "${TRUTHGATE_GID}" >/dev/null; then groupadd --gid "${TRUTHGATE_GID}" truthgate; fi \ + && if existing_user="$(getent passwd "${TRUTHGATE_UID}" | cut -d: -f1)" && [ -n "${existing_user}" ]; then \ + usermod --login truthgate --home /home/truthgate --move-home --shell /bin/bash "${existing_user}"; \ + else \ + useradd --uid "${TRUTHGATE_UID}" --gid "${TRUTHGATE_GID}" --create-home --shell /bin/bash truthgate; \ + fi \ && mkdir -p /workspace /home/truthgate/.aspnet \ - && chown truthgate:truthgate /workspace \ + && chown "${TRUTHGATE_UID}:${TRUTHGATE_GID}" /workspace \ && ln -s /data/truthgate/secrets/data-protection-keys /home/truthgate/.aspnet/DataProtection-Keys COPY --from=kubo /usr/local/bin/ipfs /usr/local/bin/ipfs From e0f63662622052a8f4e7588c1417811fa10eb124 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:04:17 -0400 Subject: [PATCH 16/59] Use the runtime user's numeric primary group --- docker/entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index ed09d1e..6e53846 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -3,7 +3,7 @@ set -Eeuo pipefail mode="${1:-production}" truthgate_user="truthgate" -truthgate_group="truthgate" +truthgate_group="$(id -g "${truthgate_user}")" log() { printf '[truthgate] %s\n' "$*" From 3ed41df14219468322cb8cf9d1f990d2b8ad9aa7 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:11:21 -0400 Subject: [PATCH 17/59] Cross-compile .NET efficiently for multi-arch images --- Dockerfile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index d739dd0..b446b17 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,17 +7,22 @@ ARG TRUTHGATE_GID=1000 FROM ipfs/kubo:${KUBO_VERSION} AS kubo -FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-resolute AS build +# Compile on the builder's native CPU while targeting the requested image +# architecture. This avoids running the full .NET build under QEMU for ARM64. +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-resolute AS build +ARG TARGETARCH WORKDIR /src COPY TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj TruthGate-Web/TruthGate-Web/ COPY TruthGate-Web/TruthGate-Web.Client/TruthGate-Web.Client.csproj TruthGate-Web/TruthGate-Web.Client/ -RUN dotnet restore TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj +RUN dotnet restore TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj --arch "${TARGETARCH}" COPY . . RUN dotnet publish TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj \ --configuration Release \ + --arch "${TARGETARCH}" \ --no-restore \ + --no-self-contained \ --output /out \ /p:UseAppHost=false From 74b50b13f6aaf71beccb336d018a82086407d6d6 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:17:41 -0400 Subject: [PATCH 18/59] Validate the appliance natively on AMD64 and ARM64 --- .github/workflows/docker.yml | 127 ++++++++++++++++++++++++----------- 1 file changed, 88 insertions(+), 39 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index c1fd7d5..ca33b79 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -22,7 +22,7 @@ env: jobs: validate-compose: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Validate production Compose @@ -31,80 +31,130 @@ jobs: run: docker compose -f compose.yaml -f compose.dev.yaml config smoke-test: - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - architecture: amd64 + runner: ubuntu-24.04 + - architecture: arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v4 - - name: Build production image (AMD64) + - name: Build production image (${{ matrix.architecture }}) shell: bash run: | set -o pipefail - docker build --progress=plain --target production --tag truthgate-ipfs:test . 2>&1 \ - | tee production-image-build.log + docker build \ + --progress=plain \ + --platform linux/${{ matrix.architecture }} \ + --target production \ + --tag truthgate-ipfs:test-${{ matrix.architecture }} \ + . 2>&1 | tee production-image-build-${{ matrix.architecture }}.log - name: Upload production build diagnostics if: always() uses: actions/upload-artifact@v4 with: - name: production-image-build - path: production-image-build.log + name: production-image-build-${{ matrix.architecture }} + path: production-image-build-${{ matrix.architecture }}.log if-no-files-found: error - - name: Build development image (AMD64) + - name: Build development image (${{ matrix.architecture }}) shell: bash run: | set -o pipefail - docker build --progress=plain --target development --tag truthgate-ipfs:dev-test . 2>&1 \ - | tee development-image-build.log + docker build \ + --progress=plain \ + --platform linux/${{ matrix.architecture }} \ + --target development \ + --tag truthgate-ipfs:dev-test-${{ matrix.architecture }} \ + . 2>&1 | tee development-image-build-${{ matrix.architecture }}.log - name: Upload development build diagnostics if: always() uses: actions/upload-artifact@v4 with: - name: development-image-build - path: development-image-build.log + name: development-image-build-${{ matrix.architecture }} + path: development-image-build-${{ matrix.architecture }}.log if-no-files-found: ignore - - name: Start appliance and wait for health + - name: Start, replace, and revalidate the appliance shell: bash run: | set -Eeuo pipefail - trap 'docker compose logs --no-color || true; docker compose down --remove-orphans || true' EXIT + + export TRUTHGATE_IMAGE=truthgate-ipfs:test-${{ matrix.architecture }} + export TRUTHGATE_HTTP_PORT=18080 + export TRUTHGATE_HTTPS_PORT=18443 + export IPFS_SWARM_PORT=14001 + export TRUTHGATE_DATA_HOST_PATH=./.ci-data/truthgate + export IPFS_REPO_HOST_PATH=./.ci-data/ipfs/repo + export IPFS_BLOCKS_HOST_PATH=./.ci-data/ipfs/blocks + + cleanup() { + docker compose logs --no-color 2>/dev/null || true + docker compose down --remove-orphans 2>/dev/null || true + } + trap cleanup EXIT + + wait_for_health() { + for attempt in $(seq 1 60); do + status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' truthgate)" + if [[ "${status}" == "healthy" ]]; then + return 0 + fi + + if [[ "${status}" == "unhealthy" ]]; then + echo "TruthGate became unhealthy." + return 1 + fi + + sleep 5 + done + + echo "TruthGate did not become healthy in time." + return 1 + } + + read_peer_id() { + docker exec truthgate \ + ipfs --api=/ip4/127.0.0.1/tcp/5001 id \ + | python3 -c 'import json, sys; print(json.load(sys.stdin)["ID"])' + } mkdir -p .ci-data/truthgate .ci-data/ipfs/repo .ci-data/ipfs/blocks - TRUTHGATE_IMAGE=truthgate-ipfs:test \ - TRUTHGATE_HTTP_PORT=18080 \ - TRUTHGATE_HTTPS_PORT=18443 \ - IPFS_SWARM_PORT=14001 \ - TRUTHGATE_DATA_HOST_PATH=./.ci-data/truthgate \ - IPFS_REPO_HOST_PATH=./.ci-data/ipfs/repo \ - IPFS_BLOCKS_HOST_PATH=./.ci-data/ipfs/blocks \ docker compose up --detach --no-build + wait_for_health + + test -s .ci-data/ipfs/repo/config + test -s .ci-data/truthgate/state/bootstrap-admin-password + peer_id_before="$(read_peer_id)" + bootstrap_hash_before="$(sha256sum .ci-data/truthgate/state/bootstrap-admin-password | cut -d' ' -f1)" - for attempt in $(seq 1 60); do - status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' truthgate)" - if [[ "${status}" == "healthy" ]]; then - docker compose logs --no-color - exit 0 - fi + # Replace the container while retaining only the documented bind-mounted + # state. The Kubo identity and first-run credential must remain stable. + docker compose down --remove-orphans + docker compose up --detach --no-build + wait_for_health - if [[ "${status}" == "unhealthy" ]]; then - echo "TruthGate became unhealthy." - exit 1 - fi + peer_id_after="$(read_peer_id)" + bootstrap_hash_after="$(sha256sum .ci-data/truthgate/state/bootstrap-admin-password | cut -d' ' -f1)" - sleep 5 - done + test "${peer_id_before}" = "${peer_id_after}" + test "${bootstrap_hash_before}" = "${bootstrap_hash_after}" - echo "TruthGate did not become healthy in time." - exit 1 + docker compose logs --no-color build-and-publish: + if: github.event_name != 'pull_request' needs: - validate-compose - smoke-test - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -113,7 +163,6 @@ jobs: - uses: docker/setup-buildx-action@v3 - name: Log in to GitHub Container Registry - if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: registry: ghcr.io @@ -132,13 +181,13 @@ jobs: type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - - name: Build and optionally publish AMD64 + ARM64 + - name: Publish AMD64 + ARM64 under one image tag uses: docker/build-push-action@v6 with: context: . target: production platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} + push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha From 07516190aa1210cb50a35d8af158bf812f58f0a4 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:20:35 -0400 Subject: [PATCH 19/59] Read protected state inside the smoke-test container --- .github/workflows/docker.yml | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ca33b79..bffe3ea 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -84,6 +84,7 @@ jobs: - name: Start, replace, and revalidate the appliance shell: bash run: | + exec > >(tee runtime-smoke-${{ matrix.architecture }}.log) 2>&1 set -Eeuo pipefail export TRUTHGATE_IMAGE=truthgate-ipfs:test-${{ matrix.architecture }} @@ -125,15 +126,23 @@ jobs: | python3 -c 'import json, sys; print(json.load(sys.stdin)["ID"])' } + read_bootstrap_hash() { + docker exec truthgate \ + sha256sum /data/truthgate/state/bootstrap-admin-password \ + | cut -d' ' -f1 + } + mkdir -p .ci-data/truthgate .ci-data/ipfs/repo .ci-data/ipfs/blocks docker compose up --detach --no-build wait_for_health - test -s .ci-data/ipfs/repo/config - test -s .ci-data/truthgate/state/bootstrap-admin-password + # Read protected state from inside the container. The host runner is + # intentionally unable to inspect the bootstrap secret directly. + docker exec truthgate test -s /data/ipfs/repo/config + docker exec truthgate test -s /data/truthgate/state/bootstrap-admin-password peer_id_before="$(read_peer_id)" - bootstrap_hash_before="$(sha256sum .ci-data/truthgate/state/bootstrap-admin-password | cut -d' ' -f1)" + bootstrap_hash_before="$(read_bootstrap_hash)" # Replace the container while retaining only the documented bind-mounted # state. The Kubo identity and first-run credential must remain stable. @@ -141,14 +150,24 @@ jobs: docker compose up --detach --no-build wait_for_health + docker exec truthgate test -s /data/ipfs/repo/config + docker exec truthgate test -s /data/truthgate/state/bootstrap-admin-password peer_id_after="$(read_peer_id)" - bootstrap_hash_after="$(sha256sum .ci-data/truthgate/state/bootstrap-admin-password | cut -d' ' -f1)" + bootstrap_hash_after="$(read_bootstrap_hash)" test "${peer_id_before}" = "${peer_id_after}" test "${bootstrap_hash_before}" = "${bootstrap_hash_after}" docker compose logs --no-color + - name: Upload runtime diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: runtime-smoke-${{ matrix.architecture }} + path: runtime-smoke-${{ matrix.architecture }}.log + if-no-files-found: ignore + build-and-publish: if: github.event_name != 'pull_request' needs: From 051d16e397a7500657658ba1932e577c889f0ad9 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:43:17 -0400 Subject: [PATCH 20/59] Add Rider-friendly merged development compose file --- compose.rider.yaml | 73 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 compose.rider.yaml diff --git a/compose.rider.yaml b/compose.rider.yaml new file mode 100644 index 0000000..e33027c --- /dev/null +++ b/compose.rider.yaml @@ -0,0 +1,73 @@ +# Rider-friendly single-file development configuration. +# Keep this file synchronized with compose.yaml + compose.dev.yaml. +name: truthgate + +services: + truthgate: + image: truthgate-ipfs:dev + build: + context: . + dockerfile: Dockerfile + target: development + args: + DOTNET_VERSION: ${DOTNET_VERSION:-10.0} + KUBO_VERSION: ${KUBO_VERSION:-v0.42.0} + TRUTHGATE_UID: ${TRUTHGATE_UID:-1000} + TRUTHGATE_GID: ${TRUTHGATE_GID:-1000} + container_name: ${TRUTHGATE_CONTAINER_NAME:-truthgate} + command: ["development"] + restart: "no" + read_only: false + working_dir: /workspace + security_opt: + - no-new-privileges:true + sysctls: + net.ipv4.ip_unprivileged_port_start: "0" + environment: + ASPNETCORE_ENVIRONMENT: Development + DOTNET_ENVIRONMENT: Development + ASPNETCORE_URLS: http://0.0.0.0:80 + DOTNET_USE_POLLING_FILE_WATCHER: "1" + IPFS_PATH: /data/ipfs/repo + TMPDIR: /run/truthgate + TRUTHGATE_ACME_STAGING: ${TRUTHGATE_ACME_STAGING:-false} + TRUTHGATE_CERT_IPS: ${TRUTHGATE_CERT_IPS:-} + TRUTHGATE_CERT_PATH: /data/truthgate/certificates + TRUTHGATE_CONFIG_PATH: /data/truthgate/config/config.json + TRUTHGATE_DATABASE_PATH: /data/truthgate/database + TRUTHGATE_STATE_PATH: /data/truthgate/state + TRUTHGATE_DEV_PROJECT: /workspace/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj + TRUTHGATE_HEALTH_URL: http://127.0.0.1:80/ + ports: + - "${TRUTHGATE_DEV_HTTP_PORT:-8080}:80/tcp" + - "${IPFS_SWARM_PORT:-4001}:4001/tcp" + - "${IPFS_SWARM_PORT:-4001}:4001/udp" + volumes: + - type: bind + source: . + target: /workspace + - type: volume + source: truthgate_nuget + target: /home/truthgate/.nuget/packages + - type: bind + source: ${TRUTHGATE_DATA_HOST_PATH:-./data/truthgate} + target: /data/truthgate + - type: bind + source: ${IPFS_REPO_HOST_PATH:-./data/ipfs/repo} + target: /data/ipfs/repo + - type: bind + source: ${IPFS_BLOCKS_HOST_PATH:-./data/ipfs/blocks} + target: /data/ipfs/repo/blocks + tmpfs: + - /tmp:rw,noexec,nosuid,size=256m + - /run/truthgate:rw,noexec,nosuid,size=64m + healthcheck: + test: ["CMD", "/usr/local/bin/truthgate-healthcheck"] + interval: 30s + timeout: 10s + start_period: 90s + retries: 3 + stop_grace_period: 45s + +volumes: + truthgate_nuget: From 98412dbc8504407b3e11026f5025a50c33a90431 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:44:50 -0400 Subject: [PATCH 21/59] Add PowerShell Rider Compose generator --- docker/generate-rider-compose.ps1 | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docker/generate-rider-compose.ps1 diff --git a/docker/generate-rider-compose.ps1 b/docker/generate-rider-compose.ps1 new file mode 100644 index 0000000..ccf3070 --- /dev/null +++ b/docker/generate-rider-compose.ps1 @@ -0,0 +1,32 @@ +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +$repositoryRoot = Split-Path -Parent $PSScriptRoot +$composeFile = Join-Path $repositoryRoot 'compose.yaml' +$devComposeFile = Join-Path $repositoryRoot 'compose.dev.yaml' +$outputFile = Join-Path $repositoryRoot 'compose.rider.yaml' +$tempFile = Join-Path $repositoryRoot ('.compose.rider.{0}.tmp' -f [Guid]::NewGuid().ToString('N')) + +try { + Push-Location $repositoryRoot + + & docker compose -f $composeFile -f $devComposeFile config | Set-Content -Path $tempFile -Encoding utf8 + + if ($LASTEXITCODE -ne 0) { + throw "docker compose config failed with exit code $LASTEXITCODE." + } + + if (-not (Test-Path $tempFile) -or (Get-Item $tempFile).Length -eq 0) { + throw 'Generated Compose file is empty.' + } + + Move-Item -Path $tempFile -Destination $outputFile -Force + Write-Host "Generated $outputFile" +} +finally { + if (Test-Path $tempFile) { + Remove-Item $tempFile -Force + } + + Pop-Location +} From 2fcdaea01c5cb113597a20d9a57dca54866abb31 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:44:57 -0400 Subject: [PATCH 22/59] Add shell Rider Compose generator --- docker/generate-rider-compose.sh | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 docker/generate-rider-compose.sh diff --git a/docker/generate-rider-compose.sh b/docker/generate-rider-compose.sh new file mode 100644 index 0000000..cff2252 --- /dev/null +++ b/docker/generate-rider-compose.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +repository_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +compose_file="${repository_root}/compose.yaml" +dev_compose_file="${repository_root}/compose.dev.yaml" +output_file="${repository_root}/compose.rider.yaml" +temp_file="$(mktemp "${repository_root}/.compose.rider.XXXXXX.tmp")" + +cleanup() { + rm -f "${temp_file}" +} +trap cleanup EXIT + +cd "${repository_root}" + +docker compose \ + -f "${compose_file}" \ + -f "${dev_compose_file}" \ + config > "${temp_file}" + +if [[ ! -s "${temp_file}" ]]; then + echo "Generated Compose file is empty." >&2 + exit 1 +fi + +mv -f "${temp_file}" "${output_file}" +trap - EXIT + +echo "Generated ${output_file}" From c6b5f3a07d85c5a2cc625646c73f55befd1b0e09 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:45:14 -0400 Subject: [PATCH 23/59] Document Rider Compose generation --- DOCKER.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/DOCKER.md b/DOCKER.md index c02383e..ff7c476 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -106,6 +106,29 @@ Rider can attach to the running development container or use the container's SDK. The source directory is mounted directly, so normal edits trigger `dotnet watch`. +### Rider single-file Compose configuration + +Some Rider versions fail while discovering services from multiple Compose +files. `compose.rider.yaml` is the merged development configuration for that +case. Regenerate it from `compose.yaml` and `compose.dev.yaml` before launch. + +PowerShell 7 on Windows, Linux, or macOS: + +```powershell +pwsh -NoProfile -File ./docker/generate-rider-compose.ps1 +``` + +Bash on Linux or macOS: + +```bash +bash ./docker/generate-rider-compose.sh +``` + +Both scripts write to a temporary file and replace `compose.rider.yaml` only +after `docker compose config` succeeds. In Rider, use `compose.rider.yaml` as +the sole Compose file and add the appropriate command as an External Tool in +the run configuration's **Before launch** tasks. + ## Configuration All ordinary host-facing settings live in `.env`. Important values include: From aa8cd8790018a4964af2307c91a4b18c082e50fd Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:46:36 -0400 Subject: [PATCH 24/59] Rename merged development compose file --- compose.dev.generated.yaml | 73 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 compose.dev.generated.yaml diff --git a/compose.dev.generated.yaml b/compose.dev.generated.yaml new file mode 100644 index 0000000..d8cbdd9 --- /dev/null +++ b/compose.dev.generated.yaml @@ -0,0 +1,73 @@ +# Generated single-file development configuration. +# Regenerate from compose.yaml + compose.dev.yaml; do not edit this file directly. +name: truthgate + +services: + truthgate: + image: truthgate-ipfs:dev + build: + context: . + dockerfile: Dockerfile + target: development + args: + DOTNET_VERSION: ${DOTNET_VERSION:-10.0} + KUBO_VERSION: ${KUBO_VERSION:-v0.42.0} + TRUTHGATE_UID: ${TRUTHGATE_UID:-1000} + TRUTHGATE_GID: ${TRUTHGATE_GID:-1000} + container_name: ${TRUTHGATE_CONTAINER_NAME:-truthgate} + command: ["development"] + restart: "no" + read_only: false + working_dir: /workspace + security_opt: + - no-new-privileges:true + sysctls: + net.ipv4.ip_unprivileged_port_start: "0" + environment: + ASPNETCORE_ENVIRONMENT: Development + DOTNET_ENVIRONMENT: Development + ASPNETCORE_URLS: http://0.0.0.0:80 + DOTNET_USE_POLLING_FILE_WATCHER: "1" + IPFS_PATH: /data/ipfs/repo + TMPDIR: /run/truthgate + TRUTHGATE_ACME_STAGING: ${TRUTHGATE_ACME_STAGING:-false} + TRUTHGATE_CERT_IPS: ${TRUTHGATE_CERT_IPS:-} + TRUTHGATE_CERT_PATH: /data/truthgate/certificates + TRUTHGATE_CONFIG_PATH: /data/truthgate/config/config.json + TRUTHGATE_DATABASE_PATH: /data/truthgate/database + TRUTHGATE_STATE_PATH: /data/truthgate/state + TRUTHGATE_DEV_PROJECT: /workspace/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj + TRUTHGATE_HEALTH_URL: http://127.0.0.1:80/ + ports: + - "${TRUTHGATE_DEV_HTTP_PORT:-8080}:80/tcp" + - "${IPFS_SWARM_PORT:-4001}:4001/tcp" + - "${IPFS_SWARM_PORT:-4001}:4001/udp" + volumes: + - type: bind + source: . + target: /workspace + - type: volume + source: truthgate_nuget + target: /home/truthgate/.nuget/packages + - type: bind + source: ${TRUTHGATE_DATA_HOST_PATH:-./data/truthgate} + target: /data/truthgate + - type: bind + source: ${IPFS_REPO_HOST_PATH:-./data/ipfs/repo} + target: /data/ipfs/repo + - type: bind + source: ${IPFS_BLOCKS_HOST_PATH:-./data/ipfs/blocks} + target: /data/ipfs/repo/blocks + tmpfs: + - /tmp:rw,noexec,nosuid,size=256m + - /run/truthgate:rw,noexec,nosuid,size=64m + healthcheck: + test: ["CMD", "/usr/local/bin/truthgate-healthcheck"] + interval: 30s + timeout: 10s + start_period: 90s + retries: 3 + stop_grace_period: 45s + +volumes: + truthgate_nuget: From 0f54cf91ff4c37ae1f4b69e3c0fae50626e26fe6 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:46:43 -0400 Subject: [PATCH 25/59] Remove Rider-specific merged compose name --- compose.rider.yaml | 73 ---------------------------------------------- 1 file changed, 73 deletions(-) delete mode 100644 compose.rider.yaml diff --git a/compose.rider.yaml b/compose.rider.yaml deleted file mode 100644 index e33027c..0000000 --- a/compose.rider.yaml +++ /dev/null @@ -1,73 +0,0 @@ -# Rider-friendly single-file development configuration. -# Keep this file synchronized with compose.yaml + compose.dev.yaml. -name: truthgate - -services: - truthgate: - image: truthgate-ipfs:dev - build: - context: . - dockerfile: Dockerfile - target: development - args: - DOTNET_VERSION: ${DOTNET_VERSION:-10.0} - KUBO_VERSION: ${KUBO_VERSION:-v0.42.0} - TRUTHGATE_UID: ${TRUTHGATE_UID:-1000} - TRUTHGATE_GID: ${TRUTHGATE_GID:-1000} - container_name: ${TRUTHGATE_CONTAINER_NAME:-truthgate} - command: ["development"] - restart: "no" - read_only: false - working_dir: /workspace - security_opt: - - no-new-privileges:true - sysctls: - net.ipv4.ip_unprivileged_port_start: "0" - environment: - ASPNETCORE_ENVIRONMENT: Development - DOTNET_ENVIRONMENT: Development - ASPNETCORE_URLS: http://0.0.0.0:80 - DOTNET_USE_POLLING_FILE_WATCHER: "1" - IPFS_PATH: /data/ipfs/repo - TMPDIR: /run/truthgate - TRUTHGATE_ACME_STAGING: ${TRUTHGATE_ACME_STAGING:-false} - TRUTHGATE_CERT_IPS: ${TRUTHGATE_CERT_IPS:-} - TRUTHGATE_CERT_PATH: /data/truthgate/certificates - TRUTHGATE_CONFIG_PATH: /data/truthgate/config/config.json - TRUTHGATE_DATABASE_PATH: /data/truthgate/database - TRUTHGATE_STATE_PATH: /data/truthgate/state - TRUTHGATE_DEV_PROJECT: /workspace/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj - TRUTHGATE_HEALTH_URL: http://127.0.0.1:80/ - ports: - - "${TRUTHGATE_DEV_HTTP_PORT:-8080}:80/tcp" - - "${IPFS_SWARM_PORT:-4001}:4001/tcp" - - "${IPFS_SWARM_PORT:-4001}:4001/udp" - volumes: - - type: bind - source: . - target: /workspace - - type: volume - source: truthgate_nuget - target: /home/truthgate/.nuget/packages - - type: bind - source: ${TRUTHGATE_DATA_HOST_PATH:-./data/truthgate} - target: /data/truthgate - - type: bind - source: ${IPFS_REPO_HOST_PATH:-./data/ipfs/repo} - target: /data/ipfs/repo - - type: bind - source: ${IPFS_BLOCKS_HOST_PATH:-./data/ipfs/blocks} - target: /data/ipfs/repo/blocks - tmpfs: - - /tmp:rw,noexec,nosuid,size=256m - - /run/truthgate:rw,noexec,nosuid,size=64m - healthcheck: - test: ["CMD", "/usr/local/bin/truthgate-healthcheck"] - interval: 30s - timeout: 10s - start_period: 90s - retries: 3 - stop_grace_period: 45s - -volumes: - truthgate_nuget: From ddc144b363e2212eac29bd2ddf35015381a9a564 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:46:50 -0400 Subject: [PATCH 26/59] Generate generic merged development compose file --- docker/generate-rider-compose.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/generate-rider-compose.ps1 b/docker/generate-rider-compose.ps1 index ccf3070..f984077 100644 --- a/docker/generate-rider-compose.ps1 +++ b/docker/generate-rider-compose.ps1 @@ -4,8 +4,8 @@ Set-StrictMode -Version Latest $repositoryRoot = Split-Path -Parent $PSScriptRoot $composeFile = Join-Path $repositoryRoot 'compose.yaml' $devComposeFile = Join-Path $repositoryRoot 'compose.dev.yaml' -$outputFile = Join-Path $repositoryRoot 'compose.rider.yaml' -$tempFile = Join-Path $repositoryRoot ('.compose.rider.{0}.tmp' -f [Guid]::NewGuid().ToString('N')) +$outputFile = Join-Path $repositoryRoot 'compose.dev.generated.yaml' +$tempFile = Join-Path $repositoryRoot ('.compose.dev.generated.{0}.tmp' -f [Guid]::NewGuid().ToString('N')) try { Push-Location $repositoryRoot From bfa00d10877b5e9787d89181b74a60d757459689 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:46:57 -0400 Subject: [PATCH 27/59] Generate generic merged development compose file --- docker/generate-rider-compose.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/generate-rider-compose.sh b/docker/generate-rider-compose.sh index cff2252..5120707 100644 --- a/docker/generate-rider-compose.sh +++ b/docker/generate-rider-compose.sh @@ -4,8 +4,8 @@ set -Eeuo pipefail repository_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" compose_file="${repository_root}/compose.yaml" dev_compose_file="${repository_root}/compose.dev.yaml" -output_file="${repository_root}/compose.rider.yaml" -temp_file="$(mktemp "${repository_root}/.compose.rider.XXXXXX.tmp")" +output_file="${repository_root}/compose.dev.generated.yaml" +temp_file="$(mktemp "${repository_root}/.compose.dev.generated.XXXXXX.tmp")" cleanup() { rm -f "${temp_file}" From 98d8a731cad634185e4fcc3068a3b34e61d1575d Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:47:16 -0400 Subject: [PATCH 28/59] Document generic generated development compose file --- DOCKER.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/DOCKER.md b/DOCKER.md index ff7c476..2ea48f1 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -106,11 +106,12 @@ Rider can attach to the running development container or use the container's SDK. The source directory is mounted directly, so normal edits trigger `dotnet watch`. -### Rider single-file Compose configuration +### Generated single-file development configuration -Some Rider versions fail while discovering services from multiple Compose -files. `compose.rider.yaml` is the merged development configuration for that -case. Regenerate it from `compose.yaml` and `compose.dev.yaml` before launch. +Some tools and IDEs fail while discovering services from multiple Compose +files. `compose.dev.generated.yaml` is the fully merged development +configuration for that case. It is generated from `compose.yaml` and +`compose.dev.yaml` and should not be edited directly. PowerShell 7 on Windows, Linux, or macOS: @@ -124,10 +125,11 @@ Bash on Linux or macOS: bash ./docker/generate-rider-compose.sh ``` -Both scripts write to a temporary file and replace `compose.rider.yaml` only -after `docker compose config` succeeds. In Rider, use `compose.rider.yaml` as -the sole Compose file and add the appropriate command as an External Tool in -the run configuration's **Before launch** tasks. +Both scripts write to a temporary file and replace +`compose.dev.generated.yaml` only after `docker compose config` succeeds. In +Rider, use `compose.dev.generated.yaml` as the sole Compose file and add the +appropriate generator as an External Tool in the run configuration's +**Before launch** tasks. ## Configuration From 31d34812cbd947ff282248f3bf595cd6a7160375 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:58:23 -0400 Subject: [PATCH 29/59] Remove generated development compose workaround --- compose.dev.generated.yaml | 73 -------------------------------------- 1 file changed, 73 deletions(-) delete mode 100644 compose.dev.generated.yaml diff --git a/compose.dev.generated.yaml b/compose.dev.generated.yaml deleted file mode 100644 index d8cbdd9..0000000 --- a/compose.dev.generated.yaml +++ /dev/null @@ -1,73 +0,0 @@ -# Generated single-file development configuration. -# Regenerate from compose.yaml + compose.dev.yaml; do not edit this file directly. -name: truthgate - -services: - truthgate: - image: truthgate-ipfs:dev - build: - context: . - dockerfile: Dockerfile - target: development - args: - DOTNET_VERSION: ${DOTNET_VERSION:-10.0} - KUBO_VERSION: ${KUBO_VERSION:-v0.42.0} - TRUTHGATE_UID: ${TRUTHGATE_UID:-1000} - TRUTHGATE_GID: ${TRUTHGATE_GID:-1000} - container_name: ${TRUTHGATE_CONTAINER_NAME:-truthgate} - command: ["development"] - restart: "no" - read_only: false - working_dir: /workspace - security_opt: - - no-new-privileges:true - sysctls: - net.ipv4.ip_unprivileged_port_start: "0" - environment: - ASPNETCORE_ENVIRONMENT: Development - DOTNET_ENVIRONMENT: Development - ASPNETCORE_URLS: http://0.0.0.0:80 - DOTNET_USE_POLLING_FILE_WATCHER: "1" - IPFS_PATH: /data/ipfs/repo - TMPDIR: /run/truthgate - TRUTHGATE_ACME_STAGING: ${TRUTHGATE_ACME_STAGING:-false} - TRUTHGATE_CERT_IPS: ${TRUTHGATE_CERT_IPS:-} - TRUTHGATE_CERT_PATH: /data/truthgate/certificates - TRUTHGATE_CONFIG_PATH: /data/truthgate/config/config.json - TRUTHGATE_DATABASE_PATH: /data/truthgate/database - TRUTHGATE_STATE_PATH: /data/truthgate/state - TRUTHGATE_DEV_PROJECT: /workspace/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj - TRUTHGATE_HEALTH_URL: http://127.0.0.1:80/ - ports: - - "${TRUTHGATE_DEV_HTTP_PORT:-8080}:80/tcp" - - "${IPFS_SWARM_PORT:-4001}:4001/tcp" - - "${IPFS_SWARM_PORT:-4001}:4001/udp" - volumes: - - type: bind - source: . - target: /workspace - - type: volume - source: truthgate_nuget - target: /home/truthgate/.nuget/packages - - type: bind - source: ${TRUTHGATE_DATA_HOST_PATH:-./data/truthgate} - target: /data/truthgate - - type: bind - source: ${IPFS_REPO_HOST_PATH:-./data/ipfs/repo} - target: /data/ipfs/repo - - type: bind - source: ${IPFS_BLOCKS_HOST_PATH:-./data/ipfs/blocks} - target: /data/ipfs/repo/blocks - tmpfs: - - /tmp:rw,noexec,nosuid,size=256m - - /run/truthgate:rw,noexec,nosuid,size=64m - healthcheck: - test: ["CMD", "/usr/local/bin/truthgate-healthcheck"] - interval: 30s - timeout: 10s - start_period: 90s - retries: 3 - stop_grace_period: 45s - -volumes: - truthgate_nuget: From 5ee2482606ee28cc4aedd64667298e611576d8c5 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:58:28 -0400 Subject: [PATCH 30/59] Remove obsolete Rider compose generator --- docker/generate-rider-compose.ps1 | 32 ------------------------------- 1 file changed, 32 deletions(-) delete mode 100644 docker/generate-rider-compose.ps1 diff --git a/docker/generate-rider-compose.ps1 b/docker/generate-rider-compose.ps1 deleted file mode 100644 index f984077..0000000 --- a/docker/generate-rider-compose.ps1 +++ /dev/null @@ -1,32 +0,0 @@ -$ErrorActionPreference = 'Stop' -Set-StrictMode -Version Latest - -$repositoryRoot = Split-Path -Parent $PSScriptRoot -$composeFile = Join-Path $repositoryRoot 'compose.yaml' -$devComposeFile = Join-Path $repositoryRoot 'compose.dev.yaml' -$outputFile = Join-Path $repositoryRoot 'compose.dev.generated.yaml' -$tempFile = Join-Path $repositoryRoot ('.compose.dev.generated.{0}.tmp' -f [Guid]::NewGuid().ToString('N')) - -try { - Push-Location $repositoryRoot - - & docker compose -f $composeFile -f $devComposeFile config | Set-Content -Path $tempFile -Encoding utf8 - - if ($LASTEXITCODE -ne 0) { - throw "docker compose config failed with exit code $LASTEXITCODE." - } - - if (-not (Test-Path $tempFile) -or (Get-Item $tempFile).Length -eq 0) { - throw 'Generated Compose file is empty.' - } - - Move-Item -Path $tempFile -Destination $outputFile -Force - Write-Host "Generated $outputFile" -} -finally { - if (Test-Path $tempFile) { - Remove-Item $tempFile -Force - } - - Pop-Location -} From b51a6f42521d89ce8a7a40a69201ce539e086117 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:58:35 -0400 Subject: [PATCH 31/59] Remove obsolete Rider compose generator --- docker/generate-rider-compose.sh | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 docker/generate-rider-compose.sh diff --git a/docker/generate-rider-compose.sh b/docker/generate-rider-compose.sh deleted file mode 100644 index 5120707..0000000 --- a/docker/generate-rider-compose.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -set -Eeuo pipefail - -repository_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -compose_file="${repository_root}/compose.yaml" -dev_compose_file="${repository_root}/compose.dev.yaml" -output_file="${repository_root}/compose.dev.generated.yaml" -temp_file="$(mktemp "${repository_root}/.compose.dev.generated.XXXXXX.tmp")" - -cleanup() { - rm -f "${temp_file}" -} -trap cleanup EXIT - -cd "${repository_root}" - -docker compose \ - -f "${compose_file}" \ - -f "${dev_compose_file}" \ - config > "${temp_file}" - -if [[ ! -s "${temp_file}" ]]; then - echo "Generated Compose file is empty." >&2 - exit 1 -fi - -mv -f "${temp_file}" "${output_file}" -trap - EXIT - -echo "Generated ${output_file}" From 461dbda00de73aeea86b4af3e7b728e89faef7b5 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:58:50 -0400 Subject: [PATCH 32/59] Remove obsolete generated compose documentation --- DOCKER.md | 31 +++---------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/DOCKER.md b/DOCKER.md index 2ea48f1..7a26849 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -102,34 +102,9 @@ Kubo initialization, repository migrations, persistent paths, startup order, and process supervision are shared with production. This prevents development from silently using a different node layout. -Rider can attach to the running development container or use the container's -SDK. The source directory is mounted directly, so normal edits trigger -`dotnet watch`. - -### Generated single-file development configuration - -Some tools and IDEs fail while discovering services from multiple Compose -files. `compose.dev.generated.yaml` is the fully merged development -configuration for that case. It is generated from `compose.yaml` and -`compose.dev.yaml` and should not be edited directly. - -PowerShell 7 on Windows, Linux, or macOS: - -```powershell -pwsh -NoProfile -File ./docker/generate-rider-compose.ps1 -``` - -Bash on Linux or macOS: - -```bash -bash ./docker/generate-rider-compose.sh -``` - -Both scripts write to a temporary file and replace -`compose.dev.generated.yaml` only after `docker compose config` succeeds. In -Rider, use `compose.dev.generated.yaml` as the sole Compose file and add the -appropriate generator as an External Tool in the run configuration's -**Before launch** tasks. +Rider can use `compose.yaml` followed by `compose.dev.yaml` directly in a Docker +Compose run configuration. The source directory is mounted directly, so normal +edits trigger `dotnet watch`. ## Configuration From 2ebef1a6453def1f15ffcc672dac6b1524fbd85d Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:01:12 -0400 Subject: [PATCH 33/59] Fix Rider development container runtime paths --- compose.dev.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/compose.dev.yaml b/compose.dev.yaml index b1a9952..c540ebb 100644 --- a/compose.dev.yaml +++ b/compose.dev.yaml @@ -12,6 +12,11 @@ services: DOTNET_ENVIRONMENT: Development ASPNETCORE_URLS: http://0.0.0.0:80 DOTNET_USE_POLLING_FILE_WATCHER: "1" + DOTNET_CLI_HOME: /home/truthgate + HOME: /home/truthgate + NUGET_PACKAGES: /home/truthgate/.nuget/packages + NUGET_HTTP_CACHE_PATH: /home/truthgate/.nuget/http-cache + TINI_SUBREAPER: "1" TRUTHGATE_DEV_PROJECT: /workspace/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj TRUTHGATE_HEALTH_URL: http://127.0.0.1:80/ ports: !override @@ -25,6 +30,13 @@ services: - type: volume source: truthgate_nuget target: /home/truthgate/.nuget/packages + - type: volume + source: truthgate_nuget_http + target: /home/truthgate/.nuget/http-cache + tmpfs: !override + - /tmp:rw,noexec,nosuid,size=1g + - /run/truthgate:rw,noexec,nosuid,size=64m volumes: truthgate_nuget: + truthgate_nuget_http: From a93a8c40a84a3d12becfe48115c7b318629bc2e5 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:03:07 -0400 Subject: [PATCH 34/59] Fix development NuGet directory ownership --- docker/entrypoint.sh | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 6e53846..e68ed61 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -55,6 +55,35 @@ install -d -m 0750 -o "${truthgate_user}" -g "${truthgate_group}" \ "${blocks_directory}" \ "${TMPDIR}" +# Named volumes are created as root-owned directories. Development tools such as +# NuGet run as the non-root truthgate user, so initialize the complete NuGet home +# (including its configuration directory) before dropping privileges. +if [[ "${mode}" == "development" ]]; then + : "${HOME:=/home/truthgate}" + : "${DOTNET_CLI_HOME:=${HOME}}" + : "${NUGET_PACKAGES:=${HOME}/.nuget/packages}" + : "${NUGET_HTTP_CACHE_PATH:=${HOME}/.nuget/http-cache}" + + export HOME DOTNET_CLI_HOME NUGET_PACKAGES NUGET_HTTP_CACHE_PATH + + nuget_root="${HOME}/.nuget" + nuget_config_directory="${nuget_root}/NuGet" + + install -d -m 0750 -o "${truthgate_user}" -g "${truthgate_group}" \ + "${HOME}" \ + "${nuget_root}" \ + "${nuget_config_directory}" \ + "${NUGET_PACKAGES}" \ + "${NUGET_HTTP_CACHE_PATH}" + + chown "${truthgate_user}:${truthgate_group}" \ + "${HOME}" \ + "${nuget_root}" \ + "${nuget_config_directory}" \ + "${NUGET_PACKAGES}" \ + "${NUGET_HTTP_CACHE_PATH}" +fi + # The default Compose layout mounts the repo and blockstore separately. Changing # ownership on the mount roots is cheap and avoids recursively walking a large # existing blockstore on every container start. From e9ac0a2b8ec5ef3fa69476dc8dc11b94a5e31703 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:37:59 -0400 Subject: [PATCH 35/59] Add Kubo diagnostics command --- docker/kubo-status.sh | 57 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docker/kubo-status.sh diff --git a/docker/kubo-status.sh b/docker/kubo-status.sh new file mode 100644 index 0000000..ded9810 --- /dev/null +++ b/docker/kubo-status.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +: "${IPFS_PATH:=/data/ipfs/repo}" +api_address="/ip4/127.0.0.1/tcp/5001" + +config_value() { + ipfs config "$1" 2>/dev/null || printf '' +} + +config_json() { + ipfs config "$1" --json 2>/dev/null | jq -c . || printf '' +} + +printf 'TruthGate Kubo status\n' +printf '=====================\n' +printf 'Repository: %s\n' "${IPFS_PATH}" +printf 'Kubo version: %s\n' "$(ipfs version --number 2>/dev/null || printf '')" +printf 'Routing.Type: %s\n' "$(config_value Routing.Type)" +printf 'Addresses.API: %s\n' "$(config_value Addresses.API)" +printf 'Addresses.Gateway: %s\n' "$(config_value Addresses.Gateway)" +printf 'Addresses.Swarm: %s\n' "$(config_json Addresses.Swarm)" +printf 'Addresses.AppendAnnounce: %s\n' "$(config_json Addresses.AppendAnnounce)" +printf 'Swarm.EnableHolePunching: %s\n' "$(config_value Swarm.EnableHolePunching)" +printf 'Swarm.RelayClient.Enabled: %s\n' "$(config_value Swarm.RelayClient.Enabled)" +printf 'Provide.Enabled: %s\n' "$(config_value Provide.Enabled)" +printf 'Provide.Strategy: %s\n' "$(config_value Provide.Strategy)" +printf 'Provide.DHT.Interval: %s\n' "$(config_value Provide.DHT.Interval)" +printf 'Provide.DHT.SweepEnabled: %s\n' "$(config_value Provide.DHT.SweepEnabled)" +printf 'Provide.DHT.ResumeEnabled: %s\n' "$(config_value Provide.DHT.ResumeEnabled)" +printf 'Datastore.StorageMax: %s\n' "$(config_value Datastore.StorageMax)" +printf 'Datastore.StorageGCWatermark: %s%%\n' "$(config_value Datastore.StorageGCWatermark)" + +if ! ipfs --api="${api_address}" id >/dev/null 2>&1; then + printf '\nDaemon: unavailable at %s\n' "${api_address}" + exit 1 +fi + +peer_id="$(ipfs --api="${api_address}" id -f='')" +peer_count="$(ipfs --api="${api_address}" swarm peers 2>/dev/null | awk 'NF { count++ } END { print count + 0 }')" + +printf '\nDaemon: ready\n' +printf 'Peer ID: %s\n' "${peer_id}" +printf 'Connected swarm peers: %s\n' "${peer_count}" +printf 'Active listen addresses:\n' +ipfs --api="${api_address}" swarm addrs listen 2>/dev/null | sed 's/^/ /' || true + +printf '\nRepository statistics:\n' +ipfs --api="${api_address}" repo stat --human 2>/dev/null | sed 's/^/ /' || true + +printf '\nAutoNAT observations:\n' +ipfs --api="${api_address}" swarm addrs autonat 2>/dev/null | sed 's/^/ /' || \ + printf ' unavailable on this Kubo build\n' + +printf '\nProvider statistics:\n' +ipfs --api="${api_address}" provide stat 2>/dev/null | sed 's/^/ /' || \ + printf ' unavailable or provider is still initializing\n' From 1dc2eb1285445c3148c47ca01b5ea7ef491bd8d6 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:38:16 -0400 Subject: [PATCH 36/59] Install Kubo configuration tooling --- Dockerfile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Dockerfile b/Dockerfile index b446b17..a0e54a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,6 +35,7 @@ RUN apt-get update \ ca-certificates \ curl \ gosu \ + jq \ tini \ && rm -rf /var/lib/apt/lists/* \ && if ! getent group "${TRUTHGATE_GID}" >/dev/null; then groupadd --gid "${TRUTHGATE_GID}" truthgate; fi \ @@ -49,6 +50,7 @@ RUN apt-get update \ COPY --from=kubo /usr/local/bin/ipfs /usr/local/bin/ipfs COPY --chmod=0755 docker/entrypoint.sh /usr/local/bin/truthgate-entrypoint COPY --chmod=0755 docker/healthcheck.sh /usr/local/bin/truthgate-healthcheck +COPY --chmod=0755 docker/kubo-status.sh /usr/local/bin/truthgate-kubo-status ENV ASPNETCORE_ENVIRONMENT=Production \ DOTNET_ENVIRONMENT=Production \ @@ -60,6 +62,8 @@ ENV ASPNETCORE_ENVIRONMENT=Production \ TRUTHGATE_CERT_PATH=/data/truthgate/certificates \ TRUTHGATE_CONFIG_PATH=/data/truthgate/config/config.json \ TRUTHGATE_DATABASE_PATH=/data/truthgate/database \ + TRUTHGATE_KUBO_OVERRIDES_PATH=/data/truthgate/config/kubo-overrides.json \ + TRUTHGATE_KUBO_SETTINGS_PATH=/data/truthgate/config/kubo-settings.json \ TRUTHGATE_STATE_PATH=/data/truthgate/state \ TRUTHGATE_HEALTH_URL=https://127.0.0.1:443/ @@ -84,6 +88,7 @@ RUN apt-get update \ curl \ git \ gosu \ + jq \ tini \ && rm -rf /var/lib/apt/lists/* \ && if ! getent group "${TRUTHGATE_GID}" >/dev/null; then groupadd --gid "${TRUTHGATE_GID}" truthgate; fi \ @@ -99,6 +104,7 @@ RUN apt-get update \ COPY --from=kubo /usr/local/bin/ipfs /usr/local/bin/ipfs COPY --chmod=0755 docker/entrypoint.sh /usr/local/bin/truthgate-entrypoint COPY --chmod=0755 docker/healthcheck.sh /usr/local/bin/truthgate-healthcheck +COPY --chmod=0755 docker/kubo-status.sh /usr/local/bin/truthgate-kubo-status ENV ASPNETCORE_ENVIRONMENT=Development \ DOTNET_ENVIRONMENT=Development \ @@ -112,6 +118,8 @@ ENV ASPNETCORE_ENVIRONMENT=Development \ TRUTHGATE_CONFIG_PATH=/data/truthgate/config/config.json \ TRUTHGATE_DATABASE_PATH=/data/truthgate/database \ TRUTHGATE_DEV_PROJECT=/workspace/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj \ + TRUTHGATE_KUBO_OVERRIDES_PATH=/data/truthgate/config/kubo-overrides.json \ + TRUTHGATE_KUBO_SETTINGS_PATH=/data/truthgate/config/kubo-settings.json \ TRUTHGATE_STATE_PATH=/data/truthgate/state \ TRUTHGATE_HEALTH_URL=http://127.0.0.1:80/ From b55b0d96476ac44be02a0f3518c890e1185101e6 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:38:31 -0400 Subject: [PATCH 37/59] Expose managed Kubo overrides --- compose.yaml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/compose.yaml b/compose.yaml index dfee427..d01c7f3 100644 --- a/compose.yaml +++ b/compose.yaml @@ -23,6 +23,7 @@ services: ASPNETCORE_ENVIRONMENT: Production DOTNET_ENVIRONMENT: Production IPFS_PATH: /data/ipfs/repo + IPFS_SWARM_PORT: ${IPFS_SWARM_PORT:-4001} TMPDIR: /run/truthgate TRUTHGATE_ACME_STAGING: ${TRUTHGATE_ACME_STAGING:-false} TRUTHGATE_CERT_IPS: ${TRUTHGATE_CERT_IPS:-} @@ -31,6 +32,26 @@ services: TRUTHGATE_DATABASE_PATH: /data/truthgate/database TRUTHGATE_STATE_PATH: /data/truthgate/state TRUTHGATE_HEALTH_URL: https://127.0.0.1:443/ + TRUTHGATE_KUBO_SETTINGS_PATH: /data/truthgate/config/kubo-settings.json + TRUTHGATE_KUBO_OVERRIDES_PATH: /data/truthgate/config/kubo-overrides.json + TRUTHGATE_KUBO_APPLY_SERVER_PROFILE: "${TRUTHGATE_KUBO_APPLY_SERVER_PROFILE:-}" + TRUTHGATE_KUBO_ENSURE_SWARM_LISTENERS: "${TRUTHGATE_KUBO_ENSURE_SWARM_LISTENERS:-}" + TRUTHGATE_KUBO_ROUTING_TYPE: "${TRUTHGATE_KUBO_ROUTING_TYPE:-}" + TRUTHGATE_KUBO_HOLE_PUNCHING: "${TRUTHGATE_KUBO_HOLE_PUNCHING:-}" + TRUTHGATE_KUBO_RELAY_CLIENT: "${TRUTHGATE_KUBO_RELAY_CLIENT:-}" + TRUTHGATE_KUBO_PROVIDE_ENABLED: "${TRUTHGATE_KUBO_PROVIDE_ENABLED:-}" + TRUTHGATE_KUBO_PROVIDE_STRATEGY: "${TRUTHGATE_KUBO_PROVIDE_STRATEGY:-}" + TRUTHGATE_KUBO_PROVIDE_INTERVAL: "${TRUTHGATE_KUBO_PROVIDE_INTERVAL:-}" + TRUTHGATE_KUBO_PROVIDE_SWEEP: "${TRUTHGATE_KUBO_PROVIDE_SWEEP:-}" + TRUTHGATE_KUBO_PROVIDE_RESUME: "${TRUTHGATE_KUBO_PROVIDE_RESUME:-}" + TRUTHGATE_KUBO_STORAGE_MAX: "${TRUTHGATE_KUBO_STORAGE_MAX:-}" + TRUTHGATE_KUBO_STORAGE_PERCENT: "${TRUTHGATE_KUBO_STORAGE_PERCENT:-}" + TRUTHGATE_KUBO_STORAGE_FALLBACK: "${TRUTHGATE_KUBO_STORAGE_FALLBACK:-}" + TRUTHGATE_KUBO_STORAGE_GC_WATERMARK: "${TRUTHGATE_KUBO_STORAGE_GC_WATERMARK:-}" + TRUTHGATE_KUBO_ENABLE_GC: "${TRUTHGATE_KUBO_ENABLE_GC:-}" + TRUTHGATE_KUBO_PUBLIC_IPV4: "${TRUTHGATE_KUBO_PUBLIC_IPV4:-}" + TRUTHGATE_KUBO_PUBLIC_IPV6: "${TRUTHGATE_KUBO_PUBLIC_IPV6:-}" + TRUTHGATE_KUBO_ANNOUNCE_PORT: "${TRUTHGATE_KUBO_ANNOUNCE_PORT:-}" ports: - "${TRUTHGATE_HTTP_PORT:-80}:80/tcp" - "${TRUTHGATE_HTTPS_PORT:-443}:443/tcp" From 280002d3771fc46619d280342586a0845c543706 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:38:41 -0400 Subject: [PATCH 38/59] Document Kubo server overrides --- .env.example | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.env.example b/.env.example index 659b47c..64c411a 100644 --- a/.env.example +++ b/.env.example @@ -23,3 +23,33 @@ IPFS_BLOCKS_HOST_PATH=./data/ipfs/blocks TRUTHGATE_ACME_STAGING=false # Optional comma-separated IP addresses for the self-signed fallback cert. TRUTHGATE_CERT_IPS= + +# TruthGate creates data/truthgate/config/kubo-settings.json on first boot. +# That persistent file contains the normal server defaults and is the future +# control-plane contract for the TruthGate UI. Uncomment any value below to +# override the corresponding persistent setting from Docker/Compose. +# +# TRUTHGATE_KUBO_APPLY_SERVER_PROFILE=true +# TRUTHGATE_KUBO_ENSURE_SWARM_LISTENERS=true +# TRUTHGATE_KUBO_ROUTING_TYPE=dhtserver +# TRUTHGATE_KUBO_HOLE_PUNCHING=true +# TRUTHGATE_KUBO_RELAY_CLIENT=true +# TRUTHGATE_KUBO_PROVIDE_ENABLED=true +# TRUTHGATE_KUBO_PROVIDE_STRATEGY=all +# TRUTHGATE_KUBO_PROVIDE_INTERVAL=22h +# TRUTHGATE_KUBO_PROVIDE_SWEEP=true +# TRUTHGATE_KUBO_PROVIDE_RESUME=true +# +# Storage can be "auto" or any Kubo size such as 750GB, 2TB, or 5TiB. +# Auto uses the configured percentage of the filesystem containing the blockstore. +# TRUTHGATE_KUBO_STORAGE_MAX=auto +# TRUTHGATE_KUBO_STORAGE_PERCENT=90 +# TRUTHGATE_KUBO_STORAGE_FALLBACK=200GB +# TRUTHGATE_KUBO_STORAGE_GC_WATERMARK=90 +# TRUTHGATE_KUBO_ENABLE_GC=true +# +# Public addresses can be "auto", "off", or a literal address. The announce +# port defaults to IPFS_SWARM_PORT so non-default Docker host mappings work. +# TRUTHGATE_KUBO_PUBLIC_IPV4=auto +# TRUTHGATE_KUBO_PUBLIC_IPV6=auto +# TRUTHGATE_KUBO_ANNOUNCE_PORT=4001 From 72f79023b5c2dfd2548a092286b883dcd95675b4 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:40:40 -0400 Subject: [PATCH 39/59] Configure Kubo as a public server --- docker/kubo-configure.sh | 323 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 docker/kubo-configure.sh diff --git a/docker/kubo-configure.sh b/docker/kubo-configure.sh new file mode 100644 index 0000000..d245bf3 --- /dev/null +++ b/docker/kubo-configure.sh @@ -0,0 +1,323 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +truthgate_user="truthgate" +truthgate_group="$(id -g "${truthgate_user}")" + +log() { printf '[truthgate] %s\n' "$*"; } +warn() { printf '[truthgate] WARNING: %s\n' "$*" >&2; } +fail() { printf '[truthgate] ERROR: %s\n' "$*" >&2; exit 1; } +as_truthgate() { gosu "${truthgate_user}:${truthgate_group}" "$@"; } + +normalize_bool() { + case "${1,,}" in + 1|true|yes|on) printf 'true' ;; + 0|false|no|off) printf 'false' ;; + *) return 1 ;; + esac +} + +json_setting() { + local filter="$1" fallback="$2" value + value="$(jq -er "${filter} // empty" "${TRUTHGATE_KUBO_SETTINGS_PATH}" 2>/dev/null || true)" + [[ -n "${value}" ]] && printf '%s' "${value}" || printf '%s' "${fallback}" +} + +setting() { + local environment_name="$1" filter="$2" fallback="$3" + local environment_value="${!environment_name:-}" + [[ -n "${environment_value}" ]] \ + && printf '%s' "${environment_value}" \ + || json_setting "${filter}" "${fallback}" +} + +set_string() { + as_truthgate ipfs config "$1" --json "$(jq -cn --arg value "$2" '$value')" +} + +set_json() { + as_truthgate ipfs config "$1" --json "$2" +} + +ensure_array_value() { + local key="$1" item="$2" current compact updated + current="$(as_truthgate ipfs config "${key}" --json 2>/dev/null || printf '[]')" + compact="$(jq -c . <<<"${current}")" + updated="$(jq -c --arg item "${item}" \ + 'if type != "array" then [$item] elif index($item) then . else . + [$item] end' \ + <<<"${current}")" + [[ "${updated}" == "${compact}" ]] || set_json "${key}" "${updated}" +} + +valid_ipv4() { + local a b c d extra octet + IFS=. read -r a b c d extra <<<"$1" + [[ -z "${extra:-}" && -n "${a:-}" && -n "${b:-}" && -n "${c:-}" && -n "${d:-}" ]] || return 1 + for octet in "${a}" "${b}" "${c}" "${d}"; do + [[ "${octet}" =~ ^[0-9]{1,3}$ ]] || return 1 + (( 10#${octet} <= 255 )) || return 1 + done +} + +valid_ipv6() { + [[ "$1" == *:* && "$1" =~ ^[0-9A-Fa-f:.]+$ ]] +} + +resolve_public_address() { + local family="$1" requested="$2" address="" + case "${requested,,}" in + ""|off|none|disabled|false) return 0 ;; + auto) + if [[ "${family}" == "4" ]]; then + address="$(curl -4fsS --max-time 5 https://api.ipify.org 2>/dev/null || true)" + else + address="$(curl -6fsS --max-time 5 https://api6.ipify.org 2>/dev/null || true)" + fi + address="${address//$'\r'/}" + address="${address//$'\n'/}" + ;; + *) address="${requested}" ;; + esac + + if [[ -z "${address}" ]]; then + warn "Could not detect a public IPv${family} address; no managed IPv${family} announce address will be added." + return 0 + fi + + if [[ "${family}" == "4" ]]; then + valid_ipv4 "${address}" || fail "Invalid public IPv4 address: '${address}'." + else + valid_ipv6 "${address}" || fail "Invalid public IPv6 address: '${address}'." + fi + printf '%s' "${address}" +} + +human_bytes() { + command -v numfmt >/dev/null 2>&1 \ + && numfmt --to=iec-i --suffix=B "$1" \ + || printf '%sB' "$1" +} + +: "${IPFS_PATH:=/data/ipfs/repo}" +: "${TRUTHGATE_CONFIG_PATH:=/data/truthgate/config/config.json}" +: "${TRUTHGATE_STATE_PATH:=/data/truthgate/state}" +: "${TMPDIR:=/run/truthgate}" + +config_directory="$(dirname "${TRUTHGATE_CONFIG_PATH}")" +blocks_directory="${IPFS_PATH}/blocks" +: "${TRUTHGATE_KUBO_SETTINGS_PATH:=${config_directory}/kubo-settings.json}" +: "${TRUTHGATE_KUBO_OVERRIDES_PATH:=${config_directory}/kubo-overrides.json}" +export IPFS_PATH TRUTHGATE_KUBO_SETTINGS_PATH TRUTHGATE_KUBO_OVERRIDES_PATH + +if [[ ! -e "${TRUTHGATE_KUBO_SETTINGS_PATH}" ]]; then + cat >"${TRUTHGATE_KUBO_SETTINGS_PATH}" <<'JSON' +{ + "serverProfile": true, + "routingType": "dhtserver", + "ensureSwarmListeners": true, + "holePunching": true, + "relayClient": true, + "provide": { + "enabled": true, + "strategy": "all", + "interval": "22h", + "sweepEnabled": true, + "resumeEnabled": true + }, + "storage": { + "max": "auto", + "percent": 90, + "fallback": "200GB", + "gcWatermark": 90, + "enableGc": true + }, + "publicAnnounce": { + "ipv4": "auto", + "ipv6": "auto", + "port": null + } +} +JSON + chown "${truthgate_user}:${truthgate_group}" "${TRUTHGATE_KUBO_SETTINGS_PATH}" + chmod 0640 "${TRUTHGATE_KUBO_SETTINGS_PATH}" +fi +jq -e 'type == "object"' "${TRUTHGATE_KUBO_SETTINGS_PATH}" >/dev/null \ + || fail "Kubo settings file is not a valid JSON object: ${TRUTHGATE_KUBO_SETTINGS_PATH}" + +if [[ ! -e "${TRUTHGATE_KUBO_OVERRIDES_PATH}" ]]; then + printf '{}\n' >"${TRUTHGATE_KUBO_OVERRIDES_PATH}" + chown "${truthgate_user}:${truthgate_group}" "${TRUTHGATE_KUBO_OVERRIDES_PATH}" + chmod 0640 "${TRUTHGATE_KUBO_OVERRIDES_PATH}" +fi +jq -e 'type == "object"' "${TRUTHGATE_KUBO_OVERRIDES_PATH}" >/dev/null \ + || fail "Kubo overrides file is not a valid JSON object: ${TRUTHGATE_KUBO_OVERRIDES_PATH}" + +server_profile_raw="$(setting TRUTHGATE_KUBO_APPLY_SERVER_PROFILE '.serverProfile' true)" +server_profile="$(normalize_bool "${server_profile_raw}")" \ + || fail "TRUTHGATE_KUBO_APPLY_SERVER_PROFILE must be true or false." +server_profile_marker="${TRUTHGATE_STATE_PATH}/kubo-server-profile-v1" + +if [[ ! -s "${IPFS_PATH}/config" ]]; then + if [[ "${server_profile}" == "true" ]]; then + log "Initializing a new Kubo repository with the public-server profile." + as_truthgate ipfs init --profile server + printf 'applied during ipfs init\n' >"${server_profile_marker}" + chown "${truthgate_user}:${truthgate_group}" "${server_profile_marker}" + else + log "Initializing a new Kubo repository without the server profile." + as_truthgate ipfs init + fi +else + log "Using the existing Kubo repository at ${IPFS_PATH}." + if [[ "${server_profile}" == "true" && ! -e "${server_profile_marker}" ]]; then + log "Applying Kubo's public-server profile to the existing repository." + as_truthgate ipfs config profile apply server + printf 'applied to existing repository\n' >"${server_profile_marker}" + chown "${truthgate_user}:${truthgate_group}" "${server_profile_marker}" + fi +fi + +set_string Routing.Type "$(setting TRUTHGATE_KUBO_ROUTING_TYPE '.routingType' dhtserver)" + +ensure_swarm_raw="$(setting TRUTHGATE_KUBO_ENSURE_SWARM_LISTENERS '.ensureSwarmListeners' true)" +ensure_swarm="$(normalize_bool "${ensure_swarm_raw}")" \ + || fail "TRUTHGATE_KUBO_ENSURE_SWARM_LISTENERS must be true or false." +if [[ "${ensure_swarm}" == "true" ]]; then + for address in \ + /ip4/0.0.0.0/tcp/4001 \ + /ip6/::/tcp/4001 \ + /ip4/0.0.0.0/udp/4001/quic-v1 \ + /ip4/0.0.0.0/udp/4001/quic-v1/webtransport \ + /ip6/::/udp/4001/quic-v1 \ + /ip6/::/udp/4001/quic-v1/webtransport + do + ensure_array_value Addresses.Swarm "${address}" + done +fi + +for spec in \ + "TRUTHGATE_KUBO_HOLE_PUNCHING|.holePunching|true|Swarm.EnableHolePunching" \ + "TRUTHGATE_KUBO_RELAY_CLIENT|.relayClient|true|Swarm.RelayClient.Enabled" \ + "TRUTHGATE_KUBO_PROVIDE_ENABLED|.provide.enabled|true|Provide.Enabled" \ + "TRUTHGATE_KUBO_PROVIDE_SWEEP|.provide.sweepEnabled|true|Provide.DHT.SweepEnabled" \ + "TRUTHGATE_KUBO_PROVIDE_RESUME|.provide.resumeEnabled|true|Provide.DHT.ResumeEnabled" +do + IFS='|' read -r env_name filter fallback key <<<"${spec}" + raw="$(setting "${env_name}" "${filter}" "${fallback}")" + value="$(normalize_bool "${raw}")" || fail "${env_name} must be true or false." + set_json "${key}" "${value}" +done + +set_string Provide.Strategy "$(setting TRUTHGATE_KUBO_PROVIDE_STRATEGY '.provide.strategy' all)" +set_string Provide.DHT.Interval "$(setting TRUTHGATE_KUBO_PROVIDE_INTERVAL '.provide.interval' 22h)" + +storage_max_requested="$(setting TRUTHGATE_KUBO_STORAGE_MAX '.storage.max' auto)" +storage_percent="$(setting TRUTHGATE_KUBO_STORAGE_PERCENT '.storage.percent' 90)" +storage_fallback="$(setting TRUTHGATE_KUBO_STORAGE_FALLBACK '.storage.fallback' 200GB)" +storage_gc_watermark="$(setting TRUTHGATE_KUBO_STORAGE_GC_WATERMARK '.storage.gcWatermark' 90)" + +[[ "${storage_percent}" =~ ^[0-9]+$ ]] && (( storage_percent >= 1 && storage_percent <= 100 )) \ + || fail "TRUTHGATE_KUBO_STORAGE_PERCENT must be an integer from 1 through 100." +[[ "${storage_gc_watermark}" =~ ^[0-9]+$ ]] && (( storage_gc_watermark <= 100 )) \ + || fail "TRUTHGATE_KUBO_STORAGE_GC_WATERMARK must be an integer from 0 through 100." + +storage_source="fixed" +if [[ "${storage_max_requested,,}" == "auto" ]]; then + storage_total_bytes="$(df -PB1 -- "${blocks_directory}" 2>/dev/null | awk 'NR == 2 { print $2 }' || true)" + if [[ "${storage_total_bytes}" =~ ^[0-9]+$ && "${storage_total_bytes}" -gt 0 ]]; then + storage_max_effective="$(( storage_total_bytes * storage_percent / 100 ))B" + storage_source="auto (${storage_percent}% of $(human_bytes "${storage_total_bytes}"))" + else + storage_max_effective="${storage_fallback}" + storage_source="fallback because filesystem capacity detection failed" + fi +else + storage_max_effective="${storage_max_requested}" +fi +set_string Datastore.StorageMax "${storage_max_effective}" +set_json Datastore.StorageGCWatermark "${storage_gc_watermark}" + +enable_gc_raw="$(setting TRUTHGATE_KUBO_ENABLE_GC '.storage.enableGc' true)" +enable_gc="$(normalize_bool "${enable_gc_raw}")" \ + || fail "TRUTHGATE_KUBO_ENABLE_GC must be true or false." +printf '%s\n' "${enable_gc}" >"${TMPDIR}/kubo-enable-gc" + +announce_port="${TRUTHGATE_KUBO_ANNOUNCE_PORT:-}" +[[ -n "${announce_port}" ]] || announce_port="$(json_setting '.publicAnnounce.port' '')" +[[ -n "${announce_port}" && "${announce_port}" != "null" ]] || announce_port="${IPFS_SWARM_PORT:-4001}" +[[ "${announce_port}" =~ ^[0-9]+$ ]] && (( announce_port >= 1 && announce_port <= 65535 )) \ + || fail "TRUTHGATE_KUBO_ANNOUNCE_PORT must be an integer from 1 through 65535." + +public_ipv4="$(resolve_public_address 4 "$(setting TRUTHGATE_KUBO_PUBLIC_IPV4 '.publicAnnounce.ipv4' auto)")" +public_ipv6="$(resolve_public_address 6 "$(setting TRUTHGATE_KUBO_PUBLIC_IPV6 '.publicAnnounce.ipv6' auto)")" + +managed_addresses=() +if [[ -n "${public_ipv4}" ]]; then + managed_addresses+=( + "/ip4/${public_ipv4}/tcp/${announce_port}" + "/ip4/${public_ipv4}/udp/${announce_port}/quic-v1" + "/ip4/${public_ipv4}/udp/${announce_port}/quic-v1/webtransport" + ) +fi +if [[ -n "${public_ipv6}" ]]; then + managed_addresses+=( + "/ip6/${public_ipv6}/tcp/${announce_port}" + "/ip6/${public_ipv6}/udp/${announce_port}/quic-v1" + "/ip6/${public_ipv6}/udp/${announce_port}/quic-v1/webtransport" + ) +fi + +managed_state="${TRUTHGATE_STATE_PATH}/kubo-managed-append-announce.json" +current="$(as_truthgate ipfs config Addresses.AppendAnnounce --json 2>/dev/null || printf '[]')" +previous='[]' +if [[ -s "${managed_state}" ]]; then + previous="$(cat "${managed_state}")" + jq -e 'type == "array"' <<<"${previous}" >/dev/null || previous='[]' +fi +without_previous="$(jq -c --argjson previous "${previous}" \ + '. as $current | [$current[] | select(. as $item | ($previous | index($item)) == null)]' \ + <<<"${current}")" +if (( ${#managed_addresses[@]} > 0 )); then + desired="$(printf '%s\n' "${managed_addresses[@]}" | jq -R . | jq -sc .)" +else + desired='[]' +fi +updated="$(jq -c --argjson desired "${desired}" \ + 'reduce $desired[] as $item (. ; if index($item) then . else . + [$item] end)' \ + <<<"${without_previous}")" +set_json Addresses.AppendAnnounce "${updated}" +printf '%s\n' "${desired}" >"${managed_state}" +chown "${truthgate_user}:${truthgate_group}" "${managed_state}" +chmod 0640 "${managed_state}" + +while IFS= read -r entry; do + key="$(jq -r '.key' <<<"${entry}")" + value="$(jq -c '.value' <<<"${entry}")" + case "${key}" in + Addresses.API|Addresses.Gateway) + warn "Ignoring protected Kubo override '${key}'; TruthGate requires loopback-only listeners." + ;; + *) + log "Applying advanced Kubo override: ${key}" + set_json "${key}" "${value}" + ;; + esac +done < <(jq -c 'to_entries[]' "${TRUTHGATE_KUBO_OVERRIDES_PATH}") + +set_string Addresses.API /ip4/127.0.0.1/tcp/5001 +set_string Addresses.Gateway /ip4/127.0.0.1/tcp/9010 + +log "Kubo server configuration:" +log " Routing.Type: $(as_truthgate ipfs config Routing.Type)" +log " Swarm listeners: $(as_truthgate ipfs config Addresses.Swarm --json | jq -c .)" +log " Managed public announce IPv4: ${public_ipv4:-disabled or unavailable}" +log " Managed public announce IPv6: ${public_ipv6:-disabled or unavailable}" +log " Public announce port: ${announce_port}" +log " Provide.Enabled: $(as_truthgate ipfs config Provide.Enabled)" +log " Provide.Strategy: $(as_truthgate ipfs config Provide.Strategy)" +log " Provide.DHT.Interval: $(as_truthgate ipfs config Provide.DHT.Interval)" +log " Provide.DHT.SweepEnabled: $(as_truthgate ipfs config Provide.DHT.SweepEnabled)" +log " Provide.DHT.ResumeEnabled: $(as_truthgate ipfs config Provide.DHT.ResumeEnabled)" +log " Datastore.StorageMax: $(as_truthgate ipfs config Datastore.StorageMax) (${storage_source})" +log " Datastore.StorageGCWatermark: $(as_truthgate ipfs config Datastore.StorageGCWatermark)%" +log " Automatic repository GC: ${enable_gc}" From 0c042d027c0317c0a8fcb0c915a1b83c7301fe3c Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:41:03 -0400 Subject: [PATCH 40/59] Wire Kubo server configuration into images --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index a0e54a3..7278b89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,6 +50,7 @@ RUN apt-get update \ COPY --from=kubo /usr/local/bin/ipfs /usr/local/bin/ipfs COPY --chmod=0755 docker/entrypoint.sh /usr/local/bin/truthgate-entrypoint COPY --chmod=0755 docker/healthcheck.sh /usr/local/bin/truthgate-healthcheck +COPY --chmod=0755 docker/kubo-configure.sh /usr/local/bin/truthgate-configure-kubo COPY --chmod=0755 docker/kubo-status.sh /usr/local/bin/truthgate-kubo-status ENV ASPNETCORE_ENVIRONMENT=Production \ @@ -104,6 +105,7 @@ RUN apt-get update \ COPY --from=kubo /usr/local/bin/ipfs /usr/local/bin/ipfs COPY --chmod=0755 docker/entrypoint.sh /usr/local/bin/truthgate-entrypoint COPY --chmod=0755 docker/healthcheck.sh /usr/local/bin/truthgate-healthcheck +COPY --chmod=0755 docker/kubo-configure.sh /usr/local/bin/truthgate-configure-kubo COPY --chmod=0755 docker/kubo-status.sh /usr/local/bin/truthgate-kubo-status ENV ASPNETCORE_ENVIRONMENT=Development \ From f51b4c8e4b9af014f95c5f74a69d483101c0cb58 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:41:22 -0400 Subject: [PATCH 41/59] Apply managed Kubo configuration at startup --- docker/entrypoint.sh | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index e68ed61..c4ace3f 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -45,6 +45,12 @@ config_directory="$(dirname "${TRUTHGATE_CONFIG_PATH}")" blocks_directory="${IPFS_PATH}/blocks" data_protection_directory="/data/truthgate/secrets/data-protection-keys" +: "${TRUTHGATE_KUBO_SETTINGS_PATH:=${config_directory}/kubo-settings.json}" +: "${TRUTHGATE_KUBO_OVERRIDES_PATH:=${config_directory}/kubo-overrides.json}" +require_absolute_path TRUTHGATE_KUBO_SETTINGS_PATH "${TRUTHGATE_KUBO_SETTINGS_PATH}" +require_absolute_path TRUTHGATE_KUBO_OVERRIDES_PATH "${TRUTHGATE_KUBO_OVERRIDES_PATH}" +export TRUTHGATE_KUBO_SETTINGS_PATH TRUTHGATE_KUBO_OVERRIDES_PATH + install -d -m 0750 -o "${truthgate_user}" -g "${truthgate_group}" \ "${config_directory}" \ "${TRUTHGATE_DATABASE_PATH}" \ @@ -106,7 +112,7 @@ if [[ ! -s "${TRUTHGATE_CONFIG_PATH}" ]]; then else bootstrap_password="$(od -An -N24 -tx1 /dev/urandom | tr -d ' \n')" umask 077 - printf '%s' "${bootstrap_password}" > "${bootstrap_password_file}" + printf '%s' "${bootstrap_password}" >"${bootstrap_password_file}" chown "${truthgate_user}:${truthgate_group}" "${bootstrap_password_file}" fi @@ -119,20 +125,15 @@ else unset TRUTHGATE_BOOTSTRAP_ADMIN_PASSWORD || true fi -if [[ ! -s "${IPFS_PATH}/config" ]]; then - log "Initializing a new Kubo repository at ${IPFS_PATH}." - as_truthgate ipfs init -else - log "Using the existing Kubo repository at ${IPFS_PATH}." -fi +/usr/local/bin/truthgate-configure-kubo -# TruthGate talks to Kubo over loopback. The RPC API is intentionally not -# published by Compose; the gateway is likewise kept private behind TruthGate. -as_truthgate ipfs config Addresses.API /ip4/127.0.0.1/tcp/5001 -as_truthgate ipfs config Addresses.Gateway /ip4/127.0.0.1/tcp/9010 +daemon_args=(daemon --migrate=true) +if [[ "$(<"${TMPDIR}/kubo-enable-gc")" == "true" ]]; then + daemon_args+=(--enable-gc) +fi log "Starting Kubo with automatic repository migrations enabled." -as_truthgate ipfs daemon --migrate=true & +as_truthgate ipfs "${daemon_args[@]}" & ipfs_pid=$! stop_children() { From 468c7c0c2c7f7831f4f22db3908c28e53b03000a Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:42:08 -0400 Subject: [PATCH 42/59] Validate managed Kubo server defaults in CI --- .github/workflows/docker.yml | 57 +++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index bffe3ea..464d29c 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -29,6 +29,12 @@ jobs: run: docker compose -f compose.yaml config - name: Validate development override run: docker compose -f compose.yaml -f compose.dev.yaml config + - name: Validate container scripts + run: | + bash -n docker/entrypoint.sh + bash -n docker/healthcheck.sh + bash -n docker/kubo-configure.sh + bash -n docker/kubo-status.sh smoke-test: strategy: @@ -91,6 +97,8 @@ jobs: export TRUTHGATE_HTTP_PORT=18080 export TRUTHGATE_HTTPS_PORT=18443 export IPFS_SWARM_PORT=14001 + export TRUTHGATE_KUBO_PUBLIC_IPV4=off + export TRUTHGATE_KUBO_PUBLIC_IPV6=off export TRUTHGATE_DATA_HOST_PATH=./.ci-data/truthgate export IPFS_REPO_HOST_PATH=./.ci-data/ipfs/repo export IPFS_BLOCKS_HOST_PATH=./.ci-data/ipfs/blocks @@ -132,6 +140,47 @@ jobs: | cut -d' ' -f1 } + read_settings_hash() { + docker exec truthgate \ + sha256sum /data/truthgate/config/kubo-settings.json \ + | cut -d' ' -f1 + } + + validate_kubo_server_defaults() { + test "$(docker exec truthgate ipfs config Routing.Type)" = "dhtserver" + test "$(docker exec truthgate ipfs config Swarm.EnableHolePunching)" = "true" + test "$(docker exec truthgate ipfs config Swarm.RelayClient.Enabled)" = "true" + test "$(docker exec truthgate ipfs config Provide.Enabled)" = "true" + test "$(docker exec truthgate ipfs config Provide.Strategy)" = "all" + test "$(docker exec truthgate ipfs config Provide.DHT.Interval)" = "22h" + test "$(docker exec truthgate ipfs config Provide.DHT.SweepEnabled)" = "true" + test "$(docker exec truthgate ipfs config Provide.DHT.ResumeEnabled)" = "true" + test "$(docker exec truthgate ipfs config Datastore.StorageGCWatermark)" = "90" + + storage_max="$(docker exec truthgate ipfs config Datastore.StorageMax)" + test "${storage_max}" != "10GB" + [[ "${storage_max}" =~ ^[0-9]+B$ ]] + + docker exec truthgate test -s /data/truthgate/state/kubo-server-profile-v1 + docker exec truthgate test -s /data/truthgate/config/kubo-settings.json + docker exec truthgate test -s /data/truthgate/config/kubo-overrides.json + + docker exec truthgate \ + ipfs config Addresses.Swarm --json \ + | jq -e ' + index("/ip4/0.0.0.0/tcp/4001") and + index("/ip6/::/tcp/4001") and + index("/ip4/0.0.0.0/udp/4001/quic-v1") and + index("/ip4/0.0.0.0/udp/4001/quic-v1/webtransport") and + index("/ip6/::/udp/4001/quic-v1") and + index("/ip6/::/udp/4001/quic-v1/webtransport") + ' + + test "$(docker exec truthgate ipfs config Addresses.API)" = "/ip4/127.0.0.1/tcp/5001" + test "$(docker exec truthgate ipfs config Addresses.Gateway)" = "/ip4/127.0.0.1/tcp/9010" + docker exec truthgate truthgate-kubo-status + } + mkdir -p .ci-data/truthgate .ci-data/ipfs/repo .ci-data/ipfs/blocks docker compose up --detach --no-build @@ -141,22 +190,28 @@ jobs: # intentionally unable to inspect the bootstrap secret directly. docker exec truthgate test -s /data/ipfs/repo/config docker exec truthgate test -s /data/truthgate/state/bootstrap-admin-password + validate_kubo_server_defaults peer_id_before="$(read_peer_id)" bootstrap_hash_before="$(read_bootstrap_hash)" + settings_hash_before="$(read_settings_hash)" # Replace the container while retaining only the documented bind-mounted - # state. The Kubo identity and first-run credential must remain stable. + # state. Kubo identity, managed settings, and first-run credentials must + # remain stable. docker compose down --remove-orphans docker compose up --detach --no-build wait_for_health docker exec truthgate test -s /data/ipfs/repo/config docker exec truthgate test -s /data/truthgate/state/bootstrap-admin-password + validate_kubo_server_defaults peer_id_after="$(read_peer_id)" bootstrap_hash_after="$(read_bootstrap_hash)" + settings_hash_after="$(read_settings_hash)" test "${peer_id_before}" = "${peer_id_after}" test "${bootstrap_hash_before}" = "${bootstrap_hash_after}" + test "${settings_hash_before}" = "${settings_hash_after}" docker compose logs --no-color From 969be3b5208b20dbf7b1a8c1b52f28c7dc6b469a Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:42:33 -0400 Subject: [PATCH 43/59] Document managed Kubo server behavior --- DOCKER.md | 224 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 206 insertions(+), 18 deletions(-) diff --git a/DOCKER.md b/DOCKER.md index 7a26849..9222885 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -4,12 +4,19 @@ TruthGate ships as one container containing the TruthGate ASP.NET application, Kubo, and the matching `ipfs` CLI. Docker owns the software lifecycle; mounted state survives image replacement. +The appliance is intentionally configured as a contributing IPFS server. It is +not a quiet desktop-node preset: DHT server mode, inbound TCP/UDP swarm +transports, content providing, and automatic repository GC are enabled unless +an operator explicitly overrides them. + ## Requirements - Docker Engine with the Compose v2 plugin - Docker Compose 2.24.4 or newer for the development override's `!override` merge directive - TCP ports 80, 443, and 4001 plus UDP port 4001 available by default +- Public TCP and UDP forwarding for the selected swarm port when deployed + behind NAT ## Production quick start @@ -66,24 +73,203 @@ They are mounted as: Kubo still sees a conventional `$IPFS_PATH/blocks` directory, while the host can later relocate only the blockstore by changing `IPFS_BLOCKS_HOST_PATH`. No network filesystem, FUSE, JuiceFS, mount management, or mount health logic -is included in this initial implementation. +is included in this implementation. The software inside the image is disposable. Backups should target the mounted state, not the .NET runtime, Kubo binary, application binaries, or build output. +## Opinionated Kubo server defaults + +A new repository is initialized with Kubo's `server` profile. An existing +repository receives that profile once, tracked by +`data/truthgate/state/kubo-server-profile-v1`. The profile disables mDNS and +automatic NAT port mapping and filters non-public address ranges, matching a +public server deployment. Set `TRUTHGATE_KUBO_APPLY_SERVER_PROFILE=false` +before the first managed boot when that behavior is not appropriate. + +TruthGate then manages these defaults: + +```text +Routing.Type dhtserver +Addresses.Swarm current Kubo defaults, with missing + TCP/QUIC/WebTransport listeners restored +Swarm.EnableHolePunching true +Swarm.RelayClient.Enabled true +Provide.Enabled true +Provide.Strategy all +Provide.DHT.Interval 22h +Provide.DHT.SweepEnabled true +Provide.DHT.ResumeEnabled true +Datastore.StorageMax auto: 90% of the blockstore filesystem +Datastore.StorageGCWatermark 90 +Automatic repository GC enabled +Addresses.API /ip4/127.0.0.1/tcp/5001 +Addresses.Gateway /ip4/127.0.0.1/tcp/9010 +``` + +The entrypoint preserves Kubo's current listener array and only appends missing +standard listeners. That avoids deleting newer transports such as +WebTransport when Kubo changes its defaults. + +The RPC API and HTTP gateway are always loopback-only. TruthGate already +authenticates and proxies public `/api/v0`, `/ipfs`, `/ipns`, and WebUI access. +Those two Kubo listener settings cannot be overridden. + +The public swarm mapping on port 4001 is the current implementation stage. +TruthGate is expected to own and proxy swarm transports in a future +architecture; until that transport layer exists, Docker publishes Kubo's TCP +and UDP swarm traffic directly. + +## Persistent Kubo settings + +On first boot TruthGate creates: + +```text +data/truthgate/config/kubo-settings.json +``` + +This file is persistent and contains the normal managed settings. It is also +the contract intended for future live management from the TruthGate UI. +Editing the file and restarting the container changes the effective Kubo +configuration without rebuilding the image. + +Environment variables take precedence over the persistent settings file. The +available variables are documented in `.env.example`, including: + +```text +TRUTHGATE_KUBO_ROUTING_TYPE +TRUTHGATE_KUBO_PROVIDE_ENABLED +TRUTHGATE_KUBO_PROVIDE_STRATEGY +TRUTHGATE_KUBO_STORAGE_MAX +TRUTHGATE_KUBO_STORAGE_PERCENT +TRUTHGATE_KUBO_ENABLE_GC +TRUTHGATE_KUBO_PUBLIC_IPV4 +TRUTHGATE_KUBO_PUBLIC_IPV6 +TRUTHGATE_KUBO_ANNOUNCE_PORT +``` + +For advanced settings, edit: + +```text +data/truthgate/config/kubo-overrides.json +``` + +It is a JSON object whose keys are Kubo config paths and whose values are raw +JSON values: + +```json +{ + "Swarm.ConnMgr.LowWater": 100, + "Swarm.ConnMgr.HighWater": 300, + "Routing.AcceleratedDHTClient": false +} +``` + +Advanced overrides run after TruthGate's normal defaults. `Addresses.API` and +`Addresses.Gateway` are ignored in this file because they are protected +loopback interfaces. + +## Storage policy + +The default storage policy is: + +```json +{ + "storage": { + "max": "auto", + "percent": 90, + "fallback": "200GB", + "gcWatermark": 90, + "enableGc": true + } +} +``` + +`auto` reads the total capacity of the filesystem containing +`/data/ipfs/repo/blocks` and sets Kubo's `Datastore.StorageMax` to 90 percent of +that total. It uses total capacity rather than currently free space so the +limit does not shrink as content is added. If capacity detection fails, the +fallback is `200GB`. + +A fixed value is also supported: + +```env +TRUTHGATE_KUBO_STORAGE_MAX=2TB +``` + +Auto mode recalculates on every container start, so expanding a VPS disk only +requires a container restart. Fixed mode remains exactly the configured value. +`StorageMax` is a soft Kubo blockstore/GC threshold rather than a hard quota, +so filesystem and metadata headroom still matter. + +## Public announce addresses + +By default TruthGate attempts to detect public IPv4 and IPv6 addresses and +manages matching TCP, QUIC-v1, and WebTransport entries in +`Addresses.AppendAnnounce`. Previously managed entries are removed before new +ones are added, so public-IP changes do not accumulate stale addresses. + +Each address can be automatic, disabled, or literal: + +```env +TRUTHGATE_KUBO_PUBLIC_IPV4=auto +TRUTHGATE_KUBO_PUBLIC_IPV6=off +TRUTHGATE_KUBO_ANNOUNCE_PORT=4001 +``` + +The announce port defaults to `IPFS_SWARM_PORT`. If Docker publishes +`14001:4001`, set `IPFS_SWARM_PORT=14001`; TruthGate will announce `14001` while +Kubo continues listening on container port 4001. + ## Kubo startup and migrations -The entrypoint initializes Kubo only when `/data/ipfs/repo/config` is absent. -Existing repositories are preserved. Every start uses: +Every start uses automatic repository migration. Automatic GC is included when +enabled: + +```bash +ipfs daemon --migrate=true --enable-gc +``` + +The entrypoint applies managed settings before starting the daemon, then starts +TruthGate only after Kubo's loopback RPC API is ready. + +## Diagnostics + +Inspect the effective configuration and live node state with: + +```bash +docker exec truthgate truthgate-kubo-status +``` + +The report includes: + +- peer ID and connected peer count +- effective routing, provide, swarm, and storage configuration +- configured and active listen addresses +- public append-announcement addresses +- repository statistics +- AutoNAT observations +- sweep-provider statistics + +Useful direct checks include: + +```bash +docker exec truthgate ipfs config Routing.Type +docker exec truthgate ipfs config --json Addresses.Swarm +docker exec truthgate ipfs config --json Addresses.AppendAnnounce +docker exec truthgate ipfs provide stat +docker exec truthgate ipfs stats dht +``` + +A real external validation should use another IPFS node: ```bash -ipfs daemon --migrate=true +ipfs routing findpeer ``` -This lets Kubo perform supported repository migrations when a newer image is -started against older persistent data. The RPC API listens only on container -loopback at port 5001, and the Kubo HTTP gateway listens only on loopback at -port 9010. Neither is published to the host. +Then add a unique file through TruthGate, obtain its CID, and retrieve it from +the separate node. That verifies routing and content providing, not merely +local daemon health. ## Development @@ -94,21 +280,21 @@ docker compose -f compose.yaml -f compose.dev.yaml up --build ``` The override changes the image target to the .NET SDK development stage, -bind-mounts the repository at `/workspace`, adds a persistent NuGet cache, and +bind-mounts the repository at `/workspace`, adds persistent NuGet caches, and runs the web project with `dotnet watch`. TruthGate is available at `http://localhost:8080` by default. -Kubo initialization, repository migrations, persistent paths, startup order, -and process supervision are shared with production. This prevents development -from silently using a different node layout. +Kubo initialization, server defaults, repository migrations, persistent paths, +startup order, and process supervision are shared with production. This +prevents development from silently using a different node layout. Rider can use `compose.yaml` followed by `compose.dev.yaml` directly in a Docker Compose run configuration. The source directory is mounted directly, so normal edits trigger `dotnet watch`. -## Configuration +## General configuration -All ordinary host-facing settings live in `.env`. Important values include: +Ordinary host-facing settings live in `.env`. Important values include: - `TRUTHGATE_IMAGE` - `KUBO_VERSION` @@ -124,7 +310,9 @@ at the directory containing `compose.yaml`. ## Process model -`tini` is PID 1. The entrypoint starts Kubo first, waits for its RPC API, then -starts TruthGate. If either process exits, the entrypoint terminates the other -and exits so Docker's restart policy can recover the appliance. SIGTERM and -SIGINT are forwarded to both processes for graceful shutdown. +`tini` is PID 1 in normal Docker execution. Rider's debugger may become PID 1, +in which case Tini registers as a child subreaper. The entrypoint starts Kubo +first, waits for its RPC API, then starts TruthGate. If either process exits, +the entrypoint terminates the other and exits so Docker's restart policy can +recover the appliance. SIGTERM and SIGINT are forwarded to both processes for +graceful shutdown. From fca02c65079ba6107f527e8dad3e831b235ae9a8 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:43:10 -0400 Subject: [PATCH 44/59] Use canonical Kubo JSON config syntax --- docker/kubo-status.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/kubo-status.sh b/docker/kubo-status.sh index ded9810..cd91767 100644 --- a/docker/kubo-status.sh +++ b/docker/kubo-status.sh @@ -9,7 +9,7 @@ config_value() { } config_json() { - ipfs config "$1" --json 2>/dev/null | jq -c . || printf '' + ipfs config --json "$1" 2>/dev/null | jq -c . || printf '' } printf 'TruthGate Kubo status\n' From f26d8441e60be789563082c72f7b74234f640192 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:43:44 -0400 Subject: [PATCH 45/59] Use canonical Kubo JSON config syntax --- docker/kubo-configure.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docker/kubo-configure.sh b/docker/kubo-configure.sh index d245bf3..074a137 100644 --- a/docker/kubo-configure.sh +++ b/docker/kubo-configure.sh @@ -32,16 +32,16 @@ setting() { } set_string() { - as_truthgate ipfs config "$1" --json "$(jq -cn --arg value "$2" '$value')" + as_truthgate ipfs config --json "$1" "$(jq -cn --arg value "$2" '$value')" } set_json() { - as_truthgate ipfs config "$1" --json "$2" + as_truthgate ipfs config --json "$1" "$2" } ensure_array_value() { local key="$1" item="$2" current compact updated - current="$(as_truthgate ipfs config "${key}" --json 2>/dev/null || printf '[]')" + current="$(as_truthgate ipfs config --json "${key}" 2>/dev/null || printf '[]')" compact="$(jq -c . <<<"${current}")" updated="$(jq -c --arg item "${item}" \ 'if type != "array" then [$item] elif index($item) then . else . + [$item] end' \ @@ -268,7 +268,7 @@ if [[ -n "${public_ipv6}" ]]; then fi managed_state="${TRUTHGATE_STATE_PATH}/kubo-managed-append-announce.json" -current="$(as_truthgate ipfs config Addresses.AppendAnnounce --json 2>/dev/null || printf '[]')" +current="$(as_truthgate ipfs config --json Addresses.AppendAnnounce 2>/dev/null || printf '[]')" previous='[]' if [[ -s "${managed_state}" ]]; then previous="$(cat "${managed_state}")" @@ -309,7 +309,7 @@ set_string Addresses.Gateway /ip4/127.0.0.1/tcp/9010 log "Kubo server configuration:" log " Routing.Type: $(as_truthgate ipfs config Routing.Type)" -log " Swarm listeners: $(as_truthgate ipfs config Addresses.Swarm --json | jq -c .)" +log " Swarm listeners: $(as_truthgate ipfs config --json Addresses.Swarm | jq -c .)" log " Managed public announce IPv4: ${public_ipv4:-disabled or unavailable}" log " Managed public announce IPv6: ${public_ipv6:-disabled or unavailable}" log " Public announce port: ${announce_port}" From f6f3f98a607aa3a6cff0439d606ce2fddca70fe9 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:44:05 -0400 Subject: [PATCH 46/59] Use canonical Kubo JSON config syntax in CI --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 464d29c..7ec5080 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -166,7 +166,7 @@ jobs: docker exec truthgate test -s /data/truthgate/config/kubo-overrides.json docker exec truthgate \ - ipfs config Addresses.Swarm --json \ + ipfs config --json Addresses.Swarm \ | jq -e ' index("/ip4/0.0.0.0/tcp/4001") and index("/ip6/::/tcp/4001") and From 03497f1fcec7120b25bbb1ae3c3efc97ad6a2632 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:44:41 -0400 Subject: [PATCH 47/59] Describe Kubo server defaults in README --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ef04aca..437c784 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,18 @@ The first-start logs contain a generated password for the `admin` account. Open `https://localhost`, accept the temporary self-signed fallback certificate, and change the password. -For the full persistence contract, image update flow, ARM64/AMD64 publishing, -and Docker-based development setup, see **[DOCKER.md](DOCKER.md)**. +The appliance configures Kubo as a contributing server by default: DHT server +mode, TCP/QUIC/WebTransport swarm listeners, content providing, automatic +storage sizing, and repository GC are enabled with persistent per-setting +overrides. Inspect the live node with: + +```bash +docker exec truthgate truthgate-kubo-status +``` + +For the full persistence contract, Kubo settings, image update flow, +ARM64/AMD64 publishing, and Docker-based development setup, see +**[DOCKER.md](DOCKER.md)**. Development with hot reload uses the production definition plus a small override: From c426e5e9741f6fc5111b16e32c77eaddf3f52878 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:31:07 -0400 Subject: [PATCH 48/59] Migrate existing Kubo repos before configuration --- docker/entrypoint.sh | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index c4ace3f..a9de22d 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -30,6 +30,7 @@ require_absolute_path() { : "${TRUTHGATE_CERT_PATH:=/data/truthgate/certificates}" : "${TRUTHGATE_STATE_PATH:=/data/truthgate/state}" : "${TMPDIR:=/run/truthgate}" +: "${TRUTHGATE_KUBO_REPO_VERSION:=18}" export IPFS_PATH TRUTHGATE_CONFIG_PATH TRUTHGATE_DATABASE_PATH export TRUTHGATE_CERT_PATH TRUTHGATE_STATE_PATH TMPDIR @@ -40,6 +41,8 @@ require_absolute_path TRUTHGATE_DATABASE_PATH "${TRUTHGATE_DATABASE_PATH}" require_absolute_path TRUTHGATE_CERT_PATH "${TRUTHGATE_CERT_PATH}" require_absolute_path TRUTHGATE_STATE_PATH "${TRUTHGATE_STATE_PATH}" require_absolute_path TMPDIR "${TMPDIR}" +[[ "${TRUTHGATE_KUBO_REPO_VERSION}" =~ ^[0-9]+$ ]] \ + || fail "TRUTHGATE_KUBO_REPO_VERSION must be a positive integer." config_directory="$(dirname "${TRUTHGATE_CONFIG_PATH}")" blocks_directory="${IPFS_PATH}/blocks" @@ -103,6 +106,43 @@ chown "${truthgate_user}:${truthgate_group}" \ "${data_protection_directory}" \ "${TMPDIR}" +migrate_existing_kubo_repository() { + local repo_config="${IPFS_PATH}/config" + local repo_version_file="${IPFS_PATH}/version" + local current_version migrated_version + + # Fresh repositories are initialized later by truthgate-configure-kubo and + # already use the format expected by the bundled Kubo binary. + [[ -s "${repo_config}" ]] || return 0 + + [[ -s "${repo_version_file}" ]] \ + || fail "Existing Kubo repository is missing its version file: ${repo_version_file}" + + current_version="$(tr -d '[:space:]' <"${repo_version_file}")" + [[ "${current_version}" =~ ^[0-9]+$ ]] \ + || fail "Kubo repository version is invalid: '${current_version}'." + + if (( current_version > TRUTHGATE_KUBO_REPO_VERSION )); then + fail "Kubo repository version ${current_version} is newer than the bundled Kubo supports (${TRUTHGATE_KUBO_REPO_VERSION}). Upgrade the TruthGate image before starting this repository." + fi + + if (( current_version == TRUTHGATE_KUBO_REPO_VERSION )); then + log "Kubo repository is already at version ${current_version}." + return 0 + fi + + log "Migrating Kubo repository from version ${current_version} to ${TRUTHGATE_KUBO_REPO_VERSION} before applying managed configuration." + as_truthgate ipfs repo migrate --to="${TRUTHGATE_KUBO_REPO_VERSION}" + + migrated_version="$(tr -d '[:space:]' <"${repo_version_file}")" + [[ "${migrated_version}" == "${TRUTHGATE_KUBO_REPO_VERSION}" ]] \ + || fail "Kubo repository migration completed without producing expected version ${TRUTHGATE_KUBO_REPO_VERSION}; found '${migrated_version}'." + + log "Kubo repository migration completed at version ${migrated_version}." +} + +migrate_existing_kubo_repository + bootstrap_password_file="${TRUTHGATE_STATE_PATH}/bootstrap-admin-password" if [[ ! -s "${TRUTHGATE_CONFIG_PATH}" ]]; then if [[ -n "${TRUTHGATE_BOOTSTRAP_ADMIN_PASSWORD:-}" ]]; then From 3d0b0f3176f6da25a13ccf1ddd60173453af4f1b Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:31:27 -0400 Subject: [PATCH 49/59] Test automatic Kubo repository migration --- .github/workflows/docker-repo-migration.yml | 120 ++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 .github/workflows/docker-repo-migration.yml diff --git a/.github/workflows/docker-repo-migration.yml b/.github/workflows/docker-repo-migration.yml new file mode 100644 index 0000000..f95beb5 --- /dev/null +++ b/.github/workflows/docker-repo-migration.yml @@ -0,0 +1,120 @@ +name: Docker repository migration + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: docker-repo-migration-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + migrate-kubo-repository: + strategy: + fail-fast: false + matrix: + include: + - architecture: amd64 + runner: ubuntu-24.04 + - architecture: arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + + steps: + - uses: actions/checkout@v4 + + - name: Build production image (${{ matrix.architecture }}) + run: | + docker build \ + --platform linux/${{ matrix.architecture }} \ + --target production \ + --tag truthgate-ipfs:migration-test-${{ matrix.architecture }} \ + . + + - name: Create a Kubo 0.36 repository and verify automatic migration + shell: bash + run: | + exec > >(tee repo-migration-${{ matrix.architecture }}.log) 2>&1 + set -Eeuo pipefail + + export TRUTHGATE_IMAGE=truthgate-ipfs:migration-test-${{ matrix.architecture }} + export TRUTHGATE_HTTP_PORT=18080 + export TRUTHGATE_HTTPS_PORT=18443 + export IPFS_SWARM_PORT=14001 + export TRUTHGATE_KUBO_PUBLIC_IPV4=off + export TRUTHGATE_KUBO_PUBLIC_IPV6=off + export TRUTHGATE_DATA_HOST_PATH=./.ci-migration/truthgate + export IPFS_REPO_HOST_PATH=./.ci-migration/ipfs/repo + export IPFS_BLOCKS_HOST_PATH=./.ci-migration/ipfs/blocks + + cleanup() { + docker compose logs --no-color 2>/dev/null || true + docker compose down --remove-orphans 2>/dev/null || true + } + trap cleanup EXIT + + rm -rf .ci-migration + mkdir -p \ + .ci-migration/truthgate \ + .ci-migration/ipfs/repo \ + .ci-migration/ipfs/blocks + + docker run --rm \ + --user 0:0 \ + --entrypoint /usr/local/bin/ipfs \ + --env IPFS_PATH=/data/ipfs/repo \ + --volume "$PWD/.ci-migration/ipfs/repo:/data/ipfs/repo" \ + --volume "$PWD/.ci-migration/ipfs/blocks:/data/ipfs/repo/blocks" \ + ipfs/kubo:v0.36.0 \ + init --profile server + + test "$(tr -d '[:space:]' < .ci-migration/ipfs/repo/version)" = "16" + peer_id_before="$(jq -r '.Identity.PeerID' .ci-migration/ipfs/repo/config)" + test -n "${peer_id_before}" + + sudo chown -R 1000:1000 .ci-migration + + docker compose up --detach --no-build + + for attempt in $(seq 1 60); do + state="$(docker inspect --format '{{.State.Status}}' truthgate)" + health="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' truthgate)" + echo "Attempt ${attempt}: state=${state} health=${health}" + + if [[ "${health}" == "healthy" ]]; then + break + fi + + if [[ "${state}" == "exited" || "${health}" == "unhealthy" ]]; then + docker compose logs --no-color + exit 1 + fi + + sleep 5 + done + + test "$(docker inspect --format '{{.State.Health.Status}}' truthgate)" = "healthy" + test "$(docker exec truthgate tr -d '[:space:]' Date: Fri, 24 Jul 2026 18:31:57 -0400 Subject: [PATCH 50/59] Fix migration version assertion --- .github/workflows/docker-repo-migration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-repo-migration.yml b/.github/workflows/docker-repo-migration.yml index f95beb5..87f8c46 100644 --- a/.github/workflows/docker-repo-migration.yml +++ b/.github/workflows/docker-repo-migration.yml @@ -97,7 +97,7 @@ jobs: done test "$(docker inspect --format '{{.State.Health.Status}}' truthgate)" = "healthy" - test "$(docker exec truthgate tr -d '[:space:]' Date: Fri, 24 Jul 2026 18:32:17 -0400 Subject: [PATCH 51/59] Simplify migrated repo version check --- .github/workflows/docker-repo-migration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-repo-migration.yml b/.github/workflows/docker-repo-migration.yml index 87f8c46..d08f626 100644 --- a/.github/workflows/docker-repo-migration.yml +++ b/.github/workflows/docker-repo-migration.yml @@ -97,7 +97,7 @@ jobs: done test "$(docker inspect --format '{{.State.Health.Status}}' truthgate)" = "healthy" - test "$(docker exec truthgate sh -lc "tr -d '[:space:]' Date: Fri, 24 Jul 2026 18:34:16 -0400 Subject: [PATCH 52/59] Fix legacy repo test ownership --- .github/workflows/docker-repo-migration.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-repo-migration.yml b/.github/workflows/docker-repo-migration.yml index d08f626..393544e 100644 --- a/.github/workflows/docker-repo-migration.yml +++ b/.github/workflows/docker-repo-migration.yml @@ -71,12 +71,12 @@ jobs: ipfs/kubo:v0.36.0 \ init --profile server + sudo chown -R 1000:1000 .ci-migration + test "$(tr -d '[:space:]' < .ci-migration/ipfs/repo/version)" = "16" peer_id_before="$(jq -r '.Identity.PeerID' .ci-migration/ipfs/repo/config)" test -n "${peer_id_before}" - sudo chown -R 1000:1000 .ci-migration - docker compose up --detach --no-build for attempt in $(seq 1 60); do From dbae6006a7ad988055e4a05a8b59256ca8bf9d03 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:36:19 -0400 Subject: [PATCH 53/59] Read legacy repo metadata before ownership handoff --- .github/workflows/docker-repo-migration.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-repo-migration.yml b/.github/workflows/docker-repo-migration.yml index 393544e..69dd808 100644 --- a/.github/workflows/docker-repo-migration.yml +++ b/.github/workflows/docker-repo-migration.yml @@ -71,12 +71,12 @@ jobs: ipfs/kubo:v0.36.0 \ init --profile server - sudo chown -R 1000:1000 .ci-migration - - test "$(tr -d '[:space:]' < .ci-migration/ipfs/repo/version)" = "16" - peer_id_before="$(jq -r '.Identity.PeerID' .ci-migration/ipfs/repo/config)" + test "$(sudo tr -d '[:space:]' < .ci-migration/ipfs/repo/version)" = "16" + peer_id_before="$(sudo jq -r '.Identity.PeerID' .ci-migration/ipfs/repo/config)" test -n "${peer_id_before}" + sudo chown -R 1000:1000 .ci-migration + docker compose up --detach --no-build for attempt in $(seq 1 60); do From 28147d662b1a850f87ac19bb02bd09063443419d Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:56:39 -0400 Subject: [PATCH 54/59] Let authenticated users access the TruthGate portal --- .../Middleware/DomainGatewayMiddleware.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Middleware/DomainGatewayMiddleware.cs b/TruthGate-Web/TruthGate-Web/Middleware/DomainGatewayMiddleware.cs index 45260a2..d056ea5 100644 --- a/TruthGate-Web/TruthGate-Web/Middleware/DomainGatewayMiddleware.cs +++ b/TruthGate-Web/TruthGate-Web/Middleware/DomainGatewayMiddleware.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using System.IO; using System.Net; @@ -19,6 +19,16 @@ private sealed record RunOnceResult( private static async Task RunOnce(HttpContext ctx, Func next) { + // A mapped hostname can serve both the public IPFS site and TruthGate's + // administrator portal. Anonymous visitors receive the mapped site; + // authenticated users stay inside the real application, including its + // Blazor framework, SignalR circuit, static assets, and dashboard routes. + if (ctx.User?.Identity?.IsAuthenticated == true) + { + await next(); + return new RunOnceResult(Handled: true, RetryCandidate: false, Cid: null, MfsPath: null); + } + var mfsPath = DomainHelpers.GetMappedDomain(ctx); if (string.IsNullOrWhiteSpace(mfsPath)) { @@ -162,4 +172,4 @@ public static IApplicationBuilder UseDomainGateway(this IApplicationBuilder app) return app; } } -} \ No newline at end of file +} From 67c127007c27dec9b7ff3980715a8fd8c40f410e Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:56:49 -0400 Subject: [PATCH 55/59] Test authenticated portal bypass for mapped domains --- .../DomainGatewayMiddlewareTests.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 Test.TruthGate/DomainGatewayMiddlewareTests.cs diff --git a/Test.TruthGate/DomainGatewayMiddlewareTests.cs b/Test.TruthGate/DomainGatewayMiddlewareTests.cs new file mode 100644 index 0000000..790746a --- /dev/null +++ b/Test.TruthGate/DomainGatewayMiddlewareTests.cs @@ -0,0 +1,41 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using TruthGate_Web.Middleware; + +namespace Test.TruthGate; + +public sealed class DomainGatewayMiddlewareTests +{ + [Fact] + public async Task AuthenticatedRequest_BypassesGatewayBeforeMappedDomainLookup() + { + await using var services = new ServiceCollection().BuildServiceProvider(); + var app = new ApplicationBuilder(services); + var nextCalled = false; + + app.UseDomainGateway(); + app.Run(context => + { + nextCalled = true; + context.Response.StatusCode = StatusCodes.Status204NoContent; + return Task.CompletedTask; + }); + + var context = new DefaultHttpContext + { + RequestServices = services, + User = new ClaimsPrincipal( + new ClaimsIdentity( + new[] { new Claim(ClaimTypes.NameIdentifier, "admin") }, + authenticationType: "Test")) + }; + context.Request.Host = new HostString("truthgate.io"); + + await app.Build()(context); + + Assert.True(nextCalled); + Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); + } +} From 7cf8bb195ec581e8a929711f358be4498641b041 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:21:23 -0400 Subject: [PATCH 56/59] Revert authenticated domain gateway bypass --- .../Middleware/DomainGatewayMiddleware.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Middleware/DomainGatewayMiddleware.cs b/TruthGate-Web/TruthGate-Web/Middleware/DomainGatewayMiddleware.cs index d056ea5..45260a2 100644 --- a/TruthGate-Web/TruthGate-Web/Middleware/DomainGatewayMiddleware.cs +++ b/TruthGate-Web/TruthGate-Web/Middleware/DomainGatewayMiddleware.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using System.IO; using System.Net; @@ -19,16 +19,6 @@ private sealed record RunOnceResult( private static async Task RunOnce(HttpContext ctx, Func next) { - // A mapped hostname can serve both the public IPFS site and TruthGate's - // administrator portal. Anonymous visitors receive the mapped site; - // authenticated users stay inside the real application, including its - // Blazor framework, SignalR circuit, static assets, and dashboard routes. - if (ctx.User?.Identity?.IsAuthenticated == true) - { - await next(); - return new RunOnceResult(Handled: true, RetryCandidate: false, Cid: null, MfsPath: null); - } - var mfsPath = DomainHelpers.GetMappedDomain(ctx); if (string.IsNullOrWhiteSpace(mfsPath)) { @@ -172,4 +162,4 @@ public static IApplicationBuilder UseDomainGateway(this IApplicationBuilder app) return app; } } -} +} \ No newline at end of file From 0f55ef8382eee127e15ef85b1ad62b4addfeabaa Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:21:34 -0400 Subject: [PATCH 57/59] Remove invalid domain gateway regression test --- .../DomainGatewayMiddlewareTests.cs | 41 ------------------- 1 file changed, 41 deletions(-) delete mode 100644 Test.TruthGate/DomainGatewayMiddlewareTests.cs diff --git a/Test.TruthGate/DomainGatewayMiddlewareTests.cs b/Test.TruthGate/DomainGatewayMiddlewareTests.cs deleted file mode 100644 index 790746a..0000000 --- a/Test.TruthGate/DomainGatewayMiddlewareTests.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Security.Claims; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using TruthGate_Web.Middleware; - -namespace Test.TruthGate; - -public sealed class DomainGatewayMiddlewareTests -{ - [Fact] - public async Task AuthenticatedRequest_BypassesGatewayBeforeMappedDomainLookup() - { - await using var services = new ServiceCollection().BuildServiceProvider(); - var app = new ApplicationBuilder(services); - var nextCalled = false; - - app.UseDomainGateway(); - app.Run(context => - { - nextCalled = true; - context.Response.StatusCode = StatusCodes.Status204NoContent; - return Task.CompletedTask; - }); - - var context = new DefaultHttpContext - { - RequestServices = services, - User = new ClaimsPrincipal( - new ClaimsIdentity( - new[] { new Claim(ClaimTypes.NameIdentifier, "admin") }, - authenticationType: "Test")) - }; - context.Request.Host = new HostString("truthgate.io"); - - await app.Build()(context); - - Assert.True(nextCalled); - Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); - } -} From 1287e7833824b7ab4d31afe8c915f68c12645c61 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 12:32:03 -0400 Subject: [PATCH 58/59] Include Blazor web assets in production publish --- TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj b/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj index d18a0fb..0a9f586 100644 --- a/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj +++ b/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj @@ -6,6 +6,7 @@ enable TruthGate_Web $(AssemblyName.Replace(' ', '_')) + true @@ -32,4 +33,4 @@ - + \ No newline at end of file From ed2986e494457c091473a5528e8ce2b8aac56a7d Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 12:32:54 -0400 Subject: [PATCH 59/59] Fail Docker builds without Blazor bootstrap assets --- Dockerfile | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Dockerfile b/Dockerfile index 7278b89..11fae41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,21 @@ RUN dotnet publish TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj \ --output /out \ /p:UseAppHost=false +# .NET 10 publishes the Blazor bootstrap as a static web asset instead of an +# embedded framework resource. Fail the image build if restore/publish ever +# omits it, because the portal would otherwise render static HTML while every +# interactive server component silently remains inactive. +RUN set -eu; \ + manifest=/out/TruthGate-Web.staticwebassets.endpoints.json; \ + test -s "${manifest}"; \ + grep -q '"Route":"_framework/blazor\.web\.js"' "${manifest}"; \ + asset="$(find /out/wwwroot/_framework -maxdepth 1 -type f \ + \( -name 'blazor.web.js' -o -name 'blazor.web.*.js' \) \ + -size +10000c -print -quit)"; \ + test -n "${asset}"; \ + printf 'Verified Blazor bootstrap asset: %s (%s bytes)\n' \ + "${asset}" "$(wc -c <"${asset}")" + FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION}-resolute AS runtime-base ARG TRUTHGATE_UID ARG TRUTHGATE_GID