+++
title = "New features for my blog! Adding Comments + RSS to Hugo PaperMod (Giscus and Isso FTW)"
description = "A friendly, copy-pasteable guide to wiring up Giscus comments (GitHub Discussions) and Isso comments + RSS in Hugo PaperMod."
date = 2025-10-23T19:31:12+02:00
draft = false
slug = "comments-rss-papermod"
ShowToc = true
TocOpen = true
tags = ["hugo", "papermod", "giscus", "rss", "comments"]
categories = ["blog", "meta"]
+++

> Short story: I wanted comments and a proper RSS feed on my Hugo + PaperMod site.  
> Longer story: I spent 30 minutes poking around, and now you don’t have to. Here’s exactly what I did.

---

I've been wanting to add comment section to my blog for a while. 
There are a few problems. 
- I don't want to get attacked by bots or random people for no reason. 
- I don't want to allow weird things to happen. 
- I don't want to have a database for it. 

So... I decided to take a path which is not necesserially the best. 
It requires a third party to act on your behalf, but it is not as scary as it sounds. 
According to Github:

> **What the app can do**
> The app’s rights are limited to the repo(s) where the site owner installed it and to the permissions it requested (basically: Discussions read/write + metadata). It can’t touch a commenter’s private repos or do things outside those scopes. GitHub’s docs also say a GitHub App can only do what both you and the app have permission to do. 

> **What “Act on your behalf” means**
> That scary wording is GitHub’s. The giscus maintainer explains it means “post comments under your username,” and shows that the app requests Discussions read/write + metadata when installed. 

> **Do commenters have alternatives?**
> Yes. Anyone can open the linked GitHub Discussion and comment directly on GitHub, bypassing the app’s OAuth popup. The README explicitly mentions this. 

> **Privacy & storage**
> No tracking/ads; comments live in GitHub Discussions. giscus is open-source and has a published privacy policy. You can also revoke the app any time in your GitHub settings.

With the notice out of the way let's dive right in!

---

## Why I chose Giscus (instead of Disqus)

- **No ads or trackers** (my page weight thanks me).
- **Uses GitHub Discussions** (portable, transparent, easy to search).
- **Readers log in with GitHub** (no mystery auth popups).
- In theory you can directly go to the repo's discussion section and comment there!

---

## What you need (for your site, not per user)

- A Hugo site using **PaperMod** ✅  
- A GitHub account ✅  
- A **public** repository for the discussion threads (I called mine `blog-comments`) with **Discussions** enabled ✅  
- Giscus app installed on that repo ✅

If you like to keep your main site repo clean, use a separate one, e.g. `github-username/blog-comments`.

---

## 1) Turn on comments with Giscus

PaperMod renders a `comments.html` partial when `params.comments = true`.  
So we enable it in config and create the partial.

### 1.1 Enable comments globally

`hugo.toml` (snippet)
```toml
[params]
  comments = true
```

You can still opt out per post with `comments = false` in front matter.

### 1.2 Create the comments partial

Create this file **in your site** (not inside the theme):
```
layouts/partials/comments.html
```

Paste the Giscus embed (swap the placeholders with yours from giscus.app):

```html
<section id="comments" class="giscus">
  <script
    src="https://giscus.app/client.js"
    data-repo="yourname/blog-comments"
    data-repo-id="YOUR_REPO_ID"
    data-category="Comments"
    data-category-id="YOUR_CATEGORY_ID" <!-- Get them from Giscus's page! -->
    data-mapping="pathname"
    data-strict="0"
    data-reactions-enabled="1"
    data-emit-metadata="0"
    data-input-position="bottom"
    data-theme="preferred_color_scheme"
    data-lang="en"
    data-loading="lazy"
    crossorigin="anonymous"
    async>
  </script>
  <noscript>Enable JavaScript to view comments powered by Giscus.</noscript>
</section>
```

**Notes**
- You’ll get `repo-id` and `category-id` from **giscus.app** after picking your repo + category.
- `data-mapping="pathname"` works great for static sites.
- Prefer a **public** repo so visitors can read discussions without friction.
- Want a different language? Change `data-lang` (e.g., `de`, `fr`).

---

## 2) Enable RSS (home + sections)

Hugo already knows how to generate feeds; we just tell it where.

