Sign inSign up

cnsoluciones/sentineldesk

By cnsoluciones

β€’Updated 21 days ago

A full Linux desktop in a container, streamed over WebRTC, shared by people and AI agents.

Image
Machine learning & AI
Developer tools
Operating systems
0

2.2K

cnsoluciones/sentineldesk repository overview

⁠SentinelDesk

A collaborative Linux desktop for people and AI agents.

A complete Linux desktop that runs inside a container with no physical monitor, streams to the browser over WebRTC, and is driven at the same time by people and by an AI agent that sees and acts on the same X display.

One docker compose up gives a full desktop that people and an agent share β€” no control plane, no database, nothing else to stand up.


⁠Two images, one deployment

ImageContainerWhat it is
cnsoluciones/sentineldesksentineldeskThe desktop: X, the browser, the apps, the WebRTC stream, the MCP server
cnsoluciones/sentineldesk-agentsentineldesk-agentThe runtime that drives it β€” the brain behind the chat panel

The agent is optional, and it is a separate image on purpose. It has no display, no browser and no home of the desktop's: everything it does, it does through MCP over a Unix socket, so the container boundary and the protocol say the same thing. Naming the desktop is how you leave it out:

docker compose up -d                # the desktop and an agent beside it
docker compose up -d sentineldesk   # the desktop, on its own

The desktop has no depends_on and no link to the agent, so the second line is a complete deployment rather than a crippled one. Order does not matter either: started first, the agent waits for the desktop and reconnects when it comes back.

How the two are joined β€” one volume, and it is the whole security story:

sentineldesk-run   ──  a directory of Unix sockets, mounted in both

That is the only link. The agent has no published ports and no network of its own, so nothing about the conversation, or about what the agent does, leaves the host.


⁠Quick start

No clone and no build β€” download the compose file and bring it up:

curl -fsSLO https://raw.githubusercontent.com/sentineldesk/desktop/main/docker-compose.yml
AUTH_PASS=change-me HOST_IP=<your-ip> docker compose up -d
# β†’ open https://<your-ip>:8080 and log in

HOST_IP is the address browsers reach this host at β€” its LAN address, or the public IP on a VPS. Off localhost it is required rather than optional: WebRTC would otherwise advertise the container's bridge address, which nothing outside can reach, and the video connects and stays black.

Or let the installer do it β€” it installs Docker (and socat) if missing, writes the compose file to /opt/sentineldesk and starts both services:

curl -fsSL https://raw.githubusercontent.com/sentineldesk/desktop/main/install.sh | sudo bash
Installer flag
--fullthe full image (LibreOffice, Firefox, GIMP…); default is lite
--pass <p> / --user <u>the login credentials (default user admin, password generated)
--ip <addr>the address browsers reach this host at β€” set it on a public VPS
--port <n>the web port (default 8080)
--vpnallow the built-in OpenVPN client (adds NET_ADMIN + /dev/net/tun)
--no-agentthe desktop only; no agent container
--dir <path>where to write docker-compose.yml (default /opt/sentineldesk)
--no-pulluse the image already on the host
⁠The compose file, in short

The published file has more comments than configuration; this is its shape:

name: sentineldesk

