Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -225,13 +225,13 @@ GITTENSORY_REVIEW_DRAFT=false
# # write trust can differ. Unrelated to ADMIN_GITHUB_LOGINS, which
# # scopes dashboard sign-in, not the shared MCP token's per-repo reach.
# PORT=8787
# DATABASE_PATH=/data/gittensory.sqlite # SQLite file on the mounted data volume; all migrations auto-apply
# DATABASE_PATH=/data/loopover.sqlite # SQLite file on the mounted data volume; all migrations auto-apply
# POSTGRES_PASSWORD=change-this-long-random-value # used by the --profile postgres / --profile pgbouncer services
# DATABASE_URL= # set to postgres://user:pw@host:5432/db to use Postgres instead of
# # SQLite (shared DB → multi-instance). Overrides DATABASE_PATH.
# # Compose examples:
# # postgres://gittensory:<POSTGRES_PASSWORD>@postgres:5432/gittensory
# # postgres://gittensory:<POSTGRES_PASSWORD>@pgbouncer:5432/gittensory
# # postgres://loopover:<POSTGRES_PASSWORD>@postgres:5432/loopover
# # postgres://loopover:<POSTGRES_PASSWORD>@pgbouncer:5432/loopover
# PGVECTOR_ENABLED=false # set true only when using the Postgres pgvector table for RAG.
# # Leave false when QDRANT_URL is set; Qdrant remains the preferred
# # dedicated vector store for review context at scale.
Expand Down Expand Up @@ -469,14 +469,14 @@ REDIS_URL=redis://redis:6379 # REQUIRED for the self-host review
# RUNNER_REPO_URL=https://github.com/org/repo
# RUNNER_ACCESS_TOKEN= # PAT with repo scope (alternative to RUNNER_TOKEN)
# RUNNER_SCOPE=repo # repo | org | enterprise
# RUNNER_NAME=gittensory-runner
# RUNNER_NAME=loopover-runner
# RUNNER_LABELS=self-hosted,linux
# RUNNER_MEM_LIMIT=2g # per-runner-container memory ceiling; raise for memory-heavy CI jobs

# --- Docker disk hygiene (#audit-rate-headroom / #selfhost-runtime-pressure) ---
# Build cache and unused images accumulate fast on a box that builds from source or runs CI runners; a root
# disk over ~80-85% full slows down the WHOLE host (fsync latency, container scheduling), not just Docker.
# Install the systemd timer (systemd/gittensory-docker-prune.{service,timer}.example) to reclaim stopped
# Install the systemd timer (systemd/loopover-docker-prune.{service,timer}.example) to reclaim stopped
# containers, unused images, and build cache on a schedule -- never volumes/application data. Run it
# manually at any time with `sh scripts/selfhost-docker-prune.sh`, or preview first with `--dry-run`.
# GITTENSORY_DOCKER_PRUNE_RETAIN_HOURS=168 # age floor before something unused is eligible for pruning (7 days)
Expand Down
2 changes: 1 addition & 1 deletion .env.selfhost.example
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ ADMIN_GITHUB_LOGINS=your-github-login
# SQLite is the default database (this file on the mounted data volume; migrations auto-apply).
# It's fine for a single maintainer instance. Move to Postgres (--profile postgres, see
# .env.example) once you're running multiple instances or a serious volume of repos.
DATABASE_PATH=/data/gittensory.sqlite
DATABASE_PATH=/data/loopover.sqlite