`hugo.toml`
```toml
baseURL = "https://example.com/"  # set your production URL

[outputs]
  home = ["HTML", "RSS", "JSON"]     # JSON is optional (e.g., for on-site search)
  section = ["HTML", "RSS"]
  taxonomy = ["HTML", "RSS"]
  term = ["HTML", "RSS"]

[services.rss]
  limit = -1   # -1 = no cap; or set e.g. 20
```

**Feed URLs**
- Home feed: `/index.xml` → `https://example.com/index.xml`
- Section feeds: `/posts/index.xml`, `/notes/index.xml`, etc. (folders are lowercase)

> If you’re using a `.onion` address or staging domain, just set that in `baseURL`.  
> Locally, Hugo serves feeds at `http://localhost:1313/index.xml`.

---

## 3) Show an RSS button on the home page

PaperMod shows a nice RSS icon on **section/taxonomy** lists by default (when enabled), but not on the home page. Two options:

### Option A — Use the home “social icons” row
This appears if you enable **Home-Info** or **Profile** mode:

```toml
[params.homeInfoParams]
  Title = "Hello, internet!"
  Content = "Notes, experiments, and occasional rabbit holes."

[[params.socialIcons]]
  name = "rss"        # lowercase matters
  url  = "/index.xml"
```

### Option B — Add an icon in the footer (theme-safe)
Create:
```
layouts/partials/extend_footer.html
```

Paste:
```html
<a href="/index.xml" rel="alternate" type="application/rss+xml" title="RSS" class="rss-link">
  {{ partial "svg.html" (dict "name" "rss") }}
  <span>RSS</span>
</a>
```

Make it a sensible size:

Create:
```
assets/css/extended/rss.css
```

Paste:
```css
/* Small & aligned RSS icon in the footer */
.rss-link { display: inline-flex; align-items: center; gap: .35rem; }
.rss-link svg { margin-left: 10px; width: 18px; height: 18px; } /* a bit of left margin for good measure!*/
.rss-link span { font-size: .95rem; }
```

PaperMod auto-bundles anything under `assets/css/extended/`, so no extra imports needed.

---

## 4) Verify everything

Build or run dev:
```bash
hugo           # outputs to ./public
# or
hugo server    # serves at http://localhost:1313
```

Check feeds:
```bash
# Home feed:
curl -s http://localhost:1313/index.xml | head

# A section feed (adjust path to your section name):
curl -s http://localhost:1313/posts/index.xml | head
```

Check comments:
1. Open any post locally.
2. Scroll to the bottom — Giscus should appear.
3. Try a reaction or comment (you’ll be prompted to sign in with GitHub).

---

## 5) Per-post controls (handy later)

Turn comments off for a single post:
```toml
# in that post’s front matter
comments = false
```

Full content in RSS (global):
```toml
[params]
  ShowFullTextinRSS = true
```

Show RSS buttons on section/taxonomy lists:
```toml
[params]
  ShowRssButtonInSectionTermList = true
```

---

## 6) Troubleshooting (aka “what I bumped into”)

- **Home page has no RSS icon**  
  That’s expected; either enable Home-Info/Profile mode + `socialIcons`, or use the footer partial.

- **Section feed 404**  
  Folder names are lowercase (`content/posts/`, not `content/Posts/`).  
  The URL mirrors that: `/posts/index.xml`.

- **Giscus doesn’t create a discussion**  
  Double-check the four attributes: `data-repo`, `data-repo-id`, `data-category`, `data-category-id`.  
  Make sure Discussions is enabled and the Giscus app is installed for the repo.

- **Icon is comically large**  
  Keep the CSS above; 16–20px usually looks right.

---

## 7) Bonus: fast setup via GitHub CLI (optional, I didn't use it, but I should've perhaps)

```bash
# Create the public comments repo (adjust names)
gh repo create yourname/blog-comments --public -d "Blog comment threads" --confirm

# Enable Discussions for it
gh repo edit yourname/blog-comments --enable-discussions
```

Then visit **giscus.app**, select the repo + category (e.g. “Comments”), and copy the generated IDs into `layouts/partials/comments.html`.

---

### Final config snapshot (condensed)

