Self-hosted server management — SSH, SCP, RDP, VNC & local terminal from your browser or desktop
Termi runs in production at termi.run — try it there, or follow Quick Start to self-host your own instance.
⭐ If Termi is useful to you, please star the repo. It costs nothing, takes two seconds, and is the main way other developers find this project — more effective than a clone, since a fork or a clone doesn't show up anywhere public. See Why star instead of just cloning?
- Features
- Quick Start
- Desktop App Installation
- Development Setup
- Project Structure
- Configuration
- Architecture
- Security
- Roadmap
- Contributing
- Open Source & Community
- Why star instead of just cloning?
- Star History
- License
- AES-256-GCM encryption for all stored server credentials
- Argon2id password hashing with secure parameters
- TOTP-based 2FA — works with Google Authenticator, Authy, and any TOTP app
- Passkey / WebAuthn — passwordless login with hardware keys and biometrics
- Google OAuth — "Sign in with Google" support
- Optional master key — adds a second encryption layer derived via PBKDF2
- Zero-trust architecture — credentials are decrypted only in memory, never stored in plaintext
- SSH — full terminal emulation with xterm.js, key forwarding, and resizable viewport
- SCP / SFTP — web-based file manager: upload, download, create folders, rename, delete
- RDP — Windows Remote Desktop via Apache Guacamole, with touch-to-mouse translation and an on-screen keyboard on mobile
- VNC — Virtual Network Computing via Apache Guacamole, same mobile touch support
- Telnet — direct terminal access for legacy/unencrypted devices
- Port forwarding — forward any port reachable from a server, like
ssh -L, with no terminal needed. Auto-detects HTTP targets for a one-click browser link; anything else gets a copyable local-bridge script (or, in the desktop app, a real local port)
- Global command palette (⌘K / Ctrl+K) — jump to any server, group, keychain entry, or settings section without leaving the keyboard
- Command snippets — save frequently-used shell commands and run them into the active terminal with one click
- Multi-server broadcast — run a command across every server in a group at once, with a per-server results view
- QR quick-connect — scan a QR code from a mobile device to open a one-tap connection
- Session recording & playback — record any SSH session and replay it later from Settings → Recordings, encrypted at rest
- Access your local machine's shell directly from the app
- Electron: spawns PowerShell (Windows) or your default shell (macOS/Linux)
- Browser/cloud: spawns a shell on the gateway host (gated by
ALLOW_LOCAL_TERMINAL=true)
- Real-time CPU, memory, and disk metrics fetched over SSH
- Health history charts
- Configurable email, web push, and webhook alerts (Slack, Discord, or any generic URL)
- Built-in benchmark tool with persisted history and a trend chart over time
- Share server access with other users via invitation links
- Per-server share management and revocation
- Install as a PWA on any device (iOS, Android, desktop)
- Virtual keyboard with Ctrl, Alt, Shift, Fn, and arrow keys
- Touch-optimised design
- Web push notifications for monitoring alerts
- Native app for macOS, Windows, and Linux
- Local terminal access via node-pty
- Connects to your self-hosted Termi instance
- Bundled gateway for fully offline / local-stack operation (
electron:dev:full)
- Docker Compose one-command deployment
- PostgreSQL database — your data stays on your server
- No telemetry, no cloud dependencies
- Traefik reverse-proxy support included
- Docker and Docker Compose
# 1. Clone the repository
git clone https://github.com/shuv-o/termi.git
cd termi
# 2. Copy and configure environment
cp .env.example .envOpen .env and set the required secrets (generate each with openssl rand -base64 32):
DB_HOST=postgres # set to your DB host (Docker service name or external host)
DB_USER=termi
DB_PASSWORD=<strong-password>
DB_NAME=termi
SESSION_SECRET=<openssl rand -base64 32>
ENCRYPTION_KEY=<openssl rand -base64 32>
GATEWAY_JWT_SECRET=<openssl rand -base64 32>
NEXT_PUBLIC_GATEWAY_URL=ws://localhost:22080/gateway
NEXT_PUBLIC_APP_URL=http://localhost:22080# 3. Start the stack
docker compose up -d
# 4. Run database migrations
docker compose exec web npx prisma migrate deploy
# 5. Open Termi
open http://localhost:22080RDP / VNC: also start guacd — on Apple Silicon, add
--platform linux/arm64:docker run -d -p 4822:4822 --name termi-guacd guacamole/guacd: 1.6.0
Skip building from source by pulling the published images instead. Every
tagged release (v*.*.*) publishes multi-arch (amd64 + arm64) images to both
Docker Hub and
GitHub Container Registry:
| Image | Docker Hub | GHCR |
|---|---|---|
Web (@termi/web) |
shuvoo/termi-web |
ghcr.io/shuv-o/termi-web |
| Gateway | shuvoo/termi-gateway |
ghcr.io/shuv-o/termi-gateway |
Both are tagged latest and with the release version (e.g. 1.0.10).
git clone https://github.com/shuv-o/termi.git
cd termi
cp .env.example .env
# edit .env — same required secrets as the "Deploy with Docker" section above
docker compose -f docker-compose.prebuilt.yml up -dTo pull from GHCR instead of Docker Hub, or pin to a specific version, set in .env:
TERMI_WEB_IMAGE=ghcr.io/shuv-o/termi-web:1.0.10
TERMI_GATEWAY_IMAGE=ghcr.io/shuv-o/termi-gateway:1.0.10The web image runs prisma migrate deploy automatically on first start —
no separate migration step needed.
Note: the published images don't bake in
NEXT_PUBLIC_APP_URL/NEXT_PUBLIC_GATEWAY_URLat build time (they're deployment-agnostic). If your setup needs those values compiled in rather than resolved at request time, build from source withdocker-compose.ymlinstead.
Download the desktop app for your platform and architecture from the landing page. Builds are provided for macOS, Windows, and Linux in both x64 and arm64.
The macOS build is ad-hoc signed but not notarized (the project has no paid Apple Developer ID). On first launch, macOS Gatekeeper shows:
"Apple could not verify 'Termi.app' is free of malware…"
This is expected. To open the app anyway, use either method:
Option A — Right-click to open
- In Finder, locate Termi.app (drag it to
/Applicationsfirst). - Right-click (or Control-click) the app → Open.
- Click Open in the dialog. macOS remembers this choice for future launches.
Option B — Remove the quarantine flag
xattr -dr com.apple.quarantine /Applications/Termi.appThen open the app normally.
SmartScreen may warn that the publisher is unknown. Click More info → Run anyway.
The Linux build is distributed as an AppImage — no installation required.
chmod +x termi.AppImage
./termi.AppImage- Node.js 22+
- PostgreSQL 15+ (or use Docker Compose)
# Install dependencies
npm install
# Copy and configure env
cp .env.example .env
# Edit .env — set DATABASE_URL (or DB_HOST/USER/PASS/NAME) + secrets
# Generate Prisma client
npm run db:generate
# Push schema to database
npm run db:push
# Start both services (web :22080, gateway :22081)
npm run dev:allnpm run dev:all # Web + Gateway (recommended)
npm run dev # Web only
npm run dev:gateway # Gateway only
npm run db:migrate # Create + apply a migration
npm run db:studio # Prisma Studio database browser
npm run db:seed # Seed with sample data
npm run test # Unit tests (Vitest)
npm run test:e2e # End-to-end tests (Playwright)
npm run lint # ESLint across all workspaces
npm run build # Production build
# Desktop app
npm run electron:dev # Open Electron pointing at a running web instance
npm run electron:dev:full # Run full local stack + Electron together
npm run build:electron # Package Electron apptermi/
├ apps/
│ ├ web/ # Next.js 16 App Router
│ │ ├ src/
│ │ │ ├ app/ # Pages + API routes (App Router)
│ │ │ │ ├ api/ # REST endpoints
│ │ │ │ ├ panel/ # Dashboard UI
│ │ │ │ └ tunnel/ # Same-origin HTTP reverse proxy for port-forward tunnels
│ │ │ ├ components/ # React components
│ │ │ │ ├ terminal/ # SSH/RDP/VNC/local terminal
│ │ │ │ ├ scp/ # File manager
│ │ │ │ └ monitoring/ # Metrics & charts
│ │ │ └ lib/
│ │ │ ├ auth/ # Session, TOTP, passkey, OAuth
│ │ │ ├ crypto/ # AES-256-GCM, key derivation
│ │ │ ├ security/ # SSRF protection, rate limiting
│ │ │ └ services/ # SSH pool, SFTP, monitoring, alerts
│ │ └ prisma/ # Database schema & migrations
│ │
│ ├ gateway/ # WebSocket gateway (pure ESM)
│ │ └ src/
│ │ ├ handlers/ # SSH, SCP, Guacamole (RDP/VNC), Telnet, Local PTY, Tunnel
│ │ └ auth/ # JWE token validation
│ │
│ ├ electron/ # Desktop app wrapper
│ │ ├ main.js # Electron main process + node-pty IPC
│ │ ├ preload.js # Secure context bridge
│ │ └ updater.js # Auto-update via GitHub Releases
│ │
│ └ mobile/ # Capacitor iOS/Android shell
│
├ .github/workflows/ # CI, release notes, desktop builds
├ traefik/ # Reverse-proxy configuration
├ docker-compose.yml
├ docker-compose.prebuilt.yml # Deploy from published Docker Hub / GHCR images
├ electron-builder.yml # Desktop build config (authoritative)
└ .env.example
The desktop and mobile apps are thin shells around the hosted web app, so UI changes reach them without a new release. The desktop app checks GitHub Releases for shell updates on launch and every 6 hours.
| Variable | Description |
|---|---|
DB_HOST |
PostgreSQL host |
DB_USER |
PostgreSQL username |
DB_PASSWORD |
PostgreSQL password |
DB_NAME |
Database name |
SESSION_SECRET |
iron-session cookie key (≥32 chars) |
ENCRYPTION_KEY |
AES-256-GCM key for credentials at rest (≥32 chars) |
GATEWAY_JWT_SECRET |
Shared secret for JWE connection tokens (≥32 chars) |
NEXT_PUBLIC_GATEWAY_URL |
Browser-visible WebSocket URL of the gateway |
NEXT_PUBLIC_APP_URL |
Public URL of the web app |
| Variable | Default | Description |
|---|---|---|
GUACD_HOST |
localhost |
guacd host for RDP/VNC |
GUACD_PORT |
4822 |
guacd port |
ALLOW_PRIVATE_NETWORKS |
false |
Allow connections to private/internal IPs |
TRUSTED_PROXY |
false |
Trust X-Forwarded-For (enable behind Nginx/Traefik) |
ALLOWED_ORIGINS |
NEXT_PUBLIC_APP_URL |
CORS origins for the gateway |
ALLOW_LOCAL_TERMINAL |
false |
Enable local PTY terminal on the gateway host |
GATEWAY_DETACHED_TTL_MIN |
30 |
Minutes a detached SSH session is kept alive for reconnect |
GATEWAY_MAX_CONNECTIONS_PER_USER |
0 |
Concurrent sessions per user; 0 = unlimited |
GOOGLE_CLIENT_ID |
— | Google OAuth client ID |
GOOGLE_CLIENT_SECRET |
— | Google OAuth client secret |
SMTP_HOST |
— | SMTP host for email (verification, alerts, invites) |
SMTP_USER |
— | SMTP username |
SMTP_PASS |
— | SMTP password |
VAPID_PUBLIC_KEY |
— | Web push VAPID public key |
VAPID_PRIVATE_KEY |
— | Web push VAPID private key |
See
.env.examplefor the full list with descriptions.
┌─────────────────────────────────────────────────────────────┐
│ Browser / PWA / Electron │
│ Login · Dashboard · Terminal · File Manager · Monitor │
└───────────────────────────┬─────────────────────────────────┘
│ HTTP / REST
▼
┌─────────────────────────────────────────────────────────────┐
│ Next.js Web App (:22080) │
│ API Routes │ Auth │ AES-256-GCM Crypto │ Prisma ORM │
└──────────┬────────────────────────────────┬─────────────────┘
│ │
│ SQL (pg) │ POST /api/connection/token
▼ ▼
┌──────────────────┐ ┌─────────────────────────────────┐
│ PostgreSQL DB │ │ WebSocket Gateway (:22081) │
└──────────────────┘ │ JWE token validation │
└──────────┬──────────┬───────────┘
│ │
SSH / SCP RDP / VNC
│ │
┌──────┴──┐ ┌────┴──────────┐
│ SSH Host│ │ guacd :4822 │
└─────────┘ └───────┬───────┘
│
RDP / VNC Servers
Connection flow:
- Browser calls
POST /api/connection/token→ server decrypts stored credentials and issues a short-lived **JWE token ** (A256GCM, 5-minute TTL). - Browser opens a WebSocket to the gateway with the JWE token as a query parameter and a protocol of
ssh,scp,rdp,vnc,telnet,local, ortunnel. - Gateway validates the token and routes to the matching handler:
SSHHandler,SCPHandler,GuacamoleHandler(RDP/VNC),TelnetHandler,LocalHandler, orTunnelHandler. - For RDP/VNC,
GuacamoleHandlerconnects to guacd and forwards the Guacamole protocol frames to the browser. - For port forwarding,
TunnelHandleropens an SSHforwardOutchannel and relays raw bytes over the same WebSocket — either straight to a bound local port (Electron), or through/tunnel/<serverId>/<port>, a same-origin HTTP reverse proxy that works behind a reverse proxy exposing only 80/443.
See SECURITY.md for the full security policy, vulnerability reporting process, and threat model.
Highlights:
- Credentials encrypted with AES-256-GCM before database storage
- SSRF protection on all user-supplied host inputs
- Rate limiting on authentication endpoints
- CSP and security headers on every response
- Session tokens revocable per-device
Planned for the next update — scope and details are still open to change:
- Bring-your-own-provider, not a hosted default. An API key field for a provider you already have (OpenAI, Anthropic, or a local/self-hosted model via an OpenAI-compatible endpoint), consistent with Termi's no-telemetry, self-hosted-first design. Prompts/responses go only to the provider you configure — never through a termi-operated relay.
- Terminal-grounded, not a generic chatbot. Explain a confusing command's output, translate a plain-English request into the right shell command for review before running it, or summarize a long session recording instead of scrubbing through the replay.
- Monitoring-aware. Answer questions like "why is this server flagged high-load" using real metrics/health-history/benchmark-trend data already collected, instead of the user cross-referencing charts manually.
- Read-only first. The first iteration explains and suggests; it does not execute anything on its own. Any command it proposes goes through the same explicit-confirm step a human typing it would — no silent execution, matching the project's existing "ask before anything destructive" posture.
- Later, not v1: webhook alert message drafting, anomaly explanations for monitoring alerts.
This is a draft plan, not a committed spec — open a discussion if you have thoughts on scope or provider support before it's built.
Contributions are welcome! Please read CONTRIBUTING.md for guidelines on:
- Setting up the development environment
- Branching and commit conventions
- Submitting pull requests
- Reporting bugs and requesting features
- Contribution guide: CONTRIBUTING.md
- Code of conduct: CODE_OF_CONDUCT.md
- Help & support channels: SUPPORT.md
- Security policy: SECURITY.md
- Release notes: CHANGELOG.md
- Full documentation: Wiki
Cloning gets you the code. Starring is the only action that shows up publicly — it's what surfaces Termi in GitHub's trending pages, in search ranking, and in other developers' recommendations. A clone or a fork is invisible outside your own account; a star is a two-second, zero-cost signal that tells the next person searching "self-hosted SSH client" that this project is worth a look.
If you've already cloned or forked Termi and it's useful to you, this is the one extra click that actually helps the project grow:
Stars also directly inform what gets built next — they're the closest thing this project has to a roadmap signal from real users.
This project is licensed under the MIT License — see LICENSE for details.
- xterm.js — terminal emulator
- Apache Guacamole — RDP/VNC gateway
- ssh2 — SSH client for Node.js
- node-pty — local PTY for Electron
- Next.js — React framework
- Prisma — database ORM
- Tailwind CSS — utility-first CSS
- iron-session — session management
If Termi saves you time, consider supporting its development:
Made with ❤️ for the self-hosting community

