A Phoenix LiveView server fleet management dashboard with a mini-OS desktop interface. Manage your VPS fleet through an elegant browser-based desktop with SSH terminal, file explorer, Docker container management, systemd service control, nginx configuration, and live metrics -- all wrapped in a polished, themeable UI and protected by a local admin login.
- Overview
- Features
- Architecture
- Getting Started
- Configuration
- Development
- Production Deployment
- Security Notes
- License
Marsad (Arabic for "observatory" or "watchtower") is a self-hosted web application that provides a unified desktop-style interface for managing a fleet of Linux servers over SSH. Rather than juggling multiple terminal sessions, SFTP clients, and monitoring tools, Marsad consolidates these workflows into a single, real-time, browser-based desktop environment.
The application is built with Phoenix LiveView and Elixir, leveraging OTP for concurrent, fault-tolerant SSH session management. Each managed server gets its own supervised GenServer that maintains a persistent SSH connection, enabling instant command execution and file operations without per-request connection overhead.
- Draggable, resizable windows with a taskbar and app launcher
- Real-time updates via LiveView's WebSocket connection
- Themeable UI with light/dark modes and six accent colors
- Code editors with syntax highlighting for remote files
- Offline mode: a dedicated
/offlinepage plus an automatic overlay when the connection drops
- First-run setup at
/setupcreates the local admin account (username + password, PBKDF2-hashed) - Login at
/login; every app route requires authentication - Change the password or fully reset auth from the Settings app
- Add, edit, and remove SSH server connections
- Support for password and private key authentication
- Automatic host key fingerprint recording (TOFU)
- Server reachability testing with online/offline status tracking
- xterm.js-powered SSH terminal emulator
- One persistent PTY shell per terminal window — a real interactive shell, so
vim,nano,top,cd, pipes and job control all work natively - Live resize propagation, scrollback transcript replay per window, copy/paste friendly
- Remote directory browsing with breadcrumb navigation
- Smart recursive search with a query language:
word(AND),"exact phrase",-exclude,ext:conf,json(or*.conf),type:dirs,size:>10M,depth:3,limit:50,all(include.git/node_modules) - Ranked results with truncation notice and removable filter chips
- File upload (up to 50 MB per file, 3 concurrent)
- File preview with syntax highlighting
- Inline code editing with save-to-remote
- Create directories, delete files and folders
- CPU load (1m, 5m, 15m), memory usage, disk utilization
- Network I/O (RX/TX) tracking
- Per-core CPU detection (handles cgroup-limited environments)
- Process table with sort, filter, and kill capabilities
- Historical charting with configurable time ranges (1h, 6h, 24h, 7d, custom)
- Auto-refresh with configurable interval (minimum 5 seconds)
- Tabbed panel: Containers, Images, Stacks (Compose), Activity (audit trail)
- List all containers with plain-language status badges (Running/Stopped/Restarting/…) and health pills
- Search, filter by state, and sort containers
- Start, stop, restart, and remove containers (async, non-blocking)
- View container logs with tail-size, timestamps, text filter, and full-log download (up to 5 MB)
- Live resource stats with auto-refresh (CPU, memory, network, block I/O)
- Full container inspection with overview/mounts/network cards plus raw JSON
- Image list with remove and prune-unused actions
- Compose projects with per-service restart
- List all service units with load, active, sub, and description states
- Filter by text search or state (all, active, failed, inactive)
- Sort by name or state
- Start, stop, and restart services
- View unit journal logs (last 200 lines)
- Preview and edit unit files with automatic daemon-reload on save
- Service status with config test result
- SSL certificate expiry monitoring: parses every HTTPS vhost, checks live certs via
openssl, and warns (≤ 30 days) or alarms (≤ 14 days / expired) with per-domain pills - Reload and restart nginx
- Full configuration dump viewer
- Config file browser under
/etc/nginxwith path confinement - Edit config files with save capability
- Error log viewer (last 100 lines)
Fleet -- Server CRUD, SSH session lifecycle, and remote execution. Uses a Registry and DynamicSupervisor to manage one ServerSession process per active server. Sessions are started lazily and terminated when a server is updated or deleted.
ServerShell -- One persistent PTY shell per terminal window (separate SSH connection per window, keyed {:shell, server_id, window_id, liveview_pid}). Streams raw keystrokes in and screen bytes out; dies with its LiveView.
Services -- Pure functional wrappers around Docker, systemd, and nginx commands. All remote names interpolated into shell commands are strictly validated against an allow-list regex to prevent command injection.
SSH -- A behaviour defining the remote transport interface (exec plus interactive-shell primitives). The default implementation (SshAdapter) uses OTP :ssh and :ssh_sftp. A future agent-based transport can implement this same behaviour without changing callers.
Accounts -- Local admin authentication (PBKDF2-HMAC-SHA256 via OTP :crypto, no extra deps), first-time setup guard, and session enforcement.
Metrics -- Database-backed storage of server health snapshots with pruning of data older than 48 hours. Chart data is computed from snapshot history.
Settings -- Key/value store for application settings (theme mode, accent color, metrics polling interval). Unknown or missing values fall back to curated defaults.
Files / Terminal -- Pure domain helpers: the smart file-search query language and terminal copy/demo text live here so they are unit-testable without SSH.
Web Browser <--WebSocket--> LiveView Process
|
+----------------+----------------+
| (via Registry lookup) |
v v
ServerSession (GenServer) ServerShell (GenServer)
one exec channel | (own SSH connection
per command, no TTY v + PTY per window)
Remote Linux Server
Each server gets a supervised GenServer that holds a long-lived SSH connection. On first use (command execution, file listing, etc.), the session establishes the connection, decrypts the stored credential, authenticates, and records the host key fingerprint. Subsequent operations reuse the connection. If the connection drops, the session transparently reconnects on the next request. Interactive terminals additionally open one ServerShell per window with its own connection and PTY.
SSH secrets (passwords and private keys) are encrypted at rest using AES-256-GCM via OTP's :crypto module. The encryption key is read from the :marsad, :vault_key application environment variable (base64, 32 bytes). In dev and test environments, a non-secret fallback key is used so the application boots with zero setup.
In production, you must set the MARSAD_VAULT_KEY environment variable — the app refuses to boot without it. Losing this key means losing access to stored credentials.
- Elixir ~> 1.17
- Erlang/OTP (compatible with your Elixir version)
- Node.js (for asset compilation)
- SQLite (via ecto_sqlite3 dependency)
- Clone the repository:
git clone <repository-url>
cd marsad- Install dependencies and set up the database:
mix setupThis runs deps.get, ecto.setup (create, migrate, seed), assets.setup (install Tailwind and esbuild), and assets.build.
- Start the Phoenix server:
mix phx.server- Open your browser at
http://localhost:4000.
On first run you land on /setup: create the local admin account, then sign in. Afterwards the root URL opens the desktop environment directly (guests are redirected to /login).
| Variable | Required | Default | Description |
|---|---|---|---|
MARSAD_VAULT_KEY |
Yes (enforced at boot) | Dev fallback | Base64-encoded 32-byte key for AES-256-GCM credential encryption |
SECRET_KEY_BASE |
Production only | Dev fallback | Phoenix secret key for signing/encrypting cookies |
DATABASE_PATH |
Production only | marsad_dev.db (dev) |
Path to the SQLite database file |
PORT |
No | 4000 |
HTTP port to listen on |
PHX_HOST |
Production recommended | example.com |
Public hostname (used for URL generation) |
POOL_SIZE |
No | 5 |
Ecto connection pool size |
PHX_SERVER |
Production recommended | false |
Set to true to enable the HTTP server in releases |
Runtime-configurable settings are stored in the database and accessible via the Settings app on the desktop:
- Theme mode:
lightordark(default:dark) - Accent color:
ocean,royal,emerald,violet,amber,rose(default:ocean) - Metrics interval: Polling interval in milliseconds, minimum 5000 (default: 15000)
# Run tests
mix test
# Run only previously failed tests
mix test --failed
# Run tests in a specific file
mix test test/marsad/fleet_test.exs
# Format code
mix format
# Full pre-commit checks (compile, deps unlock unused, format, test)
mix precommitIn development, the following are available at /dev:
- LiveDashboard (
/dev/dashboard) -- Phoenix request and telemetry metrics - Mailbox Preview (
/dev/mailbox) -- Preview emails sent by the application
Both are mounted only when dev_routes is enabled (default in dev/test).
lib/
marsad/ -- Application contexts (business logic)
application.ex -- OTP application and supervisor tree
repo.ex -- Ecto repository
accounts.ex -- Local admin auth (PBKDF2, setup guard)
accounts/ -- Admin schema + password hashing
audit_log.ex -- Audit trail schema
files.ex -- File browser domain + smart search query language
terminal.ex -- Terminal copy + demo-mode text
fleet/ -- Server fleet management
server.ex -- Server schema and changesets
server_session.ex -- GenServer holding SSH connections
server_shell.ex -- GenServer holding one PTY shell per terminal window
credential_vault.ex -- AES-256-GCM at-rest encryption
services.ex -- Docker, systemd, nginx operations
sys_info.ex -- Health metrics collection and parsing (single-shot fetch)
ssh/ -- SSH transport layer
ssh_adapter.ex -- OTP :ssh / :ssh_sftp implementation
metrics/ -- Historical metrics
snapshot.ex -- Metrics snapshot schema
settings/ -- Application settings
helpers/text.ex -- Shared text/size/mime formatting
marsad_web/ -- Web interface
live/desktop_live.ex -- Main LiveView (mini-OS desktop)
live/offline_live.ex -- Public offline/limited-connection page
live_auth.ex -- on_mount hooks (:ensure / :guest)
plugs.ex -- RequireAdmin / RedirectIfAdmin plugs
desktop/ -- Panel components
docker_panel.ex -- Docker panel (containers/images/stacks/activity tabs)
systemd_panel.ex -- Systemd service panel
nginx_panel.ex -- Nginx management panel
controllers/
auth_controller.ex -- Setup/login pages + session writes
file_download_controller.ex -- Capped remote file downloads
docker_logs_controller.ex -- Capped container log downloads
session_controller.ex -- Logout
router.ex -- Routes (:app / :guest / :public live_sessions)
endpoint.ex -- Phoenix endpoint
telemetry.ex -- Telemetry metrics
priv/
repo/
migrations/ -- Ecto database migrations
static/ -- Compiled assets
test/
support/
file_session_stub.ex -- FIFO stub for exec/sftp flows
fake_shell_transport.ex -- In-memory PTY transport for shell flows
Build and run a production release:
# Set required environment variables
export DATABASE_PATH=/etc/marsad/marsad.db
export SECRET_KEY_BASE=$(mix phx.gen.secret)
export MARSAD_VAULT_KEY=$(openssl rand -base64 32)
export PHX_HOST=your-domain.com
export PHX_SERVER=true
# Build the release
MIX_ENV=prod mix release
# Run it
bin/marsad startThe application manages Docker containers on remote servers; the application itself can also be containerized. Ensure that DATABASE_PATH points to a persistent volume and that MARSAD_VAULT_KEY is preserved across restarts.
- SSL: In production, the endpoint is configured to force SSL. Behind a reverse proxy, set
rewrite_on: [:x_forwarded_proto]. - Database: SQLite is used for simplicity. For higher concurrency or multi-node deployments, consider switching to PostgreSQL by updating the repository configuration.
- SSH Key Forwarding: The application uses direct SSH connections from the server where Marsad runs. Ensure that host can reach your managed servers on the configured SSH ports.
- Credentials are encrypted at rest but decrypted in memory for SSH authentication.
- Host keys are accepted on first use (TOFU) and recorded for display. Future versions may enforce host key verification.
- All names interpolated into remote shell commands are validated against a strict allow-list regex (
~r/\A[\w@:.+=,~-]+\z/). - Nginx file access is confined under
/etc/nginxwith path traversal protection. - PID values are validated (1 to 4,194,303) before being passed to
kill. - Private key material written to temporary files is created with exclusive access mode and removed after use.
This project is licensed under the Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0).
You are free to:
- Share -- copy and redistribute the material in any medium or format
- Adapt -- remix, transform, and build upon the material
Under the following terms:
- Attribution -- You must give appropriate credit, provide a link to the license, and indicate if changes were made.
- NonCommercial -- You may not use the material for commercial purposes.
See the LICENSE file for the full license text.
