Automatically exports your self-hosted Outline collections as Markdown and syncs them to Git repositories.
- Smart Sync: Commits and pushes only when actual changes are detected in your notes. Documents deleted in Outline are deleted in git too.
- Easy to browse on GitHub: Every document gets a breadcrumb trail at the top (
🏠 Collection › Parent › Page), a list of its sub-pages and a ⬆ Back to parent link at the bottom. The generatedREADME.mdshows the full document tree. - No export pile-up: Each export is deleted from Outline once it has been downloaded, so Outline's storage stops filling up with zip files.
- Self-healing git: The repo is synced with the remote before every push, so a lost volume, a failed push or a commit made on GitHub never leaves the sync stuck.
- Storage Optimized: Optional
purge_local_historykeeps the container's.gitfolders tiny (shallow fetch +git gc). - Small & fast: A single static Go binary on Alpine. Shuts down cleanly on
docker stop.
Create a config.yml in the root directory. You can add as many sync tasks as you need.
outline_url: "https://docs.yourdomain.com"
outline_token: "outline_api_your_token_here"
check_interval: 3600 # Check for changes every hour (in seconds)
git_user: "Outline Robot" # Optional commit author
git_email: "bot@domain.com"
purge_local_history: true # Keep the local .git folders small
# Optional (defaults shown)
cleanup_exports: true # Delete each export from Outline after downloading it
purge_old_exports: false # Also delete exports left over from earlier runs (see below)
navigation: true # Breadcrumbs, sub-page lists and "back to parent" links
export_timeout: 1800 # Seconds to wait for Outline to build an export
sync_tasks:
- collection_name: "Knowledge Base"
repo_url: "https://<user>:<github_token>@github.com/username/kb-backup.git"
branch: "main"
- collection_name: "Engineering"
repo_url: "https://<user>:<github_token>@github.com/username/eng-docs.git"
branch: "master"OUTLINE_URL and OUTLINE_TOKEN environment variables override the values in the file, if you'd rather keep the token out of it. Set TZ (e.g. TZ=Asia/Kolkata) to get commit times in your timezone.
Note on Security: Use a GitHub Personal Access Token (PAT) in the
repo_urlfor private repositories. Tokens in URLs are masked in the logs.
Outline keeps every export zip until it is deleted. Deleting exports through the API requires an admin API token. With a non-admin token the sync keeps working and logs a single warning, but you will have to delete exports yourself under Settings → Export.
To clear out exports left behind by older versions, set purge_old_exports: true once. Only Markdown exports of the configured collections created by the token's user are deleted. If you also make manual Markdown exports of these collections with the same account, those will be deleted too.
Ensure you have Docker and Docker Compose installed, then run:
docker compose up -d --buildNothing to change: same config.yml, same volumes, same repo layout. Pull and rebuild:
git pull && docker compose up -d --buildThe first sync after upgrading makes one extra commit, because every document gains the new navigation links.
The container will maintain the following structure locally:
repo_data/: Persistent volume containing the cloned git repositories (one folder per collection, spaces replaced by_).config.yml: Your settings and API keys.
Requires Go 1.26+ and git.
go test ./...
go run . --config config.yml --repos ./repos --onceFlags: --config (env CONFIG_PATH, default config.yml), --repos (env REPOS_DIR, default repos), --once (single sync cycle, then exit).
- Exports each collection from Outline as Markdown and waits for the zip to be ready.
- Downloads the zip, then deletes the export from Outline.
- Fetches the branch from the remote so local history matches GitHub.
- Extracts the files into the local repo, replacing the previous contents.
- Adds navigation to every document and regenerates
README.md. - Checks
git diff. If nothing but the README timestamp changed, it stops. - Commits & Pushes if changes exist, using the current timestamp as the message.
- Purges local history (if enabled) to keep the
.gitfolder tiny.
- Collection Not Found: Make sure
collection_nameinconfig.ymlmatches the name in Outline. An exact match is tried first, then a case-insensitive one. - Permission Denied: Verify your Outline API token has "Admin" or "Member" read access to the collections. Deleting exports needs Admin.
- Push Failed: Check that your GitHub PAT has
reposcope permissions.