An extremely simple-to-use, lightweight, self-hosted service manager built for personal servers.
Point it at a folder of bash scripts and get a browser-based UI with live terminals, automatic restarts, log rotation, and CPU/memory monitoring, all behind a password-protected login.
Unlike simple log viewers, shellhaven gives you full shell input through the browser. This makes it ideal for services that expect interactive commands, like a Minecraft server where you need to run /op or /stop without setting up RCON or a heavy control panel. It works equally well for monitoring and managing any kind of long-running service: web servers, bots, workers, scrapers, and more.
btop is installedsudo apt install git-lfs && git lfs install)btop (optional) - install via your package manager for the integrated system monitorgit clone https://github.com/ruelalarcon/shellhaven.git
cd shellhaven
npm install
npm run build
npm start
Open http://localhost:7456 in your browser.
On first visit you will be taken to the setup page where you create a password (minimum 8 characters). This is stored as a bcrypt hash in ~/.config/shellhaven/config.json. After setup, log in and you will see the dashboard.
For running shellhaven as a persistent background service that starts on boot, see systemd Service.
shellhaven scans ~/shells/ for *.sh files at startup and spawns each one as a managed PTY process. To add a new service, drop a bash script into that directory.
~/shells/
├── api.sh
├── worker.sh
└── scheduler.sh
The shell's name in the UI is derived from the filename (without .sh).
Adding shells while shellhaven is running: Drop the new
.shfile into~/shells/and click rescan in the sidebar. The shell will appear in the UI and start immediately, no restart required.
# ~/shells/api.sh
cd /srv/myapp
exec node server.js
Scripts do not need a shebang or the executable bit set - shellhaven spawns them as
bash -l -i <script>(login + interactive), so your full user environment is available including tools installed via.bashrcsuch as nvm, GraalVM, and similar.
Tip: Use
execas the last command so the process replaces the bash wrapper. This makes CPU/memory stats accurate and ensures exit codes are passed through correctly.
Behaviour is configured via specially formatted comments at the top of the script. All directives are optional. Directive values are read at initial startup and whenever you trigger a rescan (see below); they are not re-read on individual restarts.
# restart: always
# group: backend
# log-folder-limit: 50mb
exec node /srv/myapp/server.js
# restart: <policy>
Controls what shellhaven does when the process exits.
| Policy | Behavior |
|---|---|
unless-stopped |
Restart on crash or clean exit, unless you manually stopped it from the UI. Default. |
always |
Always restart, even after a manual stop. |
never |
Never restart automatically. |
A crash is defined as a non-zero exit code when the shell was not manually stopped. Crashed shells are shown with a distinct status badge. All automatic restarts have a fixed 3-second delay before re-spawning.
Default: unless-stopped
# group: <name>
Assign the shell to a named group in the sidebar. Shells in the same group are listed together under a collapsible section header. Shells without a group directive are listed ungrouped at the top.
# group: backend
# group: workers
# group: cron jobs
Group names are free-form text. Spaces are allowed.
Default: no group (ungrouped)
# log-folder-limit: <size>
Sets the maximum cumulative size of the shell's log folder (~/shells/logs/<name>/). Accepted units: kb, mb, gb (case-insensitive). After each log rotation, the oldest compressed log files are deleted until the folder is under the limit.
# log-folder-limit: 10mb
# log-folder-limit: 500kb
# log-folder-limit: 2gb
Default: 25mb
The rescan button (in the sidebar next to start all and stop all) re-examines ~/shells/ live without restarting shellhaven. It does three things:
.sh file that wasn't present at startup is registered and spawned immediately.sh file has been deleted is stopped (if running) and removed from the UI# restart:, # group:, and # log-folder-limit: are re-parsed for every existing shell and take effect from the next spawn onwardChanged directives on a currently running shell take effect the next time it starts or restarts. A running shell is never interrupted by a rescan.
Every shell gets its own log directory:
~/shells/logs/
└── api/
├── latest.log
├── 2026-05-20_10-00-00.log.gz
└── 2026-05-21_08-30-45.log.gz
latest.log - the active log for the current run, written in real time*.log.gz - gzip-compressed logs from previous runs, named by the time they were rotatedANSI escape sequences (colors, cursor movement, etc.) are stripped before writing to disk so log files are plain text regardless of what the process outputs. The raw output (with ANSI) is still sent to the in-browser terminal and scrollback buffer.
Logs can be browsed and read directly from the UI via the Logs button in the sidebar controls for each shell.
Rotation happens automatically on every spawn (including restarts):
latest.log exists and is non-empty, it is gzip-compressed to a timestamped YYYY-MM-DD_HH-MM-SS.log.gz filelatest.log is deletedlatest.log is opened for the new runRotation is awaited before the process spawns, so there is no race between the old and new log files.
If latest.log is empty (e.g. the process exited immediately), it is deleted without creating a compressed archive.
After each rotation, shellhaven checks the total size of the log folder. If it exceeds the configured limit, it deletes the oldest compressed files one by one until the folder is within the limit.
Edge case: if a single run produces a log file that alone exceeds the limit, that compressed file is pruned immediately after creation. Only latest.log (the active run) is never pruned.
shellhaven's own log output goes to stdout/stderr with timestamps, log levels, and scoped context labels:
2026-05-22 14:30:01.123 INFO [shells] discovered 3 shell script(s): api, worker, scheduler
2026-05-22 14:30:01.201 INFO [shells] [api] started (pid=12345)
2026-05-22 14:30:05.442 WARN [shells] [worker] crashed (exit code 1)
2026-05-22 14:30:05.443 INFO [shells] [worker] scheduling restart in 3000ms
2026-05-22 14:31:02.010 INFO [ws] client connected from ::1 (1 total)
2026-05-22 14:32:15.887 WARN [auth] failed login attempt from 203.0.113.42
ANSI colors are used when stdout is a TTY and stripped automatically when output is piped or redirected (e.g. captured by systemd journal).
Log level is controlled by the SHELLHAVEN_LOG_LEVEL environment variable:
| Level | What you see |
|---|---|
error |
Errors only |
warn |
Errors and warnings |
info |
Normal operational events. Default. |
debug |
Everything, including log rotation and pruning details |
SHELLHAVEN_LOG_LEVEL=debug npm start
A summary of every default for quick reference:
| Setting | Default |
|---|---|
| Restart policy | unless-stopped |
| Restart delay | 3 seconds |
| Log folder limit | 25 MB per shell |
| Server port | 7456 |
| Server host | localhost |
| Session duration | 7 days |
| Scrollback buffer | 100 KB per terminal (in-memory only) |
| Log level | info |
| PTY size | 220 cols x 50 rows |
| btop | Enabled if btop is in PATH |
If btop is installed and available in PATH, shellhaven automatically spawns it as a shared persistent PTY on startup. It appears as a special entry in the sidebar and is accessible to all connected clients simultaneously.
If btop exits for any reason (e.g. you press q), it restarts automatically after 1 second.
To install btop:
# Debian/Ubuntu
sudo apt install btop
# Arch
sudo pacman -S btop
# macOS
brew install btop
The npm run build step in Installation compiles both the client (Vite) and server (tsc) and is required before running npm start. To rebuild after making changes:
npm run build
npm start
The server port and host are configurable via environment variables:
SHELLHAVEN_PORT=7456 SHELLHAVEN_HOST=0.0.0.0 npm start
Set SHELLHAVEN_HOST=0.0.0.0 only if you are not using a reverse proxy and need the port directly accessible. If you are using nginx (recommended), keep SHELLHAVEN_HOST=localhost and let nginx handle public traffic.
Create a service file so shellhaven starts on boot and is managed like any other system service.
which node
sudo cp shellhaven.service /etc/systemd/system/shellhaven.service
sudo nano /etc/systemd/system/shellhaven.service
you with your Linux username and /usr/bin/node with the output of which node throughout:[Unit]
Description=shellhaven
After=network.target
[Service]
ExecStart=/usr/bin/node /home/you/shellhaven/server/dist/server/src/index.js
WorkingDirectory=/home/you/shellhaven
Restart=always
User=you
Environment=NODE_ENV=production
Environment=HOME=/home/you
Environment=SHELLHAVEN_PORT=7456
Environment=SHELLHAVEN_HOST=localhost
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable shellhaven
sudo systemctl start shellhaven
sudo systemctl status shellhaven
journalctl -u shellhaven -f
Using nginx is strongly recommended. It handles TLS termination, keeps shellhaven bound to localhost, and correctly proxies the WebSocket connection.
sudo apt install nginx
your.domain.com):sudo nano /etc/nginx/sites-available/shellhaven
server {
listen 80;
server_name your.domain.com;
# Required so browsers accept the font files
location ~* \.ttf$ {
proxy_pass http://localhost:7456;
proxy_http_version 1.1;
proxy_set_header Host $host;
add_header Content-Type font/ttf;
}
location / {
proxy_pass http://localhost:7456;
proxy_http_version 1.1;
# Required for WebSocket upgrade
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
sudo ln -s /etc/nginx/sites-available/shellhaven /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d your.domain.com
server {
listen 443 ssl;
server_name your.domain.com;
ssl_certificate /etc/letsencrypt/live/your.domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your.domain.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://localhost:7456;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
server {
listen 80;
server_name your.domain.com;
return 301 https://$host$request_uri;
}
sudo certbot renew --dry-run
shellhaven itself only listens on localhost:7456 by default. If you are using nginx as a reverse proxy (recommended), you only need to open the standard web ports to the internet.
Recommended setup (with nginx + TLS):
sudo ufw allow OpenSSH # keep SSH access
sudo ufw allow 80/tcp # HTTP (redirects to HTTPS)
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
Port 7456 does not need to be opened; nginx proxies to it internally.
Direct access (no nginx, not recommended for production):
If you want to expose shellhaven directly without a reverse proxy, bind it to all interfaces and open the port:
# In your systemd service or shell:
SHELLHAVEN_HOST=0.0.0.0 SHELLHAVEN_PORT=7456 npm start
# Open the port in your firewall:
sudo ufw allow 7456/tcp
Note that without TLS your session cookie and terminal I/O will be transmitted in plaintext. Only do this on a trusted private network.
Cloud providers: If you are on AWS, GCP, DigitalOcean, Hetzner, etc., you may also need to open ports in the provider's firewall or security group settings in addition to ufw. The ports to open are the same: 80 and 443 for a standard nginx + TLS setup.
# In two separate terminals:
npm run dev:server # Express server on localhost:7456
npm run dev:client # Vite dev server (proxies API to :7456)
To type-check the server:
cd server && npx tsc --noEmit
To type-check the client:
cd client && npm run check
The SHELLHAVEN_LOG_LEVEL environment variable controls server log verbosity. Set it to debug to see detailed output including log rotation and pruning:
SHELLHAVEN_LOG_LEVEL=debug npm run dev:server