local-quickstart
description: Run a trip2g instance locally from the Docker image (or a binary) and publish a content vault over the API in a few minutes.
Local quickstart
Run a trip2g instance on your machine and push a content vault to it over the API. This requires only the server and the sync CLI; no Git remote or GIT_API_REPO_PATH is needed.
You need: the trip2g Docker image (or a locally built binary) and a MinIO (S3-compatible) endpoint for assets.
1. Run the server
trip2g needs an S3-compatible store (MinIO) for assets. The commands below put both containers on a shared Docker network so they can talk to each other by name, and publish the ports you need to the host. This works on Linux, Mac, and Windows.
# Shared network so the app can reach MinIO by container name
docker network create trip2g-local-net
# MinIO (skip if you already have one; attach it to the same network)
docker run -d --name trip2g-minio \
--network trip2g-local-net \
-p 9000:9000 -p 9001:9001 \
-e MINIO_ROOT_USER=trip2g -e MINIO_ROOT_PASSWORD=trip2g-secret \
minio/minio:latest server /data --console-address ":9001"
# trip2g app on port 24081 (health on 24082), fresh local DB
# This pulls the current published image. To build locally from source instead,
# run `docker build -t trip2g:local .` from a trip2g checkout and replace the
# image tag below with `trip2g:local`.
mkdir -p /tmp/trip2g-local
docker run -d --name trip2g-local \
--network trip2g-local-net \
-p 24081:24081 -p 24082:24082 \
-e LISTEN_ADDR=0.0.0.0:24081 -e INTERNAL_LISTEN_ADDR=:24082 \
-e DB_FILE=/data/local.sqlite3 \
-e DEV=true \
-e OWNER_EMAIL=hello@example.com \
-e MINIO_ENDPOINT=trip2g-minio:9000 \
-e MINIO_ACCESS_KEY_ID=trip2g -e MINIO_SECRET_KEY=trip2g-secret \
-e MINIO_BUCKET=trip2g-local -e MINIO_USE_SSL=false \
-e PUBLIC_URL=http://localhost:24081 \
-e JWT_SECRET=dev-secret-not-for-prod \
-e USER_TOKEN_INSECURE=true \
-e GIT_API_REPO_PATH=/data/git -e GIT_API_BASE_PATH=/git \
-e RESEND_API_KEY=dev -e MAIL_FROM=dev@example.com \
-v /tmp/trip2g-local:/data \
ghcr.io/trip2g/trip2g:latest
# wait until healthy (also verify the container is actually running)
docker ps | grep trip2g-local
until curl -sf http://localhost:24082/healthz >/dev/null; do sleep 1; done; echo "up"
Notes:
DEV=trueenables the dev sign-in code (no real email needed). Never use in production.- You do not need the
FEATURES/ vector-search env for plain content; omit it to avoid the embedding-server dependency. -p 24081:24081 -p 24082:24082publishes the app and health ports to your host machine. MinIO's-p 9000:9000does the same for S3 access (MinIO console is on 9001).MINIO_ENDPOINT=trip2g-minio:9000uses the container name as the hostname. Docker's built-in DNS resolves it on the shared network. From your host browser or CLI you still reach MinIO atlocalhost:9000.- On Linux you can skip the network setup and use
--network hoston the trip2g container withMINIO_ENDPOINT=localhost:9000instead. Docker Desktop on Mac and Windows does not support host networking, so the-papproach above is the portable default.
Building from source instead of the image: make build produces ./tmp/server; run it with the same env vars (and a reachable MinIO).
2. Mint an API key (dev flow)
In DEV=true the server accepts a fixed sign-in code (111111, 000000 also works). Sign in as the owner, then create a key:
GQL=http://localhost:24081/graphql
curl -s -X POST "$GQL" -H 'Content-Type: application/json' \
-d '{"query":"mutation($i:RequestEmailSignInCodeInput!){requestEmailSignInCode(input:$i){__typename}}","variables":{"i":{"email":"hello@example.com"}}}' >/dev/null
TOKEN=$(curl -s -X POST "$GQL" -H 'Content-Type: application/json' \
-d '{"query":"mutation($i:SignInByEmailInput!){signInByEmail(input:$i){__typename ... on SignInPayload{token} ... on ErrorPayload{message}}}","variables":{"i":{"email":"hello@example.com","code":"111111"}}}' \
| grep -o '"token":"[^"]*"' | cut -d'"' -f4)
API_KEY=$(curl -s -X POST "$GQL" -H 'Content-Type: application/json' -H "Cookie: trip2g_token=$TOKEN" \
-d '{"query":"mutation($i:CreateApiKeyInput!){admin{createApiKey(input:$i){__typename ... on CreateApiKeyPayload{value} ... on ErrorPayload{message}}}}","variables":{"i":{"description":"local"}}}' \
| grep -o '"value":"[^"]*"' | cut -d'"' -f4)
echo "API key: $API_KEY"
(If you set a custom USER_TOKEN_COOKIE_NAME, use that cookie name instead of trip2g_token.)
3. Push your content
Use the sync CLI to publish a folder of notes (.md) plus any _layouts/ templates and assets. The CLI (obsidian-sync/dist/trip2g-sync.mjs) lives in the trip2g source repository. Run the following from the root of a trip2g checkout:
node obsidian-sync/dist/trip2g-sync.mjs \
--folder /path/to/your/vault \
--api-key "$API_KEY" \
--api-url http://localhost:24081/graphql \
--verbose
Continuous sync with --watch
--watch (alias -w) keeps the CLI running as a long-running daemon. It does a full two-way reconcile on startup, then maintains a live connection:
- Remote → local: subscribes to the server's
noteChangesSSE stream and writes any server-side change back to the vault folder immediately. - Local → remote: watches the filesystem and pushes edits to the server after a ~500 ms debounce.
node obsidian-sync/dist/trip2g-sync.mjs --watch \
--folder /path/to/your/vault \
--api-key "$API_KEY" \
--api-url http://localhost:24081/_system/graphql
The process stays in the foreground. Press Ctrl-C for a clean shutdown. It exits non-zero on a fatal error, so a container restart policy or systemd service can restart it automatically.
Filtering with --include and --exclude
Use --include <glob> (-i) and --exclude <glob> (-x) to control which paths the SSE follower tracks. Both flags are repeatable.
# Only follow notes under journal/ and projects/
node obsidian-sync/dist/trip2g-sync.mjs --watch \
--folder /path/to/vault \
--api-key "$API_KEY" \
--api-url http://localhost:24081/_system/graphql \
--include "journal/**" \
--include "projects/**"
Precedence: CLI flags override any livePull patterns stored in data.json, which override the built-in default (**, meaning follow everything). With --watch and no patterns set anywhere, all paths are followed.
4. View it
Open the note's permalink, e.g. http://localhost:24081/<path>/<note>.
A note with route: yourdomain.com/ in frontmatter is served on that custom domain (needs a Host: header or DNS locally). To preview without DNS, hit the plain permalink instead.
Using this instance as AI-agent memory? See en/user/agent-memory.
Gotchas
- Dev sign-in code is
111111(also000000). Only withDEV=true. route:frontmatter maps a note to a custom domain. Locally, view via the plain permalink (/path/note) or send aHost:header.- Custom Jet layouts: a note with
layout: <theme>/<page>renders through_layouts/<theme>/<page>.html. If the template fails to parse, the server silently falls back to the default layout (HTTP 200), so the page "works" but looks wrong. {{/* ... */}}block comments break the template engine in current builds. Don't put them in_layouts/*.html.- A note is public (served to anonymous visitors) only with
free: truein frontmatter, or a**/*.md → { free: true }frontmatter patch.