```toml
baseURL = "https://example.com/"
theme = "PaperMod"

[outputs]
  home = ["HTML", "RSS", "JSON"]
  section = ["HTML", "RSS"]
  taxonomy = ["HTML", "RSS"]
  term = ["HTML", "RSS"]

[services.rss]
  limit = -1

[params]
  comments = true
  ShowRssButtonInSectionTermList = true
  ShowFullTextinRSS = true

# Optional if using Home-Info/Profile mode:
# [[params.socialIcons]]
#   name = "rss"
#   url  = "/index.xml"
```
---

# Appendix — Why I added Isso, how I wired it, and what I fixed

## The story (why Isso + why I still keep Giscus)

I wanted comments that:
- allow **anonymous** replies (no GitHub account required),
- are **lightweight** and **self-hosted**, and
- can work on my **Tor `.onion` mirror**.

**Isso** checks those boxes: it’s a tiny Python app with SQLite storage, no trackers, and a simple embed. At the same time, I like the developer-friendly workflow of **Giscus** (comments are GitHub Discussions), so I kept both and added a little switcher UI: **“Anonymous (Isso)”** or **“GitHub (Giscus)”**.

Result: readers can pick privacy/anon (Isso) or identity/notifications (Giscus).


## How I run it (high-level)

- **Site:** Hugo + PaperMod  
- **Isso:** listening on `127.0.0.1:8080`, stored locally (SQLite)  
- **Nginx:** reverse-proxies **`/isso/`** to Isso on both the clearnet and the `.onion` vhost  
- **Hugo partial:** renders the two panels (Isso & Giscus) and a switcher; Giscus is **lazy-loaded** and **auto-rethemed** with the site theme

> I’m deliberately keeping Isso’s own config tiny in this appendix; the important part for my setup is the **Nginx proxying** and the **Hugo partial**.


## Hugo partial: switcher + embeds

Save as `layouts/partials/comments.html` and include it in your single layout (e.g. `layouts/_default/single.html`) with `{{ partial "comments.html" . }}`.

```html
<!-- comments.html -->
<div class="comments-switch" style="display:flex;gap:.5rem;margin:.5rem 0 1rem;">
  <button id="btn-isso" type="button" aria-pressed="true">Anonymous (Isso)</button>
  <button id="btn-giscus" type="button" aria-pressed="false">GitHub (Giscus)</button>
  <a href="https://github.com/AlipourIm/blog-comments/discussions/categories/comments" target="_blank" rel="noopener">Open on GitHub ↗</a>
</div>

<!-- Isso panel (default) -->
<div id="panel-isso">
  <section id="isso-thread"></section>
  <script
    src="/isso/js/embed.min.js"
    data-isso="/isso"
    data-isso-css="true"
    data-isso-lang="en"
    data-isso-max-comments-nested="5"
    data-isso-sorting="newest"
    async>
  </script>
  <small class="isso-powered" style="display:block;margin:.5rem 0;color:var(--secondary,#888)">
    Comments powered by <a href="https://isso-comments.de" target="_blank" rel="noopener">Isso</a>
  </small>
</div>

<!-- Giscus panel (lazy-loaded on click) -->
<div id="panel-giscus" style="display:none">
  <section id="giscus-thread"></section>
</div>

<script>
(function () {
  const panelIsso = document.getElementById('panel-isso');
  const panelGisc = document.getElementById('panel-giscus');
  const issoBtn = document.getElementById('btn-isso');
  const giscBtn = document.getElementById('btn-giscus');
  let gLoaded = false;

  function isDark() {
    // 1) explicit PaperMod preference
    const pref = localStorage.getItem('pref-theme');
    if (pref === 'dark')  return true;
    if (pref === 'light') return false;

    // 2) page state (classes / data-theme)
    const H = document.documentElement, B = document.body;
    const hasDark =
      H.classList.contains('dark') || B.classList.contains('dark') ||
      H.dataset.theme === 'dark'    || B.dataset.theme === 'dark' ||
      H.classList.contains('theme-dark') || B.classList.contains('theme-dark');
    if (hasDark) return true;

    // 3) OS preference
    return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
  }
  const gTheme = () => (isDark() ? 'dark_dimmed' : 'noborder_light');

  function loadGiscus() {
    if (gLoaded) return;
    const s = document.createElement('script');
    s.src = 'https://giscus.app/client.js';
    s.async = true;
    s.crossOrigin = 'anonymous';
    s.setAttribute('data-repo','AlipourIm/blog-comments');
    s.setAttribute('data-repo-id','R_kgDOQGARyA');
    s.setAttribute('data-category','Comments');
    s.setAttribute('data-category-id','DIC_kwDOQGARyM4Cw3x-');
    s.setAttribute('data-mapping','pathname');
    s.setAttribute('data-strict','0');
    s.setAttribute('data-reactions-enabled','1');
    s.setAttribute('data-emit-metadata','0');
    s.setAttribute('data-input-position','bottom');
    s.setAttribute('data-lang','en');
    s.setAttribute('data-theme', gTheme());  // initial theme
    document.getElementById('giscus-thread').appendChild(s);

    // Retheme once iframe exists
    const reTheme = () => {
      const iframe = document.querySelector('iframe.giscus-frame');
      if (!iframe) return;
      iframe.contentWindow?.postMessage(
        { giscus: { setConfig: { theme: gTheme() } } },
        'https://giscus.app'
      );
    };

    // PaperMod toggle button
    document.getElementById('theme-toggle')?.addEventListener('click', () =>
      setTimeout(reTheme, 0)
    );

    // Also react to attribute changes
    const mo = new MutationObserver(reTheme);
    mo.observe(document.documentElement, { attributes: true, attributeFilter: ['class','data-theme'] });
    mo.observe(document.body,             { attributes: true, attributeFilter: ['class','data-theme'] });

    gLoaded = true;
  }

  function show(which) {
    const showIsso = which === 'isso';
    panelIsso.style.display = showIsso ? 'block' : 'none';
    panelGisc.style.display = showIsso ? 'none'  : 'block';
    issoBtn?.setAttribute('aria-pressed', showIsso);
    giscBtn?.setAttribute('aria-pressed', !showIsso);
    if (!showIsso) loadGiscus();
  }

  issoBtn?.addEventListener('click', e => { e.preventDefault(); show('isso'); });
  giscBtn?.addEventListener('click', e => { e.preventDefault(); show('giscus'); });

  // default
  show('isso');
})();
</script>
```