services:
  sentineldesk:
    image: cnsoluciones/sentineldesk:${SENTINELDESK_TAG:-latest}   # :full for the heavier apps
    container_name: sentineldesk
    restart: unless-stopped
    ports:
      - "${HTTP_PORT:-8080}:8080"
      - "3478:3478/udp"                  # the embedded STUN responder
      - "59000-59049:59000-59049/udp"    # WebRTC media
    environment:
      - AUTH_USER=${AUTH_USER:-admin}
      - AUTH_PASS=${AUTH_PASS:-change-me}
      - WEBRTC_MIN_PORT=59000
      - WEBRTC_MAX_PORT=59049
      - NAT1TO1_IP=${HOST_IP:-127.0.0.1}
      - TLS_SELFSIGNED=1
      - TLS_HOSTS=${HOST_IP:-127.0.0.1}
      - MCP_SOCK=/run/sentineldesk/mcp.sock
      - TZ=${TZ:-UTC}
      - KEYBOARD_LAYOUT=${KEYBOARD_LAYOUT:-us}
      - KEYBOARD_VARIANT=${KEYBOARD_VARIANT:-}
    volumes:
      - sentineldesk-home:/home/sentineldesk
      - sentineldesk-run:/run/sentineldesk
      - sentineldesk-audit:/var/log/sentineldesk
      - sentineldesk-work:/tmp/sentineldesk
    shm_size: "2gb"
    # The built-in OpenVPN client needs these. Uncomment only if you will use it.
    # cap_add: [NET_ADMIN]
    # devices: ["/dev/net/tun"]

  agent:
    image: cnsoluciones/sentineldesk-agent:${SENTINELDESK_AGENT_TAG:-latest}
    container_name: sentineldesk-agent
    restart: unless-stopped
    environment:
      - MCP_SOCK=/run/sentineldesk/mcp.sock
    volumes:
      - sentineldesk-run:/run/sentineldesk    # the sockets β€” the only link
      - sentineldesk-agent:/home/agent        # model choice, keys, history

volumes:
  sentineldesk-home:  { name: sentineldesk-home }
  sentineldesk-run:   { name: sentineldesk-run }
  sentineldesk-agent: { name: sentineldesk-agent }
  sentineldesk-audit: { name: sentineldesk-audit }
  sentineldesk-work:  { name: sentineldesk-work }

The volume names are pinned rather than prefixed with the project name: an agent on the host looks for a volume literally called sentineldesk-run to find the socket, and anybody who started the desktop with docker run once comes back to the same home rather than an empty one. On a machine where those volumes already exist, Compose prints a cosmetic volume … was not created by Docker Compose warning β€” that is it adopting them, which is what it should do here.

⁠Configuring it with .env

Compose reads a .env beside the file, which is the tidier place for anything you would otherwise repeat on the command line:

cat > .env <<'EOF'
AUTH_USER=admin
AUTH_PASS=a-real-password
HOST_IP=192.168.0.100
SENTINELDESK_TAG=latest        # or `full` for the heavier apps
SENTINELDESK_AGENT_TAG=latest
TZ=America/Argentina/Buenos_Aires
KEYBOARD_LAYOUT=latam
HTTP_PORT=8080
EOF
docker compose up -d

Every one of those has a default; only AUTH_PASS and HOST_IP really want setting before anybody else can reach the deployment.

⁠Everyday commands
docker compose ps                              # what is up
docker compose logs -f sentineldesk            # follow the desktop
docker compose logs -f agent                   # follow the agent
docker compose restart agent                   # restart one service
docker compose down                            # stop both, keep the volumes
docker compose pull && docker compose up -d    # upgrade in place

docker compose down leaves the volumes alone, so the home, the audit log and the agent's keys survive it. To start genuinely fresh, remove them by name β€” and read that as the destructive command it is: docker volume rm sentineldesk-home sentineldesk-agent.

⁠Just the desktop, with docker run

Compose is the supported path, but a single container still works:

docker run -d --name sentineldesk \
  -p 8080:8080 \
  -p 3478:3478/udp \
  -p 59000-59049:59000-59049/udp \
  -e AUTH_USER=admin -e AUTH_PASS=change-me \
  -v sentineldesk-home:/home/sentineldesk \
  -v sentineldesk-run:/run/sentineldesk \
  -e MCP_SOCK=/run/sentineldesk/mcp.sock \
  --shm-size=2g \
  cnsoluciones/sentineldesk:latest

Open http://localhost:8080⁠ and log in.

⁠Tags

Both images follow the same scheme; the desktop's tag also chooses how much desktop you get.

TagWhat you get
latest, liteThe everyday desktop: Chromium, VLC, terminals, file manager, remote-desktop clients
fullEverything in lite plus LibreOffice, Firefox, GIMP, Wireshark and more
<version>, <version>-lite, <version>-fullPinned builds

