> ## 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.

# Umami analytics

> Umami image Service + postgres Datastore; DATABASE_URL is a runtime secret.

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 />

<img className="towbar-doc-brand-logo" src="https://mintcdn.com/avgeek/Wq-9tmR7jDSzqtpH/assets/use-case-logos/umami.svg?fit=max&auto=format&n=Wq-9tmR7jDSzqtpH&q=85&s=1414f37a5c075c5ce427da04879a8c10" alt="Umami analytics logo" aria-hidden="true" width="24" height="24" data-path="assets/use-case-logos/umami.svg" />

**Related reading:** [Umami upstream](https://docs.umami.is/docs/install).

These are separate files in one repository. A shared Docker network works only when the workloads run on the same server; Towbar does not automatically order their deployments or copy secret values between them.

## Towbar manifests

<CodeGroup>
  ```yaml title=".towbar/datastores/umami-postgres.datastore.yml" theme={"system"}
  id: umami-postgres
  name: Umami PostgreSQL
  type: postgres
  container:
    network: application
    networkAlias: umami-postgres
    resources:
      cpus: 1
      memory: 1g
  secrets:
    runtime:
      - POSTGRES_USER
      - POSTGRES_DB
      - POSTGRES_PASSWORD
  environments:
    production:
      server: 192.0.2.10
  ```

  ```yaml title=".towbar/services/umami.service.yml" theme={"system"}
  id: umami
  name: Umami
  buildServer: null
  deployment:
    type: image
    image: ghcr.io/umami-software/umami:3.4@sha256:6cd9d24a836fac5c226c3c90cb25141d4560b7270b7827f393ccf09bf3f83b18
  container:
    port: 3000
    network: application
  environments:
    production:
      server: 192.0.2.10
  secrets:
    runtime:
      - DATABASE_URL
  domains:
    primary: umami.example.com
  tls:
    mode: direct
  ```
</CodeGroup>

## Configure

1. Register and prepare the example server or servers, connect the repository, and map `production` to the branch containing these files.
2. Save each Datastore credential value under **Datastore → Settings → Secrets**, deploy it first, and verify engine readiness before starting a dependent Service.
3. Save the Service's declared keys in its selected environment, or enable and authorize the selected [external-secret provider](/docs/secrets/external). Do not commit the values.
4. Set the Service's `DATABASE_URL` to `postgresql://<user>:<password>@umami-postgres:5432/<database>`, using the user, name, and password you configured on the Datastore. A Docker network does not copy those values. Change Umami's default administrator password after first login.
5. Sync the repository, inspect the resolved configuration, deploy manually, and perform the verification below before enabling automation.

## Verify

Use current PostgreSQL guidance and verify the site after first login.

For field constraints, see [Service manifest](/docs/services/manifest) and [Datastore manifest](/docs/datastores/manifest).
