chore: add update guide

This commit is contained in:
Abhishek Kumar 2026-04-21 08:49:48 +05:30
parent 6606a7f901
commit 330b81d908
4 changed files with 275 additions and 10 deletions

113
docs/deployment/update.mdx Normal file
View file

@ -0,0 +1,113 @@
---
title: "Update"
description: "Update your self-hosted Dograh stack to a newer image version"
---
This guide covers updating a Dograh stack you've already deployed with [Docker](/deployment/docker) or a [custom domain](/deployment/custom-domain). You run commands from the same directory that contains your `docker-compose.yaml` (this is the `dograh/` directory if you used `setup_remote.sh`).
## Find an image version
Dograh publishes two images — `dograh-api` and `dograh-ui` — to both container registries:
- **GitHub Container Registry** — [github.com/orgs/dograh-hq/packages](https://github.com/orgs/dograh-hq/packages)
- **Docker Hub** — [hub.docker.com/u/dograhai](https://hub.docker.com/u/dograhai)
Each release is published under two kinds of tags:
| Tag style | Example | When to use |
|-----------|---------|-------------|
| **Release tag** | `v0.8.2` | Stable, recommended for production |
| **Git commit SHA** | `a1b2c3d` | Bleeding edge — any commit merged to `main` |
| `latest` | `latest` | Tracks the most recent release tag |
<Warning>
Always update **`dograh-api`** and **`dograh-ui`** to the **same tag**. The two images are built from the same commit and the UI expects API responses in a matching shape — mixing versions will break the app.
</Warning>
## Option A: Update to the latest release
If your `docker-compose.yaml` uses `:latest` (the default), just pull and restart:
<CodeGroup>
```bash Local deployment
docker compose down
docker compose up --pull always
```
```bash Remote deployment
cd dograh
sudo docker compose --profile remote down
sudo docker compose --profile remote up --pull always
```
</CodeGroup>
`--pull always` forces Docker to fetch the latest `:latest` from the registry instead of reusing your cached image.
## Option B: Pin a specific tag
To update (or roll back) to a specific release or commit, edit `docker-compose.yaml` and change the `image:` lines for both `api` and `ui` services to the same tag.
Open the file:
```bash
nano docker-compose.yaml
```
Find these two lines:
```yaml
api:
image: ${REGISTRY:-dograhai}/dograh-api:latest
ui:
image: ${REGISTRY:-dograhai}/dograh-ui:latest
```
Replace `:latest` with your chosen tag on **both** services — for example:
```yaml
api:
image: ${REGISTRY:-dograhai}/dograh-api:v0.8.2
ui:
image: ${REGISTRY:-dograhai}/dograh-ui:v0.8.2
```
<Note>
You can use either registry. Leave `REGISTRY` unset for Docker Hub (`dograhai`), or export `REGISTRY=ghcr.io/dograh-hq` to pull from GitHub Container Registry.
</Note>
Then bring the stack down and back up:
<CodeGroup>
```bash Local deployment
docker compose down
docker compose up --pull always
```
```bash Remote deployment
cd dograh
sudo docker compose --profile remote down
sudo docker compose --profile remote up --pull always
```
</CodeGroup>
## Verify the update
Check the running image tags:
```bash
docker compose ps --format "table {{.Service}}\t{{.Image}}"
```
You should see the API and UI both running the tag you pinned.
Hit the health endpoint to confirm the API is responding:
```bash
curl http://localhost:8000/api/v1/health
```
## Roll back
If something breaks, roll back by pinning the previous tag using the same process in **Option B** and restarting. Your Postgres data volume persists across `down`/`up` cycles, so agents and call history are preserved.
<Warning>
Rolling back across a database migration is not always safe — if the newer release ran a schema migration, downgrading may leave the DB in a state the older API doesn't understand. If in doubt, [open an issue](https://github.com/dograh-hq/dograh/issues) before rolling back.
</Warning>

View file

@ -141,6 +141,7 @@
"deployment/introduction",
"deployment/docker",
"deployment/custom-domain",
"deployment/update",
"deployment/web-widget",
"deployment/heroku"
]

View file

@ -2,6 +2,7 @@
import type { Team } from "@stackframe/stack";
import {
ArrowUpCircle,
AudioLines,
Brain,
ChevronLeft,
@ -54,6 +55,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useAppConfig } from "@/context/AppConfigContext";
import { useLatestReleaseVersion } from "@/hooks/useLatestReleaseVersion";
import type { LocalUser } from "@/lib/auth";
import { useAuth } from "@/lib/auth";
import { cn } from "@/lib/utils";
@ -89,6 +91,12 @@ export function AppSidebar() {
// Version info from app config context
const versionInfo = config ? { ui: config.uiVersion, api: config.apiVersion } : null;
// Check for updates only on self-hosted (OSS) deployments — cloud is managed for the user.
const { latest: latestRelease, isBehind, isLatest } = useLatestReleaseVersion(
versionInfo?.ui,
{ enabled: config?.deploymentMode === "oss" },
);
const isActive = (path: string) => {
return pathname.startsWith(path);
};
@ -232,17 +240,53 @@ export function AppSidebar() {
<div className="flex items-center justify-between">
{/* Logo - only show when expanded */}
{effectiveState === "expanded" && (
<Link
href="/"
className="flex items-center gap-2 px-2 text-xl font-bold"
>
Dograh
{versionInfo && (
<span className="text-xs font-normal text-muted-foreground">
v{versionInfo.ui}
</span>
<div className="flex items-center gap-2">
<Link
href="/"
className="flex items-center gap-2 px-2 text-xl font-bold"
>
Dograh
{versionInfo && (
<span className="text-xs font-normal text-muted-foreground">
v{versionInfo.ui}
</span>
)}
</Link>
{isBehind && latestRelease && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<a
href="https://docs.dograh.com/deployment/update"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 rounded-md border bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium leading-none text-amber-900 transition-opacity hover:opacity-80 dark:bg-amber-950 dark:text-amber-200"
>
<ArrowUpCircle className="h-3 w-3" />
Update
</a>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>Latest: {latestRelease} click to see the update guide</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</Link>
{isLatest && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex items-center rounded-md border bg-emerald-50 px-1.5 py-0.5 text-[10px] font-medium leading-none text-emerald-900 dark:bg-emerald-950 dark:text-emerald-200">
Latest
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>You&apos;re running the latest release</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
)}
{/* Toggle button - center it when collapsed */}
<SidebarTrigger className={cn(

View file

@ -0,0 +1,107 @@
"use client";
import { useEffect, useState } from "react";
interface Options {
enabled: boolean;
}
interface Result {
latest: string | null;
isBehind: boolean;
isLatest: boolean;
}
const CACHE_KEY = "dograh-latest-release";
const CACHE_TTL_MS = 6 * 60 * 60 * 1000;
const SEMVER_RE = /^(?:[a-z][a-z0-9-]*-)?v?(\d+)\.(\d+)\.(\d+)$/i;
function parseSemver(tag: string): [number, number, number] | null {
const m = tag.match(SEMVER_RE);
if (!m) return null;
return [Number(m[1]), Number(m[2]), Number(m[3])];
}
function isOlder(current: string, latest: string): boolean {
const c = parseSemver(current);
const l = parseSemver(latest);
if (!c || !l) return false;
for (let i = 0; i < 3; i++) {
if (c[i] < l[i]) return true;
if (c[i] > l[i]) return false;
}
return false;
}
export function useLatestReleaseVersion(
currentVersion: string | undefined,
{ enabled }: Options,
): Result {
const [latest, setLatest] = useState<string | null>(null);
useEffect(() => {
if (!enabled || !currentVersion) return;
try {
const raw = localStorage.getItem(CACHE_KEY);
if (raw) {
const parsed = JSON.parse(raw) as { tag: string; fetchedAt: number };
if (Date.now() - parsed.fetchedAt < CACHE_TTL_MS) {
setLatest(parsed.tag);
return;
}
}
} catch {
// ignore malformed cache
}
let cancelled = false;
fetch("https://api.github.com/repos/dograh-hq/dograh/releases/latest")
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
if (cancelled || !data?.tag_name) return;
const tag = data.tag_name as string;
try {
localStorage.setItem(
CACHE_KEY,
JSON.stringify({ tag, fetchedAt: Date.now() }),
);
} catch {
// storage may be full or disabled
}
setLatest(tag);
})
.catch(() => {
// silent — don't break the sidebar if GitHub is unreachable
});
return () => {
cancelled = true;
};
}, [enabled, currentVersion]);
const normalizedCurrent = currentVersion
? currentVersion.startsWith("v")
? currentVersion
: `v${currentVersion}`
: null;
const currentParsed = normalizedCurrent ? parseSemver(normalizedCurrent) : null;
const latestParsed = latest ? parseSemver(latest) : null;
const isBehind = !!(
normalizedCurrent &&
latest &&
isOlder(normalizedCurrent, latest)
);
const isLatest = !!(
currentParsed &&
latestParsed &&
currentParsed[0] === latestParsed[0] &&
currentParsed[1] === latestParsed[1] &&
currentParsed[2] === latestParsed[2]
);
return { latest, isBehind, isLatest };
}