Package lists: docs/packages.md⁠. cnsoluciones/sentineldesk-agent has no lite/full split β€” latest or a pinned version. The two images release on their own cycles and are not required to match: that is why they are two repositories.

Both are built for linux/amd64 and linux/arm64.

⁠Ports and volumes

PortPurpose
8080/tcpWeb interface (HTTP or HTTPS)
3478/udpEmbedded STUN responder
59000-59049/udpWebRTC media range β€” must match WEBRTC_MIN_PORT / WEBRTC_MAX_PORT

The agent publishes no ports at all.

VolumeMounted inPurpose
sentineldesk-home β†’ /home/sentineldeskdesktopBrowser profiles, files, TLS certificate β€” kept across restarts
sentineldesk-run β†’ /run/sentineldeskdesktop and agentThe Unix sockets: MCP, and the chat wire the panel uses
sentineldesk-audit β†’ /var/log/sentineldeskdesktopThe action log, kept apart so wiping a browser profile never takes the record with it
sentineldesk-work β†’ /tmp/sentineldeskdesktopScratch space for screenshots and recordings in flight
sentineldesk-agent β†’ /home/agentagentThe agent's model choice, keys and history β€” and any vendor CLI installed beside them

--shm-size=2g (shm_size: "2gb") matters: Chromium crashes on the default 64 MB.

sentineldesk-agent covers the agent's whole home rather than one dotfile, because the runtime can drive a model through a vendor's own CLI (claude, codex, opencode) and those install into ~/.local/bin with credentials in their own dotfiles. Without it, replacing the container keeps the preference naming a tool and loses the tool.

⁠Environment variables β€” the desktop

⁠Authentication
VariableDefaultWhat it does
AUTH_USER(empty)Login username. Empty means no login β€” local use only. The compose file and the installer set admin
AUTH_PASS(empty)Login password
AUTH_SECRET(generated)Signing key for session tokens. Set it to keep sessions valid across restarts
AUTH_TTL_HOURS12How long a session token lives
ROOT_PASSWORD(reuses AUTH_PASS)Password for root inside the desktop
⁠Network and WebRTC
VariableDefaultWhat it does
HTTP_PORT8080Port the web interface listens on
HTTP_ADDR(all interfaces)Bind address
PUBLIC_URL(empty)External URL the desktop is reached at, when behind a proxy
NAT1TO1_IP(empty)The host's reachable IP, so ICE advertises an address a remote browser can connect to. Set it on a VPS
WEBRTC_MIN_PORT / WEBRTC_MAX_PORT(any)UDP media range β€” must match the published ports
STUN_EMBEDDEDtrueAnswer STUN from the container itself
STUN_PORT3478Port for that responder
STUN_SERVERGoogle STUNSTUN server the server side uses
CLIENT_STUNGoogle STUNSTUN server handed to the browser
CLIENT_TURN_URLS(empty)Comma-separated TURN URLs for browsers behind strict NAT
TURN_USER / TURN_PASS(empty)TURN credentials
ALLOWED_ORIGINS(empty)Restrict which origins may open the WebSocket
⁠TLS
VariableDefaultWhat it does
TLS_SELFSIGNEDtrueServe HTTPS with a self-signed certificate (WebRTC needs a secure context off localhost)
TLS_HOSTS(empty)Hostnames/IPs the certificate is issued for
TLS_DIR/home/sentineldesk/.tlsWhere the certificate persists
TLS_CERT / TLS_KEY(empty)Use your own certificate instead

Terminating TLS yourself with nginx, Caddy or Nginx Proxy Manager in front? Set TLS_SELFSIGNED=0 so the backend speaks plain HTTP to the proxy.

⁠Display, video and audio
VariableDefaultWhat it does
DISPLAY_WIDTH1920Screen width
DISPLAY_HEIGHT1080Screen height
DISPLAY:0X display
FPS30Capture frame rate
ENCODERautoauto, nvenc, vaapi, x264, vp8
VIDEO_BITRATE_KBPS4000Target video bitrate
MIN_VIDEO_BITRATE_KBPS1200Floor when the link degrades
AUDIO_BITRATE96000Opus bitrate
AUDIO_DEVICEsentineldesk.monitorPulseAudio source captured
REMOTE_CURSORfalseDraw the cursor into the video instead of overlaying it
SOFTWARE_SCALE_HEIGHT0Downscale before encoding (0 = off)
USE_DAMAGE1Encode only changed regions
MAX_VIEWERS4How many browsers may watch at once
RECORD_THREADS2Encoder threads for recording
⁠The agent plane (set on the desktop)
VariableDefaultWhat it does
AGENT_ENABLEDtrueOffer the chat plane at all. 0 turns the panel off; the desktop is unaffected
AGENT_SOCK(beside MCP_SOCK)The socket the agent runtime connects to. Leave it alone β€” relocating MCP_SOCK relocates both, which is the point
AGENT_NAMEAI agentHow the agent is named in the room
⁠MCP (set on the desktop)
VariableDefaultWhat it does
MCP_SOCK/run/user/1000/sentineldesk-mcp.sockWhere the MCP socket is created. Point it at a mounted volume β€” the compose file uses /run/sentineldesk/mcp.sock β€” so the agent container, or a host-side AI host, can reach it
MCP_POLICYfullWhat the agent may do: full, approve, safe, readonly
MCP_DENY(empty)Additionally deny these tools (ssh_* matches by prefix)
MCP_ALLOW(empty)When set, allow only these
MCP_DISCOVERY01 advertises a core set of twelve tools; the rest stay callable by name
⁠Files, logging, locale and the machine's manners
VariableDefaultWhat it does
FILES_ROOT/home/sentineldeskRoot of the browser file manager. / opens the whole container
ACTION_LOG/var/log/sentineldesk/actions.jsonlAudit log of every MCP call
ACTION_LOG_MAX_MB64Rotation size
ACTIVITY_RETENTION_HOURS24How long activity history is kept
SECRETS_FILE/var/lib/sentineldesk/secretsEncrypted store for saved credentials
TZAmerica/Argentina/Buenos_Aires (compose sets UTC)The desktop's clock. Any tzdata name
KEYBOARD_LAYOUT / KEYBOARD_VARIANTus / (empty)The X keyboard the desktop comes up with β€” e.g. latam, or es with nodeadkeys
LANG / LC_ALLC.UTF-8Locale β€” without UTF-8 a terminal mangles accented characters

TZ and the keyboard are both things somebody notices in the first minute and cannot fix from the browser, which is why they are in the compose file.

⁠Environment variables β€” the agent

The agent container is deliberately almost unconfigured. It needs one thing:

VariableDefaultWhat it does
MCP_SOCK(none)The desktop's socket, reached through the shared sentineldesk-run volume: /run/sentineldesk/mcp.sock

There is no API key in the environment, and that is deliberate β€” a provider key is not something an environment variable should carry into a container image or a shell history. The agent comes up with no model and says so; you add one from inside:

docker exec -it sentineldesk-agent sentineldesk-agent
# then: /connect

The key is checked before it is saved and stored in the sentineldesk-agent volume. The chat panel in the browser goes from amber to green on its own β€” nothing to reload.

⁠The TUI

That same session is the agent's full face β€” the browser panel is the short one:

/helpwhat you can type here
/modelpick the model that answers next β€” switches mid-session, no restart
/connectadd a provider's API key, checked before it is saved
/panel (ctrl+b)the session's context, tokens and cost so far
/compactfold the older conversation into a summary
/rewind (/undo)drop back before an earlier task β€” the desktop is not rewound
/memoryhave something remembered, permanently
/stop (ctrl-c)end the run after this turn
/languagethe language of the interface
/exit (ctrl-d)leave the session

Useful flags on the same binary:

docker exec -it sentineldesk-agent sentineldesk-agent -mode ask      # approve every change
docker exec -it sentineldesk-agent sentineldesk-agent -doctor        # 15 checks, no model, no spend
docker exec -it sentineldesk-agent sentineldesk-agent -history       # every past run: turns, calls, cost
docker exec -it sentineldesk-agent sentineldesk-agent \
  -run "install nginx and show me it working" -max-turns 40 -max-spend 0.50 -max-time 10m

