A modern, high-performance visual dashboard and orchestration suite for servers and VPS. Seamlessly manage PM2 application processes, inspect and control Docker & Podman containers (workloads, images, volumes, networks), monitor live CPU/RAM metrics via SSE, stream real-time logs, scan and free network ports, and automate GitHub deployments — all from a unified, sleek web interface.
📖 Visual Modules Guide: For walkthroughs and screenshots of all modules (Projects, Containers, Nginx Reverse Proxy, Port Manager, User Management, Teams, and Audit Logs), check out the Modules Guide.
- Nginx Engine & Reverse Proxy Manager — Visual overview of gateway configurations and
/etc/nginx/conf.d/apps/reverse proxies, click-to-edit with automatednginx -tvalidation, hot daemon reloading (nginx -s reload), and template scaffolding (admin only, see docs/modules-guide.md#7-nginx-engine--reverse-proxy-manager) - Container & Workload Management (Docker & Podman) — Native socket inspection and lifecycle control for containers, images, volumes, and networks with real-time CPU/memory stats and log streaming
- Container Watchdog & Alerts — Background watchdog that monitors critical containers and delivers immediate alerts through Email (SMTP) and Telegram
- Authentication — Email/password with Better Auth, Google sign-in, and password reset (email or console fallback)
- Dashboard — Overview of all PM2 processes with real-time status
- Project Cards — Beautiful cards showing CPU, RAM, uptime, and status
- Multi-process Groups — Monorepo/workspace projects (e.g. Atlas) grouped as a single card across all flows (see docs/multi-process-groups.md)
- Favorites — Star projects to quickly find them, filter by favorites in the list
- Projects — Restart, stop, delete processes from the UI (restart falls back to recreate on failure); favorites filter with collapsible Favorites/Others sections
- Process Actions — Restart, stop, and delete processes directly from the UI (restart falls back to recreate on failure)
- Admin Process Registration — Register unregistered PM2 processes as projects with team/member assignment (admin only)
- PM2 Save & Startup — Persist the process list (
pm2 save) and enable boot startup (pm2 startup) from the UI (admin only) - One-Click Update — Pull, rebuild, and restart the app from the UI (admin only)
- Logs — Efficient
tail -nreading, content-based level classification (info/warn/error), datetime-range filter, "Load more", and log clearing - Log Viewer toolbar — Reorganized into two aligned rows with a newest↔oldest sort order toggle, new-error highlighting, and dismiss
- Real-time Metrics — Push-based CPU/RAM updates every 10s via SSE (live-only, no DB persistence)
- Environment Variables — View, edit, add, and delete env vars (applied on next deploy)
- Env Import — Import environment variables from a local
.envfile with folder picker and paste-to-split rows - GitHub Integration — Connect a GitHub App, list/import accessible repositories, skip-install option, zero-dependency detection, and multi-app ecosystem detection (see docs/github-integration.md)
- Auto-deploy — Trigger full deploys (git → install → build → pm2 restart) from GitHub push webhooks, with email notifications and per-stage history (see docs/auto-deploy.md)
- Deploy All — Sequentially deploy every online process from one button
- Deploy Confirmation — Confirmation modal before Deploy All to prevent accidental bulk deployments
- Structured Logging — Production-ready structured JSON logging powered by Pino with clean console output for local development
- Security & Hardening — OWASP Top 10 hardening, HTTP security headers (CSP, X-Frame-Options), strict RBAC route guards, environment variable masking, and CSV formula sanitization (see SECURITY.md)
- Teams — Manage teams, invite members, assign roles (team_owner, team_admin, team_member), team-based project access
- Project Sharing — Invite users with owner/editor/viewer roles, assign projects to teams (see docs/sharing-permissions.md)
- Metrics Dashboard — Visual CPU/RAM bars, aggregated stats
- Metrics Recording — Persistent metrics snapshots stored in the database with repository layer for historical queries
- Port Manager — Scan system ports in use (TCP/UDP), search/filter by port/process/address, and free ports with OTP email verification before killing (admin only)
- Admin Panel — Manage users, teams, and audit logs; role-based access control (see docs/sharing-permissions.md)
- Audit Logs — Append-only trail of admin actions with filters (action/actor/date), pagination, and CSV export (see docs/audit-module.md)
- Dark/Light Mode — Toggle between themes with smooth transitions
- Premium Animations — Page transitions, staggered lists, smooth tab switching
| Layer | Technology |
|---|---|
| Framework | SvelteKit 2.x + Svelte 5 (runes) |
| Language | TypeScript |
| Auth | Better Auth (pluggable — see Auth Providers) |
| Database | PostgreSQL or SQLite/Turso (see Database) |
| ORM | Drizzle ORM |
| Validation | Zod |
| Styling | Tailwind CSS |
| Real-time | Server-Sent Events (SSE) |
| Containers | Dockerode (Docker & Podman socket API) |
| Logging | Pino (Structured JSON logging) |
| Testing | Vitest |
Built with Screaming Architecture — organized by domain, not by technical layer:
src/lib/
├── auth/ # Authentication domain (pluggable providers)
│ ├── provider.interface.ts
│ ├── providers/ # Auth implementations (better-auth, etc.)
│ └── factory.ts # Provider registry
├── db/ # Database domain (dialect-agnostic)
│ ├── driver.interface.ts
│ ├── drivers/ # DB implementations (libsql, postgres)
│ ├── dialect-registry.ts # Extensible dialect detection
│ ├── factory.ts # Driver factory
│ ├── schema/ # Drizzle schema definitions
│ └── repositories/ # Data access implementations
├── sse/ # Real-time communication
│ ├── sse-manager.ts # Server-side connection manager
│ ├── client.ts # Browser EventSource wrapper
│ ├── server.ts # Server-only exports
│ ├── metrics-emitter.ts
│ └── status-watcher.ts
├── services/ # Service container (DI factory)
├── containers/ # Container client models, formatting & types
├── server/containers/ # Docker & Podman engine, watchdog, alerts, settings
├── ports/ # Port scanning + OTP-verified kill (see docs/port-manager.md)
├── nginx/ # Nginx gateway & reverse proxy management domain
├── logger/ # Structured logging
├── rate-limiter/ # In-memory rate limiting
├── pagination/ # Pagination types and helpers
├── utils/ # Shared utilities (status, format, validation, shell)
├── projects/ # Projects domain
├── pm2/ # PM2 process manager domain
├── metrics/ # Metrics & monitoring domain
├── env-vars/ # Environment variables domain
├── ui/ # Shared UI components
└── config/ # Configuration
Patterns used:
- Repository Pattern — Interfaces decouple domain from implementation
- Service Layer — Business logic encapsulated in services
- Dependency Injection — Centralized
createServices()factory - Registry Pattern — Extensible driver/provider selection (Open/Closed)
- Interface-first — Contracts defined before implementations
- Client/Server Split — Clean separation of browser and Node.js code
- Node.js 20+
- pnpm (or npm/yarn)
- PM2 installed globally (
npm i -g pm2) - Optional: Docker or Podman daemon running (for container features)
# Clone the repository
git clone <your-repo-url>
cd pm2-view
# Install dependencies (committed lockfile is pnpm-lock.yaml)
pnpm install
# Copy environment variables
cp .env.example .env
# Edit .env with your database and auth configurationDo this first: after setup, promote a user to admin (see Create an Admin Account) — every new account starts with the
userrole and there is no bootstrap.
All new users are created with the user role by default, so you must promote your first user to admin. The user must already exist (registered via the app) before running this.
npm run make-admin <email>
# Example:
npm run make-admin admin@example.comThis runs a one-off script (src/lib/server/migrations/make-admin.ts) that updates the role column to admin in the database. It prints what it did: user not found, already admin, or promoted.
Note: The script connects via
TURSO_DATABASE_URL/TURSO_AUTH_TOKEN(SQLite/Turso). It does not work against a PostgreSQLDATABASE_URL— run it against your SQLite/Turso database instead.
What admins can do:
- Access the
/adminpanel (users, teams, audit logs, roles) - Create users, change roles, ban/unban, and delete users
- See all projects (admin bypasses project-level access checks)
- Access the
/portsmanager and terminate ports with OTP email verification - Manage container watchdog alerting settings
- Run admin-only PM2 operations: PM2 Save / PM2 Startup, and the Update button
Safety guards: an admin cannot change their own role, and the last remaining admin cannot be demoted, banned, or deleted (HTTP 409).
PM2 View supports multiple database backends through a driver abstraction. The dialect is auto-detected from the connection URL.
SQLite / Turso (default):
DATABASE_URL=libsql://your-database.turso.io
TURSO_AUTH_TOKEN=your-auth-tokenPostgreSQL:
DATABASE_URL=postgres://user:password@localhost:5432/pm2viewLocal SQLite file:
DATABASE_URL=file:./data/local.dbTo add a new database driver, register a dialect rule and driver in the factory — no existing code needs modification.
The default auth provider is Better Auth. All auth endpoints live under /api/auth/*. Sessions last 30 days with a 5-minute cookie cache.
Google OAuth is configured via the Better Auth socialProviders block (src/lib/auth/auth.ts):
GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-client-secret
VITE_ALLOWED_HOSTS=localhost,your-domain.comSetup in Google Cloud Console:
- Create OAuth 2.0 credentials (APIs & Credentials → Create Credentials → OAuth client ID).
- Add the Authorized redirect URI:
{BETTER_AUTH_URL}{base}/api/auth/callback/google, wherebaseis the app's base path (fromAPP_BASE_PATH, empty by default,/pm2in this deployment)- Dev:
http://localhost:5179/api/auth/callback/google - Production (base
/pm2):https://engine.clinicamedicos.com/pm2/api/auth/callback/google
- Dev:
- Add
http://localhost:5179and your production origin to Authorized JavaScript origins if required.
BETTER_AUTH_URLmust be the origin only — no base path, no trailing slash. The app appends the base path itself. Setting it tohttps://engine.clinicamedicos.com/pm2produces a doubled path (.../pm2/pm2/api/auth/...) andredirect_uri_mismatch. The full redirect URI you register in Google does include the base path.
VITE_ALLOWED_HOSTS (comma-separated) controls better-auth's trustedOrigins, used to validate the OAuth callback URL (open-redirect protection) and CSRF origin checks. The Google button appears on the login page only — first-time Google users get an account created automatically (no separate registration step).
Forgot your password? The login page links to /forgot-password, which sends a signed reset link valid for 1 hour. The email is sent via SMTP (see env vars below); if SMTP is not configured, the reset link is printed to the server console instead.
- Reset links are signed with
BETTER_AUTH_SECRETand expire after 1 hour (resetPasswordTokenExpiresIn). - Changing the password revokes all existing sessions (
revokeSessionsOnPasswordReset), so the user must sign in again. - The forgot-password page never reveals whether an account exists (privacy-preserving message).
To enable email delivery, configure SMTP:
SMTP_HOST=smtp.gmail.com
SMTP_PORT=465
SMTP_SECURE=true
SMTP_USER=you@example.com
SMTP_PASS=your-app-password
SMTP_FROM_EMAIL=you@example.com
NOTIFICATION_CHANNELS=nodemailer # default if unsetGmail works with an app password. If
SMTP_HOSTis unset, no email is sent and reset links only appear in the server logs.
To swap to a different provider:
// src/lib/auth/factory.ts
import { MyAuthProvider } from "./providers/my-auth.provider";
registerAuthProvider("my-auth", () => new MyAuthProvider());Then set AUTH_PROVIDER=my-auth in your environment.
# Database (required) — dialect auto-detected from URL
DATABASE_URL=libsql://your-database.turso.io
TURSO_AUTH_TOKEN=your-auth-token # Only for libsql/Turso
# Better Auth (required for default provider)
BETTER_AUTH_URL=http://localhost:5179
BETTER_AUTH_SECRET=your-secret-key
# Optional — forces the public origin when behind a reverse proxy.
# Without it, adapter-node derives the origin from request headers; if the
# proxy does not forward the correct Host/X-Forwarded-* headers, better-auth
# rejects requests (404 on /api/auth/*) because the perceived origin no longer
# matches BETTER_AUTH_URL. Set to the same origin as BETTER_AUTH_URL.
# ORIGIN=https://engine.clinicamedicos.com
# Auth Provider (optional — defaults to 'better-auth')
AUTH_PROVIDER=better-auth
# Google OAuth (optional — enables "Sign in with Google")
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
VITE_ALLOWED_HOSTS=localhost
# Email & Alerting (optional — password reset delivery & watchdog alerts)
SMTP_HOST=
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=
SMTP_PASS=
SMTP_FROM_EMAIL=
NOTIFICATION_CHANNELS=nodemailer
# Telegram Alerts (optional — container watchdog notifications)
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
NOTIFY_TO=
# Container Daemon (optional — auto-detected via sockets if unset)
DOCKER_HOST=
CONTAINERS_SOCKET=
# PM2 (optional)
PM2_HOST=localhost
PM2_PORT=4322
# Logging (optional)
DEBUG=true # Enable debug-level logging# Development
pnpm dev
# Build for production
pnpm build
# Preview production build
pnpm preview
# Run tests (vitest)
pnpm vitest run
# Type check
pnpm check# Push schema to your database
npx drizzle-kit push
# Generate migrations
npx drizzle-kit generateManage your server's container ecosystem directly from /container without SSH access:
- Multi-Engine Support: Automatically discovers and communicates with Docker (
/var/run/docker.sock) or rootless/root Podman (/run/podman/podman.sock) sockets. - Containers: Start, stop, restart, delete, inspect metadata, view live CPU/memory charts, and stream container logs in real time.
- Images: Browse stored images, inspect layers and tags, and remove unused images.
- Volumes & Networks: Inspect volume mount points, network drivers, IP subnets, and attached containers.
- Watchdog Service: An integrated background monitor that watches container health and dispatches instant incident notifications via SMTP email and Telegram.
Connect a GitHub App to let users import repositories. Requires one GitHub App (not a PAT, not an OAuth App) and 6 environment variables:
GITHUB_APP_ID=1234567
GITHUB_APP_SLUG=your-app-slug
GITHUB_CLIENT_ID=your-client-id
GITHUB_CLIENT_SECRET=your-client-secret
GITHUB_WEBHOOK_SECRET=your-webhook-secret
GITHUB_PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA...\n-----END RSA PRIVATE KEY-----There's one database table to create (github_installations) — run pnpm db:migrate.
Full step-by-step setup (creating the GitHub App, callback URLs, webhook, permissions, key format, migration, and troubleshooting): see docs/github-integration.md.
Admin-only operations backed by the PM2 CLI, exposed in the Projects page header (visible only to users with the admin role). The underlying PM2 commands can also be run directly in your terminal.
The PM2 Save button runs pm2 save, persisting the current process list as the "dump file" so PM2 can restore it later.
- Opens a modal, runs the command, and shows the output.
- A success banner ("Process list saved") confirms the dump was written.
Run this after adding/removing processes to keep the resurrection list up to date.
The PM2 Startup button enables PM2 to start processes automatically on boot.
- It runs
pm2 startup(quietly) and extracts the generated copy/paste command (asudo ... pm2 startup systemd ...line). - The modal shows that command with a Copy command button — you can run it yourself in a terminal — or click Apply here to run it in-app.
- To apply in-app, enter the sudo password (sent to the command's stdin via
sudo -S; never stored or placed in argv). The output streams live as NDJSON (/api/pm2/system?action=apply-startup). - On success it verifies the service with
systemctl is-enabled '<service>'(8s timeout) and reports the service name (e.g.pm2-<user>.service).
Security: the startup command is validated before running — it must be a single-line sudo invocation containing pm2 startup, blocking injection.
Note:
pm2 startupprints the copy/paste command and exits with a non-zero code as its normal success path, so the app treats "command extracted" as success, not the exit code.
The Update button in the app header (admin only) pulls and rebuilds the app:
git pull && pnpm build && pm2 restart pm2-viewFlow:
POST /api/updaterunsgit pullthenpnpm build(each fails the update if it errors).- After a 1.5s delay (to let the HTTP response flush), a detached background process runs
pm2 restart pm2-view. - The UI shows a success banner and a 30-second countdown before reloading the page — this avoids the nginx 502 that appears while PM2 is starting the new build.
Note: The app name
pm2-viewis hardcoded. If you run the app under a different PM2 name, restart manually after updating.
PM2 View uses Server-Sent Events for real-time updates:
- Metrics: CPU/RAM updates every 10 seconds
- Process Status: State change notifications (online → stopped → error)
- Deploy Logs: Per-stage log lines streamed during a deploy
Project log viewing is separate from SSE — it reads the PM2 log files with tail -n (fast, only the last N lines) and classifies each line by content level. See Logs.
The SSE endpoint is at /api/sse. Connect from any browser:
const es = new EventSource("/api/sse");
es.addEventListener("metrics", (e) => console.log(JSON.parse(e.data)));
es.addEventListener("process-status", (e) => console.log(JSON.parse(e.data)));
es.addEventListener("deploy-log", (e) => console.log(JSON.parse(e.data)));| Token | Hex | Usage |
|---|---|---|
| Base | #0A0E17 |
Page background |
| Surface | #0F1623 |
Secondary surfaces |
| Card | #141D2F |
Card backgrounds |
| Accent 1 | #CAF8FF |
Lightest accent |
| Accent 3 | #38CDFF |
Primary interactive |
| Accent 4 | #009DCD |
Buttons, links |
| Accent 6 | #005C79 |
Dark accent |
- Headings: Roboto (300, 400, 500, 700)
- Body: Poppins (300, 400, 500, 600, 700)
- Page navigation: 350ms slide-up fade
- Tab switching: 300ms slide-up
- Theme toggle: 400ms crossfade
- List items: 50ms stagger delay
For a full visual walkthrough with screenshots of every module (Projects Dashboard, Containers & Workloads, Port Manager, User Management, Teams, and Audit Logs), see the Modules Guide.
# Run all tests
pnpm vitest run
# Run with coverage
pnpm vitest run --coverage
# Watch mode
pnpm vitestpm2-view/
├── src/
│ ├── lib/
│ │ ├── auth/ # Auth domain (pluggable)
│ │ ├── db/ # Database (dialect-agnostic)
│ │ │ ├── schema/ # Drizzle schema (projects, teams, favorites, etc.)
│ │ │ └── repositories/ # Data access implementations
│ │ ├── sse/ # Real-time SSE communication
│ │ ├── services/ # DI factory
│ │ ├── containers/ # Container models and helpers
│ │ ├── server/ # Server-side engines (Docker/Podman) & background watchdog
│ │ ├── ports/ # Port scanning + OTP-verified kill
│ │ ├── logger/ # Structured logging
│ │ ├── rate-limiter/ # Rate limiting
│ │ ├── pagination/ # Pagination helpers
│ │ ├── utils/ # Shared utilities
│ │ ├── projects/ # Projects domain
│ │ ├── pm2/ # PM2 domain
│ │ ├── metrics/ # Metrics domain
│ │ ├── env-vars/ # Env vars domain
│ │ ├── ui/ # UI components
│ │ └── config/ # Configuration
│ ├── routes/
│ │ ├── (auth)/ # Login, register
│ │ ├── (app)/ # Protected routes (projects, container, teams, ports, admin)
│ │ └── api/ # API endpoints (/api/sse, /api/containers, /api/images, /api/ports, etc.)
│ ├── app.css # Global styles
│ └── app.html # HTML shell
├── drizzle/ # Migrations
├── drizzle.config.ts # Drizzle config
├── tailwind.config.ts # Tailwind config
├── svelte.config.js # SvelteKit config
├── vite.config.ts # Vite config
└── vitest.config.ts # Vitest config
- OWASP Top 10 Hardened — Comprehensive security protections across all layers (see SECURITY.md)
- HTTP Security Headers —
Content-Security-Policy,X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy, andPermissions-Policyinsrc/hooks.server.ts - Granular Route Guards (RBAC) — Endpoints enforce authentication and authorization (
requireAuth,requireAdmin,requireProjectAccess,requireProjectRole) across PM2 process actions, deployment triggers, env vars, logs, and SSE - Passwords & Sessions — Passwords hashed by Better Auth (bcrypt); HTTP-only, secure, SameSite cookies with CSRF validation
- Shell Command Sanitization — Shell commands sanitized with
escapeShellArg()to prevent command injection - PM2 Startup Hardening — PM2 system startup commands are strictly validated against strict regex and platform whitelists before execution
- Environment Secrets Protection — Sensitive environment variable values are masked by default in the UI with interactive reveal controls
- CSV Formula Injection Mitigation — Audit export spreadsheets sanitize formula characters (
=,+,-,@) - Rate Limiting — In-memory sliding-window rate limiting (100 req/min per IP) on critical endpoints (
/projects/api,/api/logout,/api/ports*) - Team-based Access Control — Users only see projects they own or have team access to; team detail pages reject non-members with 403
See SECURITY.md for full security policy and vulnerability reporting.
See CONTRIBUTING.md for development workflow and guidelines.
Copyright (c) 2026 Jerson Tapias, operating as Camidev
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files, to use, copy, modify, merge, publish, and distribute the Software for non-commercial purposes only.
Commercial use is strictly prohibited without prior written permission from the copyright holder.
See LICENSE for the full license text.
