Skip to content

Latest commit

 

History

40 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

myOps

English · Tiếng Việt · 中文 · 한국어 · 日本語

Self-hosted control panel for your own Linux and macOS servers: live metrics, nginx sites, pm2 apps, Docker, databases, cron, firewall, backups, an SFTP file browser and a full SSH terminal - all in the browser, over nothing but SSH.

Next.js 16 (App Router) embedded in a custom Hono server, data in SQLite through Prisma.

Why it is different

  • SSH only. No agent daemon calls home, no extra port to open, no cloud account. myOps connects out to your machines; the box running myOps does not need a public address.
  • One binary-free agent. The optional metrics agent is a POSIX shell script - no runtime to install on the target.
  • Never OS-sniffing. Every system-dependent command is built as a POSIX if/elif chain and executed on the target, so the same code works on Debian, RHEL, Arch, Alpine, SUSE and macOS.
  • Nothing rendered from the server's words. The API returns message keys, the UI translates them, so every screen exists in Vietnamese, English, Chinese, Korean and Japanese.

Screenshots

Server overview
Overview - live CPU/RAM/disk/network, the service stack probed over SSH, recent activity
Node.js and pm2 apps
Apps - Node.js versions through nvm and every pm2 process, in dark theme
CI/CD runners
Runners - install and register GitHub Actions and GitLab CI runners on the server
Global settings
Settings - master password, SMTP and Telegram, web push, encrypted config backup

Features

Area What you get
Servers Card grid of the whole fleet with status, CPU/RAM/disk and tag filtering; add/edit/delete servers with password or SSH key (passphrase supported), connection test with typed errors before saving
Overview System info, live charts, service stack state, pending package updates, swap management, reboot, ad-hoc command, recent activity
Metrics Agent samples every 15s on the target, myOps pulls over SSH every 30s; CPU/RAM/network/load charts from 1 hour to 7 days
Sites (nginx) Create proxy, static and redirect vhosts, edit the raw vhost, enable/disable, config test and reload, Let's Encrypt issue/renew or import your own certificate
Apps (pm2) Deploy from a Git repo, start/stop/restart/reload, scale instances, save and resurrect the process list, live log tail per app, install pm2 and its startup unit
Docker Containers with actions and inspection, images with update check and pull, volumes, networks, disk usage, prune, plus Compose stacks with an inline compose editor and stack logs
Databases MySQL/MariaDB, PostgreSQL, MongoDB and Redis, installed either natively or as a container; create and drop databases, dump, flush Redis, install and uninstall the engine
Cron List, create, edit, enable/disable, run now and delete jobs per user, with a human-readable schedule description and per-job log
Firewall ufw or firewalld rules, service control, fail2ban jails with unban, and generated hardening tips
Users System accounts, groups, home and shell, password reset, authorized SSH keys per user
Runners Register and control GitHub Actions and GitLab CI runners as services
Backup restic repository setup, scheduled and manual runs, optional database dump, snapshot browse, restore, export and forget
Uptime HTTP/TCP checks run from the myOps host: status code, body match, response time, 24h uptime and TLS expiry; alerts on down and on recovery
Files Browse, view and edit text files, drag-and-drop upload, download, rename, chmod, delete - all over SFTP
Terminal Real SSH shell over WebSocket + xterm.js, auto resize, tabs kept alive across route changes, session limits and idle timeout
Logs Discover log sources (journald or plain files), live tail over WebSocket, filter by level and search, download
Ports Listening ports and top CPU processes, kill by PID (myOps refuses to kill its own SSH session)
Disk Filesystems with usage and inodes, block devices, a du browser that finds what fills a folder, one-click cleanup of caches and rotated logs, and partition + format + mount for an unused disk
Fleet Pick servers by tag or by hand and run one action across all of them (custom command, OS update, reboot, nginx/pm2 reload, Docker prune, backup) with a separate job and log per server
Maintenance Scheduled mute windows for one server or the whole fleet, suppressing every notification in that period
Alerts Threshold rules per metric with email (SMTP), Telegram and web push delivery, fired once per breach
Settings Master password, SMTP and Telegram credentials with a test button, push notifications, reveal and copy the master key and the MCP token, encrypted export/import of the whole configuration
MCP Model Context Protocol endpoint at /mcp: an AI assistant can inspect metrics, subsystem state and logs across the fleet, and restart, back up, update and deploy on it; token generated on first run
Security Single master password (argon2id), httpOnly session cookie, SSH credentials encrypted with AES-256-GCM, audit log for every state-changing operation
Extras Installable PWA with web push, Cmd/Ctrl + K command palette across servers and screens, light/dark theme, five languages