# Redis is REQUIRED and starts automatically with the default compose stack (webhook dedup, rate
# limiting, the GitHub GET-response cache). Only override this if you're pointing at an external
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ CLAUDE_AI_EFFORT=medium`}
creation time, so a plain restart keeps serving the old value — recreate the service
instead:
</p>
<CodeBlock code={`docker compose up -d --no-deps gittensory`} />
<CodeBlock code={`docker compose up -d --no-deps loopover`} />
<p>
Prefer not pasting the raw token into <code>.env</code>? Write it into{" "}
<code>secrets/claude_code_oauth_token.txt</code> instead (see{" "}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ LITESTREAM_REGION=us-east-1`}
<h2>Scheduled backups</h2>
<p>
The bundled <code>backup</code> profile writes the active app database to the{" "}
<code>gittensory-backups</code> volume. SQLite installs use an online backup; Postgres
<code>loopover-backups</code> volume. SQLite installs use an online backup; Postgres
installs use <code>pg_dump</code>. The same run also snapshots Qdrant when it is enabled.
</p>
<CodeBlock lang="bash" code={`docker compose --profile backup up -d`} />
Expand All @@ -64,7 +64,7 @@ LITESTREAM_REGION=us-east-1`}
<p>
Each run keeps the newest <code>BACKUP_RETAIN</code> backups (default <strong>7</strong>) —
applied <em>independently per target</em>: <code>postgres/</code>, <code>sqlite/</code>, and{" "}
<code>qdrant/</code> in the <code>gittensory-backups</code> volume each retain their own
<code>qdrant/</code> in the <code>loopover-backups</code> volume each retain their own
newest 7, not 7 combined across all three. Set it in <code>.env</code> to change the window:
</p>
<CodeBlock filename=".env" code={`BACKUP_RETAIN=14`} />
Expand Down Expand Up @@ -108,7 +108,7 @@ LITESTREAM_REGION=us-east-1`}
<CodeBlock
filename=".env"
code={`POSTGRES_PASSWORD=<password>
DATABASE_URL=postgres://gittensory:<password>@pgbouncer:5432/gittensory
DATABASE_URL=postgres://loopover:<password>@pgbouncer:5432/loopover
REDIS_URL=redis://redis:6379
QDRANT_URL=http://qdrant:6333`}
/>
Expand All @@ -131,9 +131,9 @@ QDRANT_URL=http://qdrant:6333`}
</p>
<CodeBlock
lang="bash"
code={`export DATABASE_URL=postgres://gittensory:<password>@pgbouncer:5432/gittensory
npm run selfhost:postgres:migrate -- --sqlite /data/gittensory.sqlite
npm run selfhost:postgres:migrate -- --sqlite /data/gittensory.sqlite --execute`}
code={`export DATABASE_URL=postgres://loopover:<password>@pgbouncer:5432/loopover
npm run selfhost:postgres:migrate -- --sqlite /data/loopover.sqlite
npm run selfhost:postgres:migrate -- --sqlite /data/loopover.sqlite --execute`}
/>

