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.
- 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/elifchain 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.
| 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 |
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.
The fastest way to try it, no checkout involved:
npx @hyydev/myops # http://localhost:3000Installing 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:3000That 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.
- 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 offWhen 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.
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
shadowtoolchain (useradd,usermod,chpasswd); Alpine needs theshadowpackage and macOS usesdscl, so the screen is not available there yet. - Nginx: only Debian ships
sites-enabledby default. When creating a site myOps drops anincludeline intoconf.difnginx.conflacks 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.
Install it from the UI (Overview tab → Install / update agent). myOps will:
- Copy
agent/myops-agent.shto/usr/local/bin/myops-agentover SFTP. - Create
myops-agent.service+.timerandsystemctl enable --nowit (15s period). - Fall back to cron running four times a minute when the target has no systemd.
- Record the login user in
/var/lib/myops-agent/owner(root-owned, 0644). The agent runs as root from a timer or cron, whereSUDO_USERis 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.
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.
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.
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.
| 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.
| 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.
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.
- Losing
MYOPS_MASTER_KEYmeans losing every SSH credential - there is no recovery path. Back the key up separately fromprisma/dev.db. - Credentials are never returned by the API; only a
hasCredentialflag 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_TOKENis a fleet-wide operator credential - see MCP server. It is generated on first run; clear it in.envand restart, and/mcpno longer exists.
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.
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.