Architecture

Browser ──http──► Hono ──► /api/*         REST
        └──ws────►      ──► /ws/terminal       ⇄ SSH shell
                        ├─► /ws/agent-install  agent install log
                        ├─► /ws/app-logs       live log stream
                        └─► everything else → Next.js

MCP client ──http──► Hono ──► /mcp        read + operate tools (bearer token, generated on first run)

myOps ──ssh──► target: myops-agent (systemd timer / cron)
                        └─ writes /var/lib/myops-agent/metrics.jsonl (ring buffer)

The agent never calls out - myOps SSHes in and reads the file, so the myOps host needs no public address and the target opens no port beyond SSH. While myOps is down the agent keeps sampling; the next pull picks up from a stored cursor, so the charts have no holes.

Path Role
server.ts Custom server: Hono takes the request, steals /ws/* upgrades, forwards everything else (HMR included) to Next; also starts the scheduler
src/server/api.ts Hono app on /api basePath, session middleware, central error handler, Server-Timing
src/server/routes/ HTTP and WebSocket layer only: auth, servers, settings, metrics, fleet, ops, files, terminal, agent, log-tail, mcp
src/server/services/ Every command that touches a target machine: ssh, shell, platform, jobs, cache, crypto plus one service per domain
src/server/mcp/ MCP tool surface built on the services - tools.ts reads, actions.ts writes - served at /mcp while MYOPS_MCP_TOKEN holds a value
agent/myops-agent.sh POSIX shell agent, no runtime required on the target
prisma/schema.prisma Setting, Session, Server, Credential, MetricSample, Job, AuditLog, SslCert, UptimeCheck, UptimeSample, Maintenance, BackupConfig, NotifyRule, PushSubscription
src/app/(app)/ Authenticated route group: fleet dashboard, per-server screens, fleet run, maintenance, global settings
src/components/ui/ The design system - one component per file, re-exported from index.ts
messages/ vi/en/zh/ko/ja.json, identical key sets, grouped by screen

Layering rule: routes contain no SSH logic, and the client never imports a service except as a type.

Getting started

The fastest way to try it, no checkout involved:

npx @hyydev/myops              # http://localhost:3000

Installing it globally (npm i -g @hyydev/myops) puts the shorter myops command on the PATH.

State goes to ~/.myops - the .env with the master key and the SQLite file - so it survives every update. PORT and HOSTNAME override the defaults, and myops --help lists the rest.

From source, for development or a permanent install:

git clone <repo> myops && cd myops
yarn install
yarn dev                          # http://localhost:3000

That is the whole setup. yarn dev and yarn start both create .env from .env.example, generate a MYOPS_MASTER_KEY if there is none, and apply migrations before the server comes up (see Database). An existing key is never regenerated - a new one would make every stored credential undecryptable.

Back that key up. It encrypts every SSH credential myOps holds, and there is no recovery path. Keep a copy somewhere other than next to prisma/dev.db.

On the first run myOps redirects to /setup to set the master password. Then: add a server → test the connection → install the agent.

Requirements

  • Node.js 20 or newer on the myOps host
  • SSH access to each target, with a POSIX shell and sudo/root for state-changing operations

myOps runs non-interactive commands and therefore uses sudo -n: the SSH account must be either root or hold passwordless sudo. On most Linux VPSes it is already root and nothing is needed. On macOS this must be configured, since SSH login as root is disabled by default and an admin account is still prompted for a password:

# on the target macOS machine
sudo visudo -f /etc/sudoers.d/myops     # add: <user> ALL=(ALL) NOPASSWD: ALL
sudo systemsetup -setremotelogin on     # enable Remote Login if it is off

When passwordless sudo is not an option, myOps can send the password instead. A password login reuses its own password automatically; a key login can fill in Sudo password on the server form. Either way the secret is encrypted like any other, never returned by the API, and piped to sudo -S through stdin, so it never appears on a command line or in the sudo log.

Nothing is sent blindly: Test connection reports what the account can actually do - root, passwordless sudo, sudo with a password, or no sudo at all - and myOps only falls back to sudo -S on the machines that really prompt.

Supported systems

Every Linux distribution with SSH, plus macOS. myOps does not detect the OS up front: each system-dependent command is built as a POSIX if/elif chain in src/server/services/platform.ts and the target picks its own branch within a single round trip.

Layer Support
Package manager apt-get, dnf, yum, zypper, pacman, apk, Homebrew
Init system systemd, OpenRC, launchd (through brew services), sysvinit
Firewall ufw (Debian, Arch), firewalld (RHEL, SUSE), application firewall (macOS)
Cron cron, crond, cronie, busybox crond
System logs journald, or /var/log/syslog · /var/log/messages without systemd
Machine metrics /proc on Linux; sysctl + vm_stat + netstat + top on macOS
Architecture x86_64 and aarch64/arm64 (Apple Silicon included)

A few gaps come from upstream, not from myOps:

  • MongoDB ships official repositories for deb and rpm only; on Arch and Alpine use the Docker mode.
  • The GitHub Actions runner registers itself as a systemd service, so that button only works on a systemd machine.
  • The Users screen relies on the shadow toolchain (useradd, usermod, chpasswd); Alpine needs the shadow package and macOS uses dscl, so the screen is not available there yet.
  • Nginx: only Debian ships sites-enabled by default. When creating a site myOps drops an include line into conf.d if nginx.conf lacks one, so vhosts work on every distribution. On macOS Homebrew keeps vhosts in $(brew --prefix)/etc/nginx/servers; myOps counts them but the Sites screen still writes with the Linux layout.
  • Firewall, backup and certificates on macOS are read-only for now: the action buttons still speak ufw/firewalld and certbot.

Three screens are Linux-only - Firewall, Users and CI/CD runners - so when a server reports anything else (macOS, BSD, Windows) the matching sidebar entry is greyed out and inert, the command palette drops it, and a hand-typed URL lands on a screen explaining why. A server whose OS the agent has not reported yet keeps every screen: myOps never hides one on a guess.

Agent

Install it from the UI (Overview tab → Install / update agent). myOps will:

  1. Copy agent/myops-agent.sh to /usr/local/bin/myops-agent over SFTP.
  2. Create myops-agent.service + .timer and systemctl enable --now it (15s period).
  3. Fall back to cron running four times a minute when the target has no systemd.
  4. Record the login user in /var/lib/myops-agent/owner (root-owned, 0644). The agent runs as root from a timer or cron, where SUDO_USER is empty, so without that file it cannot reach the nvm, Homebrew or Docker Desktop binaries that live in the login user's home.

The agent only reads /proc/stat, /proc/meminfo, /proc/net/dev, /proc/loadavg, /proc/uptime and df -P / (the macOS branch uses sysctl, vm_stat, netstat -ib and top), then appends to a 2000-line ring buffer (~400KB) and trims itself. Uninstalling is one button.

A server without the agent still reports RAM/disk/load through one SSH command per cycle; only CPU% stays empty, since it needs two consecutive samples.

MCP server

myOps can expose the fleet to an AI assistant over the Model Context Protocol, so a model can look at what is actually running instead of being told about it - and fix the small things without a human relaying commands.

Most tools only read. Ten of them operate the fleet, and what they can do is bounded on purpose: restart apps, containers, stacks and services, toggle sites and cron jobs, run a backup, issue certificates, install updates and deploy. Nothing deletes a site, app, container, database or user, nothing reboots a machine, and there is no arbitrary-command tool. Every action is written to the audit log, and the ones that can interrupt a service are marked destructiveHint so a client can ask a human first.

The token is generated on first run, exactly like the master key, and written to .env as MYOPS_MCP_TOKEN. You never have to produce one by hand: Connect MCP on the home page shows the ready-made client config and the CLI command with the token already in them, and Settings -> Keys shows the raw value.

To switch the endpoint off, clear the value and restart. myOps only generates a token when the line is missing from .env altogether, so an empty one is read as a deliberate opt-out and never refilled. A value shorter than 24 characters logs a warning and leaves /mcp returning 404.

Point a client at http://localhost:3000/mcp with Authorization: Bearer <token>. For Claude Code:

claude mcp add --transport http myops http://localhost:3000/mcp \
  --header "Authorization: Bearer $MYOPS_MCP_TOKEN"
Tool What it answers
list_servers The whole fleet with its latest CPU/RAM/disk sample. Start here - it returns the serverId every other tool takes
server_detail Connection details, hardware, OS, installed stack and the per-section counters
server_metrics CPU/RAM/swap/disk/load/network series over 1h–7d, read from the myOps database
server_state One subsystem probed live over SSH: nginx, apps, docker, databases, firewall, cron, node, ports, disk, runners, users, backup, swap, sshKeys
check_updates Pending OS packages, how many are security fixes, outdated stack components and whether a reboot is due
monitoring Uptime checks with their last result and TLS expiry, alert rules and whether they are firing, active maintenance windows
uptime_history The recorded probe results of one uptime check: up/down, response time, status code
log_sources / read_logs Discover what can be tailed, then tail it filtered by level, substring and time window
app_logs / container_logs The tail of one pm2 application or one docker container
list_files One directory over SFTP - names, sizes, permissions, mtime. File contents are deliberately not exposed
list_jobs / recent_activity Background jobs with their captured output, and the audit trail of state-changing operations
fleet_overview The fleet as a whole: tags in use, and recent runs of an action executed across several servers

Tools that change something:

Tool What it does
app_action / container_action start, stop, restart, reload a pm2 app; start, stop, restart, pause, unpause a container
stack_action up, start, stop, restart or pull a docker compose stack
site_action / nginx_action enable or disable a vhost; test, reload or restart nginx
cron_action enable, disable or run a cron job now
run_backup run the configured restic backup, optionally dumping databases first
issue_cert issue a Let's Encrypt certificate for a domain, or renew everything due
update_packages / deploy_app install pending updates, or redeploy a pm2 app - both as background jobs you follow with list_jobs

Two things worth knowing before you enable it:

  • Log output is untrusted input. Container names, process names and log lines come from software you do not control, and they end up in a model's context - next to tools that restart and deploy. A log line that reads like an instruction is not one. What bounds the damage is the action list: nothing there deletes data or runs an arbitrary command, and every call is audited. If you want the older guarantee that a prompt injection has nothing at all to reach for, keep the endpoint off.
  • The token is a fleet-wide operator credential. It exposes hostnames, open ports, installed packages and log contents for every server, and it can restart and deploy on all of them. Treat it like the master password, and do not put the endpoint on a public address without HTTPS in front of it.

Internationalisation

Built on next-intl without i18n routing - the locale comes from the myops_lang cookie instead of a URL prefix, so every path stays the same. Vietnamese is the default; English, Chinese, Korean and Japanese ship with it.

// Client component
const t = useTranslations("firewall");
t("ufwSummary", { fw: "UFW", status: "on", rules: 12, jails: 3 });

// Server component
const t = await getTranslations("meta");

Strings live in messages/<locale>.json - one file per language, all with an identical key set and ICU syntax (English plurals included). Keys are type-checked at build time through the AppConfig declaration in src/global.d.ts - a typo is a TypeScript error. The locale is switched with useSetLocale(), which writes the cookie then calls router.refresh() (messages are resolved on the server, so a round trip is required).

Adding a language: extend LOCALES in src/lib/lang.ts, update parseLang, then create messages/<locale>.json.

Database

The schema travels with the source as versioned migrations in prisma/migrations/. scripts/db-setup.ts runs through the predev / prestart hooks, so a fresh clone only needs yarn dev, and an existing install picks up whatever migrations it is missing.

Situation What the script does
No dev.db yet Creates the file, then prisma migrate deploy for every migration
Database created with db push (legacy) Marks 0_init as applied (baseline), then deploys the rest
Already on migrations Applies only the missing ones

After changing prisma/schema.prisma, create a migration with yarn db:migrate --name <name> and commit the generated folder. Do not use prisma db push any more: it mutates the database in place and leaves nothing for other machines to apply.

Scripts

Command Description
yarn dev Custom server in dev mode (tsx watch)
yarn build / yarn start Production build and run
yarn format / format:check Prettier write / check
yarn lint / yarn typecheck ESLint and tsc --noEmit
yarn db:migrate --name <name> Create a migration from the schema changes
yarn db:setup Apply migrations manually (dev/start do it too)
yarn db:studio / db:generate Prisma Studio · regenerate Prisma Client

Run yarn format && yarn lint && yarn typecheck before finishing any change.

Environment variables

Variable Required Description
DATABASE_URL yes file:./dev.db (relative to prisma/schema.prisma)
MYOPS_MASTER_KEY yes Credential encryption key, at least 32 characters
MYOPS_HOME no Where .env and the SQLite file live - see below
PORT, HOSTNAME no Default 3000 / localhost
MYOPS_INSECURE_COOKIE no Set to 1 when serving production over plain HTTP on LAN
MYOPS_MCP_TOKEN no MCP endpoint credential; generated on first run

MYOPS_HOME decides where the two irreplaceable files live. Unset, myOps uses the working directory when it finds a .env or a prisma/schema.prisma there - a checkout and the Docker image both keep their state exactly where they always did. Anywhere else it falls back to ~/.myops, so an install whose code sits in a disposable directory keeps its key across updates.

Data retention

Raw metrics are kept for 24 hours; beyond that one sample per 5 minutes survives; past 30 days they are deleted. Housekeeping runs hourly together with expired-session cleanup.

Security notes

  • Losing MYOPS_MASTER_KEY means losing every SSH credential - there is no recovery path. Back the key up separately from prisma/dev.db.
  • Credentials are never returned by the API; only a hasCredential flag is.
  • Host keys are trusted on first use: the first connection to a server records the SHA256 fingerprint and every later one must match, so a machine that starts answering with a different key is refused instead of being handed the stored credentials. A rebuilt machine needs its key forgotten once, from the server's Settings screen.
  • Exposing myOps outside your LAN requires a reverse proxy with HTTPS: the session cookie and every file and terminal operation travel over that connection.
  • File operations and terminal sessions are written to AuditLog (terminal keystrokes are not).
  • MYOPS_MCP_TOKEN is a fleet-wide operator credential - see MCP server. It is generated on first run; clear it in .env and restart, and /mcp no longer exists.

Contributing

Start with CONTRIBUTING.md, then AGENTS.md and conventions.md - they define the layering, naming, i18n and comment rules the codebase follows. There is no test framework yet; verification is yarn format && yarn lint && yarn typecheck.

Found a security problem? Do not open a public issue - see SECURITY.md.

License

Licensed under the Apache License 2.0. You may use, modify and self-host myOps, including commercially, as long as you keep the license and copyright notices and state your changes.


Copyright © 2026 myOps. Licensed under the Apache License, Version 2.0.

About

Open source server control panel: manage a fleet of Linux and macOS machines over SSH from one dashboard

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages