Run remote SSH commands with a plain HTTP GET — straight from your homelab, no bloat.
comnix is a tiny container that turns a plain HTTP GET into a remote SSH command. Drop it into your homelab, mount a volume with your SSH connection profiles and the commands you want to expose, and every route becomes a one-shot remote action — no client library, no auth handshake, no runtime to babysit.
The whole point is to stay out of the way:
| Multiple SSH profiles | Define as many named servers as you want in ssh.json (different hosts, users, ports, keys). Each route in commands.json picks which one it runs against. |
| Route → command mapping | Every entry in commands.json maps a URL segment to a shell command. No code, no scripting language — just JSON. |
| ~9 MB image | Published with .NET 10 Native AOT, self-contained, on top of runtime-deps:alpine. No SDK, no JIT, no dead weight. |
| Zero external dependencies | No database, queue, or sidecar service needed. Just the container and two JSON files. |
| Configuration via volume | Profiles and commands live in JSON files mounted as a volume — edit them and the change applies immediately, no image rebuild. |
| Customizable response | Optionally returns HTML (with CSS/JS) instead of JSON — great for bookmarklets, home-screen shortcuts, and automations. |
Ideal for triggering everyday homelab tasks — restarting a service, running a backup script, sending a Wake-on-LAN, checking disk usage — from a phone shortcut, a browser bookmarklet, a Home Assistant button, or anything that can make a GET request.
- How it works
- Running with Docker
- Running with Docker Compose
- Configuration files
- Calling the API
- Security
- Building the image locally
- License
comnix exposes a single HTTP route:
GET /api/{route}
Each {route} is mapped, via commands.json, to a shell command executed over SSH against a server described in ssh.json. The response carries the execution result — JSON by default, or HTML if you supply a response.html template (see below).
GET /api/restart-nginx ─────▶ comnix ─────▶ ssh user@server "systemctl restart nginx"
│
◀──────────────────────────┘
{ "success": true, "result": "" }
docker run -d \
--name comnix \
-p 5000:5000 \
-v ./config:/app/config \
adolfok3/comnix:latest- Port: the API listens on port
5000inside the container (ASPNETCORE_HTTP_PORTS=5000). Map it to whichever host port you prefer. - Volume: mount a host directory at
/app/config— that's where comnix looks forssh.json,commands.json, and optionallyresponse.html. - If
ssh.json/commands.jsondon't exist in the volume on first run, comnix creates empty defaults automatically ({"ssh": {}}and{"commands": []}), ready for you to edit.
services:
comnix:
image: adolfok3/comnix:latest
container_name: comnix
restart: unless-stopped
ports:
- "5000:5000"
volumes:
- ./config:/app/configAll the files below live in the directory mounted at /app/config.
Note
These files are watched in real time — changes may take a few seconds to take effect, no container restart needed.
Defines one or more named SSH connections, referenced by the commands in commands.json.
{
"ssh": {
"default": {
"host": "192.168.1.10",
"user": "homelab",
"port": 22,
"password": "my-password"
},
"nas": {
"host": "192.168.1.20",
"user": "admin",
"port": 2222,
"keyPath": "/app/config/keys/nas_id_rsa",
"commandTimeout": 60
}
}
}| Field | Required | Default | Description |
|---|---|---|---|
host |
Yes | — | Address of the SSH server. |
user |
Yes | — | Username for authentication. |
port |
No | 22 |
SSH service port. |
password |
No* | — | Password for the user. |
keyPath |
No* | — | Path (inside the container) to a private key file. Takes priority over password if set. |
commandTimeout |
No | 120 |
Timeout in seconds for command execution. |
* provide either password or keyPath — if using a key, mount the key file via volume as well (e.g. ./keys:/app/config/keys).
Each connection's key ("default", "nas", ...) is the name that entries in commands.json use to pick where to run.
Defines the available routes and the command executed for each one.
{
"commands": [
{ "route": "restart-nginx", "command": "sudo systemctl restart nginx" },
{ "route": "disk-usage", "command": "df -h" },
{
"route": "wake-nas",
"command": "wakeonlan AA:BB:CC:DD:EE:FF",
"connection": "nas"
}
]
}| Field | Required | Default | Description |
|---|---|---|---|
route |
Yes | — | Segment after /api/. Requests to GET /api/restart-nginx execute this entry's command. Matching is case-insensitive. |
command |
Yes | — | Shell command executed on the remote server. |
connection |
No | default |
Name of the connection (key from ssh.json) used for this route. |
By default, every call returns a plain JSON payload:
{ "success": true, "result": "command output" }If you drop a response.html file at /app/config/response.html, comnix returns that HTML instead of JSON — with two placeholders substituted automatically:
| Placeholder | Replaced with |
|---|---|
{{success}} |
true or false |
{{result}} |
Command output (HTML-encoded) |
This makes room for a response with your own look and feel — CSS, animations, even JavaScript behavior. A classic homelab use case: use comnix as the target of a phone home-screen shortcut and auto-close the tab right after execution, so no result screen is left hanging around:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<style>
body {
font-family: system-ui, sans-serif;
background: #111;
color: #eee;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
}
.icon {
font-size: 3rem;
}
</style>
</head>
<body>
<span class="icon">{{success}}</span>
<script>
// fire the action and disappear — no result screen taking up space
setTimeout(() => window.close(), 300);
</script>
</body>
</html>Any valid HTML/CSS/JS works — comnix just does a text substitution before returning the page.
Anything capable of making a GET request can act as a comnix "client": curl, a browser bookmarklet, an iOS/Android home-screen shortcut, a Home Assistant rest_command, a physical button wired to an ESP8266, etc.
curl http://homelab.local:5000/api/restart-nginx{ "success": true, "result": "" }comnix does not implement its own authentication or API key validation — this is a deliberate choice, not an oversight (see ADR 0001 for the full reasoning). Anyone who can reach the port can trigger any route in commands.json, so the network boundary is the security model: run it only behind a trusted network (your LAN, a VPN like Tailscale/WireGuard, or a reverse proxy with authentication in front), and never expose the port directly to the internet.
docker build -f src/Comnix/Dockerfile -t comnix .Distributed under the MIT license.