## Nginx: proxy Isso on **both** sites (and avoid the 404-onion trap)

The bug I hit: my `.onion` server had a regex static block that **won over** the plain `/isso/` location, so `/isso/js/embed.min.js` returned **404**.

**Fix:** make `/isso/` a `^~` prefix location so it beats regex, and place it **above** the static block.

```nginx
# /etc/nginx/sites-enabled/onion-blog  (same idea for the clearnet vhost)
server {
    listen 127.0.0.1:3301 default_server;
    server_name fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion 127.0.0.1 localhost;

    add_header Referrer-Policy no-referrer always;
    add_header X-Content-Type-Options nosniff always;
    add_header X-Frame-Options SAMEORIGIN always;

    root /srv/hugo/mysite/public-onion;
    index index.html;

    location / { try_files $uri $uri/ /index.html; }

    # /isso -> /isso/
    location = /isso { return 301 /isso/; }

    # IMPORTANT: this must beat regex locations below
    location ^~ /isso/ {
        proxy_pass http://127.0.0.1:8080/;    # Isso container/port
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_redirect off;
        proxy_read_timeout 60s;
    }

    # static
    location ~* \\.(css|js|ico|png|jpg|jpeg|gif|svg|webp|txt|xml)$ {
        access_log off;
        add_header Cache-Control "public, max-age=31536000, immutable";
        try_files $uri =404;
    }
}
```

Reload and smoke test:

```bash
sudo nginx -t && sudo systemctl reload nginx
curl -I http://<your-onion-host>/isso/js/embed.min.js   # expect 200
```

> If you ever point `data-isso` to a **different origin**, add CORS headers on that vhost. For same-origin (`data-isso="/isso"`), you don’t need CORS.


## Small CSS tweaks (one color across light/dark + spacing)

I wanted the same blue for **reply / edit / delete** links and for **Submit / Preview** buttons, in both themes, and a little spacing:

```css
/* put this in your site CSS */
:root { --btn-blue: #2563eb; } /* adjust to taste */

/* Isso form buttons */
.isso-post-action input[type="submit"],
.isso-post-action input[name="preview"] {
  background: var(--btn-blue) !important;
  color: #fff !important;
  border: none;
  padding: .5rem .75rem;
  border-radius: .4rem;
  cursor: pointer;
}

/* link actions under comments */
.isso-comment-footer a {
  color: var(--btn-blue) !important;
  margin-right: .5rem;       /* gives "edit" and "delete" some breathing room */
  text-decoration: none;
}
.isso-comment-footer a:hover { text-decoration: underline; }

/* keep name/email/website stacked, slightly larger */
.isso-auth-section .isso-input-wrapper {
  display: block;
  margin: .35rem 0;
}
.isso-auth-section label { font-size: .95rem; }
.isso-auth-section input[type="text"],
.isso-auth-section input[type="email"],
.isso-auth-section input[type="url"] {
  width: 100%;
  font-size: 1rem;
  padding: .5rem .6rem;
}
```


