GitNode is a simple, open-source, self-hosted Git registry inspired by GitHub. It gives you full control over your repositories, pull requests, and CI/CD pipelines (YAML workflows + self-hosted runner agent) β running entirely on your own infrastructure, with zero dependency on third-party platforms.
No subscriptions. No data leaving your servers. No vendor lock-in. Just Git, hosted your way.
GitNode is built for developers and teams who care about ownership β whether you're an indie developer running it on a VPS, or an enterprise team deploying it on private infrastructure. If you've ever thought "I wish GitHub ran on my own server", GitNode is for you.
GitNode covers the full Git hosting loop β repos, review, browsing, issues, project boards, releases, webhooks, code snippets, and collaborator access β plus CI/CD Actions (YAML workflows + Go runner agent), AI code review, commit suggestions, PR description generation, and codebase analysis (OpenAI / Anthropic / Gemini / Ollama), enterprise SAML/LDAP SSO, platform admin tooling, rate limiting, Prometheus/Grafana observability, and audit logging β all on your own infrastructure.
| π Repository Management | π€ Public Profiles | π₯ GitHub Migration |
| π Code Browsing | π Pull Requests | π Issues |
| π Project Boards | π Code Snippets | π· Tags & Releases |
| π Webhooks | π Authentication | π₯ Collaborators |
| π΄ Repository Forks | π‘ Access Policies | π’ Enterprise SSO |
| π Admin Panel | β‘ Rate Limiting | π Observability |
| π Audit Logging | β‘ Actions | π€ AI Features |
- Create, clone, push, and pull repositories
- Public and private repositories, descriptions, and topics
- Git over HTTP and HTTPS (TLS): smart HTTP backend at
/git/β¦β usehttp://orhttps://remote URLs with your GitNode host - SSH Git on a configurable port (default 2222 in Docker)
- Per-repo Settings: general metadata, optional auto-delete head branch after PR merge or close
- Every account has a public profile at
/:usernameshowing public repositories - Optional profile README rendered from the account's special repository
- Paginated public repository list
- Migrate from GitHub with a repository URL and personal access token (classic or fine-grained with repo read)
- Mirror clone the Git history into your GitNode account
- Optionally migrate pull requests from GitHub in the same job
- File tree with breadcrumbs; blob viewer and raw file URLs
- Markdown README on the repo home (images and relative links resolved like on GitHub)
- Commit history and diffs
- Open, review, merge, or close PRs
- Merge strategies: merge commit, squash, rebase
- Draft PRs, inline discussion, file-level comments
- Track bugs and feature requests per repository
- Labels, comments, open/close status
- Link issues to Kanban tasks β resolving a PR can auto-complete linked tasks
- Projects with boards and configurable columns (per-project)
- Tasks and subtasks with types, status, assignee, and ordering
- Create Git branches from a task or subtask (conventional branch names, e.g.
TASK-1orTASK-1.SUB-1-β¦) - Link a branch's pull request to the task or subtask; see PR status on the card
- Optional automation (per project): when a linked PR is merged, mark the task or subtask completed
- Project settings page for the above PR β status behaviour
- Projects linked to a repository are paginated in the repo's Projects tab
- Create public or private snippets with syntax-highlighted code blocks
- Multi-file support per snippet
- Full revision history β track edits and diff between revisions
- Fork any public snippet
- Paginated snippets per repository in the repo's Snippets tab
- Manage all your snippets from the Snippets section in the app bar
- Signed HTTP delivery (
X-Hub-Signature-256) to your services for pushes, PR events, and more - Automatic retries (3 attempts, exponential back-off) on delivery failure
- Dead-letter queue (DLQ) β permanently failed deliveries are queued and retried on a schedule; admin can inspect and replay from the admin panel
- Per-host circuit breaker (Resilience4j) β when a target endpoint fails repeatedly the circuit opens; subsequent deliveries go straight to the DLQ instead of burning retries; circuit auto-recovers when the endpoint comes back
- Configured per-repository in Settings β Webhooks
- Create lightweight and annotated tags on any commit via the UI
- Draft or publish releases tied to a tag β write release notes with Markdown
- Upload release assets (binaries, archives, checksums) directly from the browser
- Browse all releases in the repo's Releases tab; latest release shown on the repo home
- Delete releases or tags from the UI (tag is removed from the underlying Git repo)
- Release badge on the repo home shows the latest published version at a glance
- Invite other GitNode users to your repository with fine-grained per-permission roles
- Available permissions (each toggled independently): Push, Pull Request management, Issue management, * Settings access*, Admin (all permissions)
- Share an invite link with a configurable expiry β recipient accepts via the link, no admin approval needed
- Manage active collaborators and revoke access at any time from Settings β Collaborators
- Collaborators inherit the base repo visibility β private repos remain private to non-collaborators
- How to invite: go to your repository β Settings β Collaborators β Invite β pick permissions β copy the generated link and send it to the person you want to add
- Fork any public repository to your own account with a single click
- Fork preserves the full commit history of the upstream repo at the time of forking
- Work on your fork independently β push branches, open issues, create snippets
- Open a pull request from your fork back to the upstream repository to propose changes
- How to fork: navigate to any public repository β click Fork in the top-right area of the repo header
- Define access rules per repository that apply on top of base visibility
- Policies control what authenticated (non-owner, non-collaborator) users can do β e.g. allow public read but restrict push, or allow fork but restrict issue creation
- Useful for organizations that want open-source-style read access without enabling arbitrary contributions
- Configured in Settings β Access Policies; changes take effect immediately for all subsequent requests
- YAML workflow definitions checked in at
.gitnode/workflows/*.ymlβpush,pull_request, andworkflow_dispatchtriggers - Job graph with matrix strategy expansion and
needsdependency ordering;concurrencygroups with cancellation - Runner protocol: runners register via token, receive jobs over WebSocket, report step logs and status back to the server
- Shell and Docker executors β built-in
actions/checkout@v1step; customrunsteps execute in a per-job workspace - Secrets vault β AES-256-GCM encrypted secrets per repo; injected as env vars at job runtime (masked in logs)
- Artifact store β upload/download artifacts by run + name; retained per run
- Cache store β key-based cache for workflow dependencies
- SSE log streaming β real-time step logs via Server-Sent Events (
GET /api/repos/{owner}/{repo}/actions/runs/{runId}/events) - Run history β list, cancel, and re-run workflows from the UI
- Runner management β register, list, delete runners per repo; runner groups per org
- Admin panel Actions tab β platform-wide runner stats, workflow run counts
Standalone agent that connects to the server. Single static binary (~12 MB), no JRE needed.
cd gitnode-runner
make build # dist/gitnode-runner (local arch)
make build-all # Linux amd64/arm64, macOS arm64, Windows amd64
./dist/gitnode-runner start \
--server-url http://gitnode.company.com \
--token ghrt_xxxxxxxxxxxx \
--name my-runner \
--labels self-hosted,linux,docker \
--executor docker \ # or: shell
--work-dir /tmp/gitnode-runner \
--concurrent-jobs 2Config file (~/.gitnode-runner/config.yml) is written automatically after first registration β subsequent starts use
runner_token from that file.
- Per-organization identity β map email domains to a SAML 2.0 IdP or corporate LDAP directory
- SAML 2.0 service provider β metadata URI, connection test, cached IdP XML, SP entity ID override
- LDAP directory auth β manager bind, user search base/filter, email and display-name attributes, optional group mapping
- Work-email login flow β users enter work email on the login page; GitNode routes to the correct org and provisions accounts on first successful sign-in
- Mutually exclusive per org β SAML and LDAP cannot both be enabled on the same organization
- Configure in the admin panel (
gitnode-admin-panel, port 4300 in local dev)
- Dev:
cd gitnode-admin-panel && pnpm startβ http://localhost:4300 - See gitnode-admin-panel/README.md
Features when enabled:
- Dashboard β users, repositories, organizations, storage; activity tables (daily/weekly); top contributors; cached stats
- Users β search, enable/disable accounts
- Organizations β create, edit, delete; configure SAML or LDAP per org; test connections before enabling
- Audit log API β query application audit events (
GET /api/admin/audit-logs)
See gitnode-admin-panel/README.md for setup. Platform admin access needs
bootstrap credentials (see Environment Variables in README).
- Redis-backed sliding-window limits on sensitive endpoints
- Covers authentication (login, register, password recovery), repo/PR/issue creation, webhooks, tags, snippets, and SSO/LDAP discovery
- Returns 429 with
rateLimitExceededwhen limits are hit
Optional. Monitoring containers are not started by default β run make monitoring when needed.
- Micrometer metrics exported at
/actuator/prometheus(toggle withGITNODE_OBSERVABILITY_ENABLED) - Docker Compose profile
monitoringβ Prometheus (9090) and Grafana (3000, admin / admin) - See monitoring/README.md for setup
- Scrape targets: app container (
gitnode:8080) or host-run backend (host.docker.internal:8080) - Circuit breaker health at
/actuator/circuitbreakersβ real-timeCLOSED / OPEN / HALF_OPENstate for webhook delivery and SAML metadata circuit breakers; included in/actuator/healthdetails
- Application audit log β
@Auditedactions persisted to partitionedaudit_logstables (append-only triggers) - Admin API β paginated queries by actor and recent window
- pgAudit β Postgres image logs write, DDL, and role operations (
shared_preload_libraries=pgaudit). Admin log viewer is off by default β setGITNODE_ADMIN_PGAUDIT_ENABLED=trueand mount the Postgres log volume into the app container. - Toggle application audit with
GITNODE_AUDIT_ENABLED(defaulttrue)
- Username + password login with JWT
- Basic Auth for git repo operations
- OAuth2: Google, GitHub, GitLab
- SSH public keys for Git over SSH
- Enterprise SAML 2.0 and LDAP per organization (see above)
AI is opt-in at every layer: users bring their own provider and API key; repositories opt in to automated PR review separately. Nothing runs until you enable it β no platform-level LLM key required.
Bring your own model (BYOK)
| Provider | Models / notes |
|---|---|
| OpenAI | GPT-4o, GPT-4o-mini, GPT-4-turbo, compatible endpoints |
| Anthropic | Claude 3.5 Sonnet, Claude 3 Opus, and newer Claude models |
| Google Gemini | Via OpenAI-compatible API β key from Google AI Studio |
| Local (Ollama) | Self-hosted models; set base URL to your Ollama instance |
Configure in User Settings β AI: pick provider, model, API key, and optional custom base URL. Keys are encrypted at rest with AES-256-GCM when GITNODE_AI_ENCRYPTION_KEY is set. Use Test connection to validate credentials before saving.
- Repo opt-in β toggle Enable PR AI review in Settings β AI Analysis (off by default)
- When enabled, opening a PR triggers an async review using the PR author's AI settings, falling back to the repo owner
- Comments are categorized (
BUG,SECURITY,PERFORMANCE,CODE_QUALITY,GENERAL) with severity (CRITICALβINFO) - View results in the PR AI Review tab β summary plus paginated inline findings
- Retry if a review failed or was skipped; uses your account's AI settings
- Memory-safe diffs β large PRs use prioritized, bounded diff sampling (vendor paths and lockfiles skipped)
- Sparkles button on the new-pull-request form suggests a Conventional Commits message from the branch diff
- Imperative, scoped, β€72 characters β ready to paste into your commit
- AI Generate on the new-PR form produces a title plus structured Markdown: summary, motivation, breaking changes, security/config notes, and a test-plan checklist
- Rate-limited to 10 requests per 10 minutes per user
Scores the repository across six dimensions (1β10 each) with reasons, issues, and fixes:
| Dimension | What it evaluates |
|---|---|
| Architecture | Modularity, layering, coupling, deployability |
| Code Quality | Readability, consistency, error-handling patterns |
| Performance | Hot paths, I/O, caching, algorithmic cost |
| Memory Usage | Allocation patterns, unbounded buffers, leak risk |
| Scalability | Stateless design, pagination, horizontal scaling readiness |
| Security | Authn/authz, secrets, injection, transport β includes a dedicated security pass |
- Run from Settings β AI Analysis; history is paginated per branch
- Memory-safe for large repos β bounded tree walk, skips
node_modules/target/ lockfiles, loads only the highest-priority source files - Rate-limited to 3 analyses per hour per user
- Production-ready prompts β structured output for parsing; focused on actionable findings, not noise
- Bounded inputs β diff and codebase samples are truncated with explicit sampling notes sent to the model
- Per-provider circuit breakers (
ai-openai,ai-anthropic,ai-gemini,ai-local) β opens at 60% failure rate over 5 calls, 60 s cooldown
Environment variable:
| Variable | Required | Default | Description |
|---|---|---|---|
GITNODE_AI_ENCRYPTION_KEY |
No | (blank = plaintext) | Base64-encoded 32-byte AES-256 key for user API keys. Generate with make ai-encryption-key or openssl rand -base64 32. Required in production. |
| Layer | Technology |
|---|---|
| Language | Java 25 |
| Framework | Spring Boot 4, Spring Security, Spring Data JPA |
| Git Engine | Eclipse JGit |
| SSH Server | Apache MINA SSHD |
| Auth | JWT, OAuth2 (Google Β· GitHub Β· GitLab), SAML 2.0, LDAP |
| Database | PostgreSQL 17 + Flyway, pgAudit |
| Cache | Redis (cache + rate limiting) |
| Observability | Micrometer, Prometheus, Grafana |
| Resilience | Resilience4j circuit breakers (webhook delivery, SAML metadata, AI providers) |
| AI | Spring AI 2.0 β OpenAI, Anthropic, Gemini (OpenAI-compat), Ollama; BYOK per user; bounded sampling for large repos |
| Audit | Application audit log (partitioned PostgreSQL) |
| CI/CD Engine | Spring Boot actions module β WebSocket runner protocol, SSE log streaming, secrets vault (AES-256-GCM), artifact/cache store |
| Runner Agent | Go 1.24 (gitnode-runner) β shell + Docker executors, single static binary |
| Frontend | Angular 21, TypeScript 5 |
| Admin UI | Angular 21 (gitnode-admin-panel) |
| Styling | Tailwind CSS 4, DaisyUI 5 |
| Container | Docker (multi-stage build, single image) |
π Documentation is available in-app at
/docsonce GitNode is running.
See CONTRIBUTING.md for run profiles, Makefile commands, and module docs.
Base app (Frontend + Backend):
make dev-setup && make dev-backend # terminal 1
cd gitnode-frontend && pnpm start # terminal 2 β :4200Full app (+ Grafana + Admin): see CONTRIBUTING.md#run-profiles
make test # unit tests + lintSECRET=$(openssl rand -base64 64 | tr -d '\n')
# Infrastructure (Postgres with pgAudit, Redis) β creates the gitnode network
docker compose up -d
# Optional: Prometheus + Grafana
docker compose --profile monitoring up -d
# Optional: admin panel UI (dev, separate terminal)
# cd gitnode-admin-panel && pnpm install && pnpm start β http://localhost:4300
docker run -d \
--name gitnode \
--network gitnode \
-p 8080:8080 \
-p 2222:2222 \
-e SPRING_PROFILES_ACTIVE=os \
-e "GITNODE_JWT_SECRET=$SECRET" \
-e SPRING_DATASOURCE_URL=jdbc:postgresql://gitnode-postgres:5432/gitnode \
-e SPRING_DATASOURCE_USERNAME=admin \
-e SPRING_DATASOURCE_PASSWORD=admin123 \
-e SPRING_DATA_REDIS_HOST=gitnode-redis \
-e SPRING_DATA_REDIS_PORT=6379 \
-e GITNODE_GIT_REPO__ROOT=/data/repos \
-e GITNODE_BOOTSTRAP_ADMIN_ENABLED=true \
-e GITNODE_BOOTSTRAP_ADMIN_USERNAME=admin \
-e GITNODE_BOOTSTRAP_ADMIN_PASSWORD=Admin123 \
-e GITNODE_ADMIN_MODULITH_EVENTS_ENABLED=true \
-e GITNODE_FRONTEND_BASE_URL=http://localhost:8080 \
-e GITNODE_AUDIT_ENABLED=true \
-e GITNODE_OBSERVABILITY_ENABLED=false \
-e GITNODE_CORS_ALLOWED_ORIGINS=http://localhost:8080 \
-v gitnode-repos:/data/repos \
repo.repsy.io/nuricanozturk/gitnode/gitnode-os:latestBase stack β Postgres + Redis + app:
make up # β http://localhost:8080Optional add-ons:
make monitoring # Prometheus + Grafana
cd gitnode-admin-panel && pnpm start # admin UI β :4300 (API on by default)All commands: CONTRIBUTING.md
| Service | URL | Default |
|---|---|---|
| App | http://localhost:8080 | β |
| SSH Git | localhost:2222 | β |
| Frontend (dev) | http://localhost:4200 | manual β pnpm start |
| Admin panel | http://localhost:4300 | manual β pnpm start |
| Prometheus | http://localhost:9090 | optional β make monitoring |
| Grafana | http://localhost:3000 | optional β make monitoring |
| Variable | Required | Default | Description |
|---|---|---|---|
GITNODE_JWT_SECRET |
β | β | Min 32-char secret for JWT signing |
GITNODE_BOOTSTRAP_ADMIN_PASSWORD |
β prod | β | Bootstrap admin password (empty skips bootstrap) |
GITNODE_BOOTSTRAP_ADMIN_USERNAME |
admin |
First-start platform admin username | |
GITNODE_BOOTSTRAP_ADMIN_ENABLED |
true |
Run bootstrap admin creation on startup | |
GITNODE_PLATFORM_ADMIN_USERNAMES |
β | Comma-separated additional platform admin usernames | |
GITNODE_GIT_REPO__ROOT |
~/.gitnode |
Git repository storage path (use a volume in Docker) | |
GITNODE_FRONTEND_BASE_URL |
http://localhost:4200 |
Base URL returned after OAuth2/SAML redirects | |
GITNODE_CORS_ALLOWED_ORIGINS |
http://localhost:4200,http://localhost:4300 |
CORS origins β add admin panel URL for admin access | |
GITNODE_AUDIT_ENABLED |
true |
Application audit log | |
GITNODE_OBSERVABILITY_ENABLED |
true |
Prometheus /actuator/prometheus |
|
GITNODE_ACTIONS_ENABLED |
true |
CI/CD Actions engine | |
GITNODE_ADMIN_MODULITH_EVENTS_ENABLED |
false |
Admin panel event publication viewer | |
GITNODE_ADMIN_PGAUDIT_ENABLED |
false |
Admin panel pgAudit log viewer | |
GITNODE_ADMIN_PGAUDIT_LOG_DIRECTORY |
β | Postgres log dir mounted into the app container | |
GITNODE_SSO_SAML_ENABLED |
false |
Global SAML feature flag | |
GITNODE_SSO_LDAP_ENABLED |
false |
Global LDAP feature flag | |
GITNODE_SSO_SAML_SP_SIGNING_KEY_PATH |
β | SP signing private key path (SAML); make saml-keygen |
|
GITNODE_SSO_SAML_SP_SIGNING_CERT_PATH |
β | SP signing certificate path (SAML); make saml-keygen |
|
ACTIONS_ENCRYPTION_KEY |
β | AES-256 key (base64) for Actions secrets vault; make actions-encryption-key |
|
GITNODE_AI_ENCRYPTION_KEY |
β | AES-256 key (base64) for user AI API keys at rest; make ai-encryption-key |
|
SPRING_DATA_REDIS_HOST |
localhost |
Redis hostname | |
SPRING_DATA_REDIS_PORT |
6379 |
Redis port | |
SPRING_DATASOURCE_URL |
jdbc:postgresql://localhost:5432/gitnode |
Postgres JDBC URL | |
SPRING_DATASOURCE_USERNAME |
admin |
Postgres username | |
SPRING_DATASOURCE_PASSWORD |
admin123 |
Postgres password | |
OAUTH2_GOOGLE_CLIENT_ID |
β | Google OAuth2 client ID | |
OAUTH2_GOOGLE_CLIENT_SECRET |
β | Google OAuth2 client secret | |
OAUTH2_GITHUB_CLIENT_ID |
β | GitHub OAuth2 client ID | |
OAUTH2_GITHUB_CLIENT_SECRET |
β | GitHub OAuth2 client secret | |
OAUTH2_GITLAB_CLIENT_ID |
β | GitLab OAuth2 client ID | |
OAUTH2_GITLAB_CLIENT_SECRET |
β | GitLab OAuth2 client secret |
GitNode is designed to run as multiple stateless app instances behind a load balancer.
| Component | Storage | Multi-instance |
|---|---|---|
| Session / JWT | Stateless | β |
Spring Cache (branches, repo-meta, β¦) |
Redis | β |
| Actions runtime state (pending uploads, ID allocation) | Redis | β |
| Webhook DLQ retry state | DB | β |
| Rate-limit counters | Redis | β |
| Component | Default path | Multi-instance requirement |
|---|---|---|
| Git repositories | GITNODE_GIT_REPO__ROOT (default /data/repos) |
Shared volume β all instances must read/write the same path |
| CI/CD artifacts & cache | gitnode.actions.artifacts.local-path |
Shared volume β artifacts committed on one instance must be readable on all others |
Without shared storage, requests routed to a different instance than the one that wrote the file will return 404. Mount a network filesystem (NFS, AWS EFS, GCP Filestore, Azure Files) at both paths, or use a ReadWriteMany Kubernetes PVC.
make up-ha N=3 # infra + 3 app instances + HAProxy (HTTP :8080, SSH :2222)
make proxy # start HAProxy only (if instances already running)
make app-scale N=3HAProxy config is in haproxy.cfg at the project root.
# Shared PVC for repos + artifacts β mount on all app pods
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gitnode-data
spec:
accessModes: [ReadWriteMany] # requires NFS / EFS / GCP Filestore / Azure Files
resources:
requests:
storage: 100Gi
---
# In your Deployment / StatefulSet:
volumeMounts:
- name: data
mountPath: /data/repos # GITNODE_GIT_REPO__ROOT
- name: data
mountPath: /data/artifacts # gitnode.actions.artifacts.local-path
subPath: artifacts
volumes:
- name: data
persistentVolumeClaim:
claimName: gitnode-dataRedis and PostgreSQL must also be external (not per-pod) in a multi-instance setup β use a managed service or a dedicated StatefulSet with persistence.
Distributed under the MIT License.