<h2>Restore checks</h2>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ function SelfHostingDocsAudit() {
</li>
<li>
<strong>Operator-owned paths:</strong> <code>gittensory-config/</code>,{" "}
<code>gittensory-data</code>, and secrets via <code>.env</code> or <code>*_FILE</code>{" "}
<code>loopover-data</code>, and secrets via <code>.env</code> or <code>*_FILE</code>{" "}
mounts — never baked into images.
</li>
</ul>
Expand Down
56 changes: 28 additions & 28 deletions apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function SelfHostingOperations() {
<CodeBlock
lang="bash"
code={`docker compose ps
docker compose logs -f gittensory
docker compose logs -f loopover
curl http://localhost:8787/ready
curl http://localhost:8787/metrics`}
/>
Expand Down Expand Up @@ -406,7 +406,7 @@ DISCORD_REPO_WEBHOOKS={"owner/repoA":"https://discord.com/api/webhooks/...","own
<strong>expected steady state, not a leak</strong> — this instance runs{" "}
<code>scripts/deploy-selfhost-prebuilt.sh</code>, which rebuilds the image from the current
git checkout on every deploy and intentionally keeps prior layers around in the build cache
for faster rebuilds. The <code>gittensory-docker-safe-prune</code> systemd timer (below)
for faster rebuilds. The <code>loopover-docker-safe-prune</code> systemd timer (below)
already runs daily against this exact instance and reclaims it on a schedule, so this is not
a number to chase down manually.
</p>
Expand Down Expand Up @@ -486,11 +486,11 @@ DISCORD_REPO_WEBHOOKS={"owner/repoA":"https://discord.com/api/webhooks/...","own
</p>
<CodeBlock
lang="bash"
code={`sudo cp systemd/gittensory-docker-prune.service.example /etc/systemd/system/gittensory-docker-prune.service
sudo cp systemd/gittensory-docker-prune.timer.example /etc/systemd/system/gittensory-docker-prune.timer
sudo $EDITOR /etc/systemd/system/gittensory-docker-prune.service # set WorkingDirectory / ExecStart to your path
code={`sudo cp systemd/loopover-docker-prune.service.example /etc/systemd/system/loopover-docker-prune.service
sudo cp systemd/loopover-docker-prune.timer.example /etc/systemd/system/loopover-docker-prune.timer
sudo $EDITOR /etc/systemd/system/loopover-docker-prune.service # set WorkingDirectory / ExecStart to your path
sudo systemctl daemon-reload
sudo systemctl enable --now gittensory-docker-prune.timer`}
sudo systemctl enable --now loopover-docker-prune.timer`}
/>
<p>
Run it manually at any time with <code>docker system df</code> before and after to see what
Expand Down Expand Up @@ -549,7 +549,7 @@ services:
condition: service_completed_successfully
environment:
<<: *runner-tmp-env
RUNNER_NAME: gittensory-runner-2
RUNNER_NAME: loopover-runner-2
RUNNER_SCOPE: \${RUNNER_SCOPE:-repo}
REPO_URL: \${RUNNER_REPO_URL:-}
RUNNER_TOKEN: \${RUNNER_TOKEN:-}
Expand Down Expand Up @@ -581,7 +581,7 @@ SENTRY_RELEASE=gittensory-selfhost@2026.07.05
Official release images bake <code>GITTENSORY_VERSION</code> as the default release id;
override with <code>SENTRY_RELEASE</code> when you tag custom builds. Mount secrets with{" "}
<code>SENTRY_DSN_FILE</code> instead of inline env when you prefer a file-backed DSN. After
changing Sentry env, restart the <code>gittensory</code> service — there is no hot reload.
changing Sentry env, restart the <code>loopover</code> service — there is no hot reload.
</p>
<Callout variant="note">
Community self-hosters should send events only to their own DSN. The shipped stack never
Expand Down Expand Up @@ -885,8 +885,8 @@ SENTRY_ORG_SLUG=<your-sentry-org-slug>

<h2>Updating and rolling back</h2>
<p>
Day-two operator flow: pull or build a new app image, restart only the{" "}
<code>gittensory</code> service, verify <code>/ready</code>, and confirm the release id. Use{" "}
Day-two operator flow: pull or build a new app image, restart only the <code>loopover</code>{" "}
service, verify <code>/ready</code>, and confirm the release id. Use{" "}
<Link to="/docs/self-hosting-releases">Releases and images</Link> to pick a tag; use the
checklists below so updates never overwrite operator-owned secrets, config, or data.
</p>
Expand All @@ -903,9 +903,9 @@ SENTRY_ORG_SLUG=<your-sentry-org-slug>
<code>.gittensory.yml</code> policy.
</li>
<li>
Named data volumes — especially <code>gittensory-data</code> (SQLite DB, Codex/Claude
auth under <code>/data</code>), <code>gittensory-pg</code>, <code>qdrant-data</code>,{" "}
<code>gittensory-backups</code>, and Grafana&apos;s <code>grafana-data</code>.
Named data volumes — especially <code>loopover-data</code> (SQLite DB, Codex/Claude auth
under <code>/data</code>), <code>loopover-pg</code>, <code>qdrant-data</code>,{" "}
<code>loopover-backups</code>, and Grafana&apos;s <code>grafana-data</code>.
</li>
<li>
Optional <code>docker-compose.override.yml</code> — still loaded via{" "}
Expand All @@ -918,14 +918,14 @@ SENTRY_ORG_SLUG=<your-sentry-org-slug>
<FeatureRow
items={[
{
title: "Restart gittensory only (normal app update)",
title: "Restart loopover only (normal app update)",
description:
"Both deploy-selfhost-image.sh and deploy-selfhost-prebuilt.sh run docker compose up -d --no-deps gittensory. Redis, Postgres, Qdrant, Grafana, backup sidecars, and every volume stay running with their existing data.",
"Both deploy-selfhost-image.sh and deploy-selfhost-prebuilt.sh run docker compose up -d --no-deps loopover. Redis, Postgres, Qdrant, Grafana, backup sidecars, and every volume stay running with their existing data.",
},
{
title: "Recreate a profile service (separate step)",
description:
"Only when you deliberately change that service's image or major version — e.g. docker compose --profile postgres pull postgres && docker compose --profile postgres up -d postgres. Never required just to ship a new gittensory app build.",
"Only when you deliberately change that service's image or major version — e.g. docker compose --profile postgres pull postgres && docker compose --profile postgres up -d postgres. Never required just to ship a new loopover app build.",
},
]}
/>
Expand Down Expand Up @@ -953,7 +953,7 @@ SENTRY_ORG_SLUG=<your-sentry-org-slug>
</li>
<li>
Image path only: note the current tag or digest from <code>docker inspect</code> on the
running <code>gittensory</code> container so rollback has a known-good target.
running <code>loopover</code> container so rollback has a known-good target.
</li>
<li>
Confirm routine health is green before you start —{" "}
Expand All @@ -964,7 +964,7 @@ SENTRY_ORG_SLUG=<your-sentry-org-slug>
<h3>Path 1: pull a published image</h3>
<p>
<code>scripts/deploy-selfhost-image.sh</code> pulls a tag or digest, restarts only the{" "}
<code>gittensory</code> service, waits for it to report <code>healthy</code> via{" "}
<code>loopover</code> service, waits for it to report <code>healthy</code> via{" "}
<code>docker inspect</code>&apos;s health status (configurable timeout, default 180s), and
then persists the resolved image reference back to <code>GITTENSORY_IMAGE</code> in{" "}
<code>.env</code> so the next plain invocation reuses it.
Expand Down Expand Up @@ -1036,7 +1036,7 @@ git merge --ff-only origin/main
<code>scripts/deploy-selfhost-prebuilt.sh</code> is the actual rebuild step (this is how{" "}
<code>GITTENSORY_VERSION</code> ends up as a short git SHA instead of an image tag). It
builds the bundle inside a Dockerized Node container — the host itself never needs Node or
npm installed — then restarts only the <code>gittensory</code> service the same way as the
npm installed — then restarts only the <code>loopover</code> service the same way as the
image path. <code>SENTRY_RELEASE</code> defaults to{" "}
<code>gittensory-selfhost@&lt;short git SHA of the current HEAD&gt;</code> unless you
override it, so each deploy from a new commit gets a distinct release id automatically. When{" "}
Expand All @@ -1062,7 +1062,7 @@ git merge --ff-only origin/main
<code>curl http://localhost:8787/ready</code> returns HTTP 200.
</li>
<li>
<code>docker compose ps gittensory</code> shows <code>healthy</code>.
<code>docker compose ps loopover</code> shows <code>healthy</code>.
</li>
<li>
Tail logs for <code>selfhost_listening</code> and, on first boot after a schema bump,{" "}
Expand All @@ -1078,9 +1078,9 @@ git merge --ff-only origin/main
code={`./scripts/selfhost-post-update-check.sh
# equivalent manual checks:
curl -sf http://localhost:8787/ready
docker compose ps gittensory
docker compose ps loopover
grep -E '^(GITTENSORY_IMAGE|GITTENSORY_VERSION|SENTRY_RELEASE)=' .env
docker inspect --format '{{.Config.Image}}' "$(docker compose ps -q gittensory)"`}
docker inspect --format '{{.Config.Image}}' "$(docker compose ps -q loopover)"`}
/>
<p>
If any check fails, see <Link to="/docs/self-hosting-troubleshooting">Troubleshooting</Link>
Expand Down Expand Up @@ -1147,8 +1147,8 @@ docker inspect --format '{{.Config.Image}}' "$(docker compose ps -q gittensory)"
<p>
Stopping the container does not delete anything — <code>docker compose stop</code> or{" "}
<code>docker compose down</code> (without <code>-v</code>) leaves every named volume (
<code>gittensory-data</code>, <code>gittensory-pg</code>, <code>qdrant-data</code>,{" "}
<code>gittensory-backups</code>, <code>grafana-data</code>, and the rest declared in{" "}
<code>loopover-data</code>, <code>loopover-pg</code>, <code>qdrant-data</code>,{" "}
<code>loopover-backups</code>, <code>grafana-data</code>, and the rest declared in{" "}
<code>docker-compose.yml</code>) on disk, along with the <code>./gittensory-config</code>{" "}
host directory (a bind mount, not a named volume, so it is never affected by <code>-v</code>{" "}
either way). Pick one:
Expand All @@ -1168,7 +1168,7 @@ docker inspect --format '{{.Config.Image}}' "$(docker compose ps -q gittensory)"
{
title: "Delete everything",
description:
"docker compose down -v removes every named volume permanently — the review database, vector index, Grafana dashboards state, and any local backup archives in gittensory-backups go with it. This does not touch ./gittensory-config (delete that host directory yourself if it should go too).",
"docker compose down -v removes every named volume permanently — the review database, vector index, Grafana dashboards state, and any local backup archives in loopover-backups go with it. This does not touch ./gittensory-config (delete that host directory yourself if it should go too).",
},
]}
/>
Expand Down Expand Up @@ -1215,13 +1215,13 @@ docker inspect --format '{{.Config.Image}}' "$(docker compose ps -q gittensory)"
request (<code>isAuthorizedGitHubSessionLogin</code> in <code>src/auth/security.ts</code>) —
it is never cached at startup or baked into an issued session. To remove someone&apos;s
operator access, delete their login from the comma/whitespace-separated list in{" "}
<code>.env</code> and restart the <code>gittensory</code> service so the process picks up
the new value:
<code>.env</code> and restart the <code>loopover</code> service so the process picks up the
new value:
</p>
<CodeBlock
lang="bash"
code={`$EDITOR .env # remove the login from ADMIN_GITHUB_LOGINS
docker compose up -d --no-deps gittensory`}
docker compose up -d --no-deps loopover`}
/>
<p>
This takes effect on their very next control-panel request after the restart — no signed-in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ function SelfHostingSecurity() {
filename="shell"
code={`./scripts/selfhost-init-secrets.sh # creates empty placeholder files (idempotent)
printf '%s' 'your-real-secret-value' > secrets/github_webhook_secret.txt
docker compose up -d --no-deps gittensory`}
docker compose up -d --no-deps loopover`}
/>

<h2>Private policy</h2>
Expand Down
6 changes: 3 additions & 3 deletions caddy/Caddyfile
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# Caddy reverse proxy for gittensory (#980 self-host).
# Caddy reverse proxy for loopover (#980 self-host).
# Activated via: docker compose --profile caddy up
#
# DOMAIN is injected from the DOMAIN env var in docker-compose.yml.
# Set DOMAIN=reviews.yourcompany.com in .env — Caddy fetches a TLS cert from Let's Encrypt automatically.
# For local testing without a domain, set DOMAIN=localhost (self-signed cert, browser will warn).
#
# When using this profile, remove the `ports:` entry from the gittensory service in docker-compose.yml
# When using this profile, remove the `ports:` entry from the loopover service in docker-compose.yml
# so port 8787 is NOT exposed publicly — all traffic should flow through Caddy on 443.

{$DOMAIN} {
reverse_proxy gittensory:8787 {
reverse_proxy loopover:8787 {
# Surface the real client IP to the app (logged in access events).
header_up X-Forwarded-For {remote_host}
header_up X-Real-IP {remote_host}
Expand Down
Loading
Loading