> ## Documentation Index
> Fetch the complete documentation index at: https://www.towbar.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# linkding

> linkding saves and organizes web bookmarks in a simple self-hosted library.

export const UseCaseNavigation = () => {
  useLayoutEffect(() => {
    const storageKey = "towbar-use-case-open-groups";
    const groupSelector = "#sidebar li[data-title] > button[aria-expanded]";
    let restoring = false;
    let frame = 0;
    function storedGroups() {
      try {
        return new Set(JSON.parse(sessionStorage.getItem(storageKey) || "[]"));
      } catch {
        return new Set();
      }
    }
    function save(groups) {
      try {
        sessionStorage.setItem(storageKey, JSON.stringify([...groups]));
      } catch {}
    }
    function groupName(button) {
      return button.parentElement?.dataset.title;
    }
    function rememberOpenGroups() {
      const groups = new Set();
      for (const button of document.querySelectorAll(groupSelector)) {
        if (button.getAttribute("aria-expanded") === "true") {
          const name = groupName(button);
          if (name) groups.add(name);
        }
      }
      save(groups);
    }
    function restoreOpenGroups() {
      frame = 0;
      const groups = storedGroups();
      if (!groups.size) return;
      restoring = true;
      for (const button of document.querySelectorAll(groupSelector)) {
        if (groups.has(groupName(button)) && button.getAttribute("aria-expanded") === "false") {
          button.click();
        }
      }
      restoring = false;
    }
    function scheduleRestore() {
      if (!frame) frame = requestAnimationFrame(restoreOpenGroups);
    }
    function onClick(event) {
      if (restoring || !(event.target instanceof Element)) return;
      const button = event.target.closest(groupSelector);
      if (button) {
        const name = groupName(button);
        if (!name) return;
        const groups = storedGroups();
        if (button.getAttribute("aria-expanded") === "true") groups.delete(name); else groups.add(name);
        save(groups);
        return;
      }
      if (event.target.closest('a[href^="/docs/use-cases/"]')) {
        rememberOpenGroups();
      }
    }
    const observer = new MutationObserver(scheduleRestore);
    document.addEventListener("click", onClick, true);
    observer.observe(document.getElementById("sidebar") || document.body, {
      childList: true,
      subtree: true
    });
    scheduleRestore();
    return () => {
      document.removeEventListener("click", onClick, true);
      observer.disconnect();
      cancelAnimationFrame(frame);
    };
  }, []);
  return null;
};

<UseCaseNavigation />

**Upstream source:** [Current installation guide](https://github.com/sissbruecker/linkding#installation).

linkding uses SQLite by default. Its database and generated signing key live under `/etc/linkding/data`, a mount path Towbar's single-Service manifest does not accept. A named Compose volume preserves both without introducing PostgreSQL.

## Towbar manifest

```yaml title=".towbar/services/linkding.compose.yml" theme={"system"}
id: linkding
name: linkding
file: deploy/linkding/compose.yml
strategy: maintenance
services:
  linkding:
    port: 9090
    domains:
      - linkding.example.com
    ingress:
      type: proxy
environments:
  production:
    server: 192.0.2.10
secrets:
  runtime:
    - LD_SUPERUSER_NAME
    - LD_SUPERUSER_PASSWORD
    - LD_CSRF_TRUSTED_ORIGINS
```

## Compose project

```yaml title="deploy/linkding/compose.yml" theme={"system"}
services:
  linkding:
    image: sissbruecker/linkding:1.42.0@sha256:12ffd6f3b48c5d46543d2f38030de1f476d8dcff5f486eb75c9c7cb5941e7127
    environment:
      LD_SUPERUSER_NAME: ${LD_SUPERUSER_NAME}
      LD_SUPERUSER_PASSWORD: ${LD_SUPERUSER_PASSWORD}
      LD_CSRF_TRUSTED_ORIGINS: ${LD_CSRF_TRUSTED_ORIGINS}
    volumes:
      - data:/etc/linkding/data
volumes:
  data: {}
```

## Configure

1. Prepare the example server, connect the repository, and map `production` to the branch containing these files. Replace the example server IP and domain.
2. Commit every file shown below under `deploy/linkding/`. The Towbar manifest points to the Compose file; it does not create it for you.
3. Sync the repository and inspect the resolved Compose project. Save the runtime values below if this example declares any.
4. Save the three runtime values before deploying. The initial administrator variables create a user only if that username does not already exist.
5. Back up the `data` volume; it holds bookmarks, archived pages, and the signing key.
6. Deploy it manually and run the verification below before enabling auto-deploy.

## Runtime values

Save these values on the Compose project after the repository sync. The manifest declares required keys, not their values.

| Key                       | Value to save                                                |
| ------------------------- | ------------------------------------------------------------ |
| `LD_SUPERUSER_NAME`       | Choose the first administrator username.                     |
| `LD_SUPERUSER_PASSWORD`   | Generate a long first administrator password.                |
| `LD_CSRF_TRUSTED_ORIGINS` | [https://linkding.example.com](https://linkding.example.com) |

## Verify

Sign in, add a bookmark, redeploy, and verify the account and bookmark still work.

For field constraints, see [Compose guide](/docs/services/modes/compose).