## What changed (the fixes I actually made)

1. **Theme correctness for Giscus.**  
   PaperMod doesn’t add `.light`—it only toggles `.dark` and stores `pref-theme`. I now:
   - read `localStorage.pref-theme` first,
   - fall back to class/data-theme, then
   - fall back to OS preference.  
   I also re-theme the Giscus iframe on every toggle via `postMessage`.

2. **Isso over Tor.**  
   On the `.onion` vhost, I moved the proxy to a `^~ /isso/` block so it beats the static regex. That fixed the `404` on `/isso/js/embed.min.js`.

3. **UI polish.**  
   - Unified button/link color across themes,  
   - ensured “edit”/“delete” aren’t touching,  
   - kept the author/email/website inputs **stacked**, and  
   - left **Submit**/**Preview** styled and grouped.


## Sanity check

- Toggle the theme: Giscus switches between `noborder_light` and `dark_dimmed` without reload.  
- Open DevTools → Network on onion: `/isso/js/embed.min.js` loads with **200**.  
- Switcher buttons swap panels instantly; Giscus only loads when clicked.



## 8) Final notes!
I could not fully fix the style of Isso. 
CSS is not easy... drat! 

I don't think anyone actually reads the blog, let alone comment on it. 
But it felt cool to have a comment section and also RSS for it. 
I had a lot of not so fun debugging experience, and will probably work on the styling to make it look right, but for now I'm happy with it! ^^


# Admin note — Delete all **Isso** comments for a single post

This note is for future me: how to remove every comment tied to one post in **Isso**.  
Tested with SQLite-backed Isso (default). Works whether you serve via clearnet or `.onion` — because this operates directly on the database.

---

## TL;DR (host with SQLite)

> Replace the placeholders for `DB` and `POST_URI` first.

```bash
# 0) Variables — edit these two lines for your setup
DB="/var/lib/isso/comments.db"          # path to your Isso SQLite DB (check isso.conf: [general] dbpath)
POST_URI="/posts/skin_routine/"         # the post's URI as stored by Isso (often just the path)

# 1) Backup (always do this!)
sqlite3 "$DB" ".backup '$(dirname "$DB")/comments.$(date +%F-%H%M%S).backup.sqlite3'"

# 2) Inspect: find the thread row and preview the comments
sqlite3 "$DB" "
.headers on
.mode column
SELECT id, uri FROM threads WHERE uri LIKE '%' || '$POST_URI' || '%';
SELECT id, tid, created, author, text
FROM comments
WHERE tid IN (SELECT id FROM threads WHERE uri LIKE '%' || '$POST_URI' || '%')
ORDER BY created DESC
LIMIT 25;
"

# 3) Delete all comments for that post (thread)
sqlite3 "$DB" "
DELETE FROM comments
WHERE tid IN (SELECT id FROM threads WHERE uri LIKE '%' || '$POST_URI' || '%');
"

# 4) (Optional) Remove the now-empty thread row too
sqlite3 "$DB" "
DELETE FROM threads WHERE uri LIKE '%' || '$POST_URI' || '%';
VACUUM;
"

# 5) Done. Isso usually picks this up automatically.
#    If you're containerized and want to be safe:
# docker restart <isso_container_name>
```

---

## If running Isso in Docker

Open a shell in the container, then run the same steps:

```bash
docker exec -it <isso_container_name> /bin/sh

# Inside the container:
DB="/db/comments.db"    # or /data/comments.db, check your image/volume mapping
POST_URI="/posts/skin_routine/"

sqlite3 "$DB" ".backup '/db/comments.$(date +%F-%H%M%S).backup.sqlite3'"
sqlite3 "$DB" "SELECT id, uri FROM threads WHERE uri LIKE '%' || '$POST_URI' || '%';"
sqlite3 "$DB" "DELETE FROM comments WHERE tid IN (SELECT id FROM threads WHERE uri LIKE '%' || '$POST_URI' || '%');"
sqlite3 "$DB" "DELETE FROM threads WHERE uri LIKE '%' || '$POST_URI' || '%'; VACUUM;"
exit

# Optionally restart the container:
docker restart <isso_container_name>
```

---

## Notes & gotchas

- **Find the real DB path**: In `isso.conf` (often `/etc/isso/isso.conf` or within your container at `/config/isso.conf`), look for:
  ```ini
  [general]
  dbpath = /var/lib/isso/comments.db
  ```
- **What goes in `POST_URI`?** Isso stores a `uri` string per thread. With the default embed config, it's usually the **path** (`/posts/<slug>/`). If you’re unsure, list recent threads:
  ```bash
  sqlite3 "$DB" ".headers on" ".mode column" "SELECT id, uri FROM threads ORDER BY id DESC LIMIT 50;"
  ```
- **Clearnet vs `.onion`**: If you load the same post under multiple origins _and_ your embed setup stores full origins, you may have **multiple thread rows**. The `LIKE` query above deliberately matches any uri containing your `POST_URI` to catch those.
- **Rollback**: To undo, stop Isso (optional), replace the DB with your backup (`comments.YYYY-MM-DD-HHMMSS.backup.sqlite3`), and start Isso again.
- **HTTP API option**: Isso supports `DELETE /<comment_id>` if admin endpoints/auth are enabled. For bulk removal it’s simpler and safer to use SQLite directly.
- **Safety**: Always run a backup first, and **preview with the SELECT** before you run the DELETE.

---

## One-liners for the impatient

```bash
DB="/var/lib/isso/comments.db"; POST_URI="/posts/skin_routine/"
sqlite3 "$DB" ".backup '$(dirname "$DB")/comments.$(date +%F-%H%M%S).backup.sqlite3'"
sqlite3 "$DB" "DELETE FROM comments WHERE tid IN (SELECT id FROM threads WHERE uri LIKE '%' || '$POST_URI' || '%');"
sqlite3 "$DB" "DELETE FROM threads  WHERE uri LIKE '%' || '$POST_URI' || '%'; VACUUM;"
```

---

**Isso container: quick checks & fixes for future me! (thank past me later ^^)**

**Find the container**
```bash
docker ps -a --format 'table {{.ID}}	{{.Names}}	{{.Status}}	{{.Ports}}' | grep -i isso
```

**Inspect basic state**
```bash
docker inspect -f 'Name={{.Name}}  Running={{.State.Running}}  Status={{.State.Status}}  StartedAt={{.State.StartedAt}}  FinishedAt={{.State.FinishedAt}}' isso
```

**Logs**
```bash
docker logs --tail=200 isso
docker logs -f isso
```

**Ports & reachability**
```bash
docker port isso
sudo ss -ltnp | grep ':8080 '
curl -i http://127.0.0.1:8080/
```

**Restart policy (keep it running)**
```bash
docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' isso
docker update --restart unless-stopped isso
```

**Start/stop the container**
```bash
docker start isso
docker stop isso
```

**Who/what stopped it (events)**
```bash
docker events --since 24h --filter container=isso
```

**Common config issue: duplicate options/sections**
If logs show DuplicateOptionError or DuplicateSectionError, fix `/config/isso.cfg` (remove duplicate `[server]` blocks or duplicate `public-endpoint`).

```bash
nl -ba /config/isso.cfg | sed -n '1,120p'
grep -n '^\[server\]' /config/isso.cfg
grep -n '^public-endpoint' /config/isso.cfg
```

**Using Docker Compose** *(run these in the directory with your compose file)*
```bash
docker compose ps
docker compose logs --tail=200 isso
docker compose up -d isso
```

**If the container doesn’t exist (create/recreate)**
```bash
docker image ls | grep -i isso

docker run -d   --name isso   -p 8080:8080   -v /srv/isso/config:/config   -v /srv/isso/db:/db   --restart unless-stopped   ghcr.io/posativ/isso:latest
```

**Optional: exclude from auto-updaters (like Watchtower)**
```bash
docker update --label-add com.centurylinklabs.watchtower.enable=false isso
```


_This note is intentionally self-contained so I can paste it at the end of the blog post as a maintainer-only appendix._

---

{{< sigdl >}}