-mode ask stops before every call that changes something and lets reads through, because a confirmation for every screenshot trains somebody to press y without reading. -doctor is the first thing to run when something is wrong: if it fails, the problem is not the key.

⁠Running the agent on the host instead

The runtime finds the socket in the sentineldesk-run volume by itself:

docker compose up -d sentineldesk   # the desktop only
sentineldesk-agent -serve           # the agent, on the host

That is the lightest way to work while changing the agent. Do not run both: two runtimes on one desktop means the newer connection wins and the older is dropped. Nothing breaks; it is just confusing. Pick one.

⁠Serving it on a LAN or a VPS

One variable does it β€” compose wires NAT1TO1_IP, TLS_HOSTS and the UDP range together from it:

HOST_IP=203.0.113.10 AUTH_PASS=change-me docker compose up -d
# β†’ open https://203.0.113.10:8080 and accept the self-signed certificate once
  • NAT1TO1_IP β€” the address ICE advertises. Without it the page stalls at β€œEstablishing WebRTC”, because the container advertised its bridge address.
  • TLS_HOSTS with TLS_SELFSIGNED=1 β€” HTTPS for that host, the certificate kept in the home volume so it is generated once. This is more than the lock icon: browsers only allow the microphone and the rich clipboard on a secure origin, so a desktop reached over plain HTTP is quietly missing features.
  • WEBRTC_MIN_PORT / WEBRTC_MAX_PORT β€” pinned to the published UDP range, which is what makes the media connect without host networking.

A relay for hostile networks. Peer-to-peer fails on symmetric NAT and where UDP is dropped. A coturn service is in the compose file, behind a profile and off by default, because a relay carries every frame through it:

docker compose --profile turn up -d

It takes 3478/tcp (the desktop's embedded responder keeps 3478/udp) and 49160-49200/udp, with TURN_USER / TURN_PASS for its credentials. Set CLIENT_TURN_URLS on the desktop when it is running, or browsers are never told the relay exists.

Built-in VPN (optional). The desktop can dial an OpenVPN connection, which needs a tunnel device and the capability to configure routes: uncomment cap_add: NET_ADMIN and devices: /dev/net/tun on the sentineldesk service. Leave them off on a deployment that will never dial one.

⁠Driving it from your own AI host

The agent container is one way to use the MCP plane; your own AI host is another, and they are the same socket. With MCP_SOCK on a mounted volume, any MCP host on the machine reaches it with a plain stdio relay β€” no docker exec, no Docker socket:

{
  "mcpServers": {
    "sentineldesk": {
      "command": "sudo",
      "args": ["socat", "STDIO",
               "UNIX-CONNECT:/var/lib/docker/volumes/sentineldesk-run/_data/mcp.sock"]
    }
  }
}

The agent is a participant, not an API bolted to the side. It shares the screen people are looking at: control is claimed and never assumed, every command runs in a terminal window the room can watch, and its only reach is that local Unix socket β€” 137 MCP tools covering the screen, the accessibility tree, the browser, windows, processes, SSH, recording and streaming, each classified by risk and by whether it requires control.

Full setup, the four permission levels and the tool catalogue: docs/mcp.md⁠.

⁠Security

The WebSocket login is the authentication gate; no HTTP endpoint returns secrets. Both Unix sockets β€” MCP and the chat wire β€” are local and 0600, and the agent container reaches the desktop through nothing else: no ports, no network of its own, no shared display or home. Inside the desktop container the desktop user has passwordless sudo β€” the container is the sandbox, so tighten with Docker's own limits and do not mount sensitive host paths. Every MCP call lands in the audit log in sentineldesk-audit, which is a separate volume so that wiping a browser profile never takes the record with it.

⁠License

Apache 2.0. The name and logo are trademarks of Federico Pereira and are not covered by that license β€” see the trademark policy⁠.

Tag summary

Content type

Image

Digest

sha256:3bc32b15f…

Size

914.9 MB

Last updated

21 days ago

docker pull cnsoluciones/sentineldesk