Docker Container Exec Browser Self-Hosted: A Safe, Fully Audited Shell Guide

docker-container-exec-browser-self-hosted.md
title Docker Container Exec Browser Self-Hosted: A Safe, Fully Audited Shell Guide
date
author VahaC
read 13 min read
category Self-Hosting
tags #container #Docker #Homelab #Linux #Security #Stashboard
docker container exec browser self-hosted

Docker container exec browser self-hosted access in Stashboard opens an interactive shell inside a running container, right from the dashboard, with no SSH login and no docker CLI on the device in front of you. It arrived in V5.7, and V5.8 added a session audit viewer that shows every shell you’ve opened. 🐳

The browser SSH terminal guide covered the host-level shell. This post covers the other half: how the docker container exec browser self-hosted path works, how to enable it, which shell to pick, how to read the audit trail, and what can go wrong.

Docker Container Exec Browser Self-Hosted vs the Host Terminal

Both features give you an xterm window in the browser, but they land in very different places:

  • Where you land: the host terminal logs you in to the Docker host itself. Exec drops you inside one container’s filesystem and process namespace.
  • Transport: the host terminal is an SSH login. Exec goes through the Docker daemon’s exec API, the same mechanism behind docker exec -it.
  • Connection types: the host terminal needs an SSH tunnel connection. The docker container exec browser self-hosted shell works on every connection type: local socket, TCP+TLS and SSH tunnel.
  • Identity: the host terminal runs as your SSH user. Exec runs as the container’s configured user, which is root for most images.
  • Command: the host terminal always starts a login shell. Exec lets you choose the command before you connect.
  • Resize: the host terminal keeps its connect-time size. Exec follows the panel size live, because the daemon exposes an exec-resize endpoint.
  • Gates: the host terminal needs three gates. Exec needs two (details below).

Since V5.7, the shell button on each container card opens Exec, and the host terminal lives at the connection level. If you’re working on Proxmox rather than Docker, the browser LXC console is the equivalent for LXC guests.

How a Docker Container Exec Browser Self-Hosted Session Is Secured

Exec reuses the host terminal’s token-less WebSocket transport. A browser WebSocket can’t send an Authorization header, so every docker container exec browser self-hosted session starts with a ticket handshake:

  1. Connect sends an authenticated POST to /api/docker/connections/{id}/containers/{name}/exec/ticket, with the command in the body. The server checks both gates and that you own the connection.
  2. It mints a ticket: 32 random bytes from a cryptographic RNG, bound to your user ID, that connection, that container and that command. It’s valid for 30 seconds and lives only in server memory.
  3. The browser opens wss://…/containers/{name}/exec/ws?ticket=…&cols=…&rows=…. No login token appears in the URL.
  4. The server redeems the ticket, deleting it on first use. A replayed or expired ticket, or one presented for a different container, gets a 401. It then re-checks the gates and ownership.
  5. It checks the concurrency caps, writes the audit row, and asks the daemon to create a TTY exec instance with stdin, stdout and stderr attached, then starts it on a hijacked stream.

Because the command is bound to the ticket server-side, a tampered WebSocket URL can’t swap /bin/sh for something else. Personal access tokens (V10.4) get a 403 on the ticket endpoint, so a leaked automation token can’t open a shell.

Two gates, both required

  • Server-wide switch: Settings → Container exec, stored in the database and off by default. The optional Stashboard:AllowContainerExec config flag only seeds it on first run.
  • Per-connection opt-in: Allow container exec on each Docker connection, also off by default.

If either gate fails, the server refuses the ticket, so the protection isn’t just a hidden button.

Prerequisites

Before you enable docker container exec browser self-hosted access, you need:

  • A current Stashboard install. The steps follow the 10.x UI.
  • At least one Docker connection that shows its containers on the Docker page.
  • A running container whose image ships a shell or the CLI you want to run.
  • A reverse proxy that forwards WebSocket upgrades, if one sits in front of Stashboard. HTTPS is strongly recommended.
  • A clear answer to “who can log in to my Stashboard?”, because they can now get a shell inside your containers.

Step-by-Step: Enable Docker Container Exec Browser Self-Hosted Access

1. Turn on the server-wide switch 🔓

Open Settings → Container exec (listed under Docker). Read the warning box, tick Enable container exec server-wide and click Save. You should see “Container exec enabled server-wide.” No restart is needed. On its own, this switch opens nothing: docker container exec browser self-hosted access still needs step 2.

2. Opt the connection in

On the Docker page, choose ⋮ → Edit on the host card. Tick Allow container exec (interactive shell inside a container) and click Save changes. Unlike the host terminal checkbox, this one appears for every host type. Enabling docker container exec browser self-hosted access on one host never enables it on another.

3. Open the Exec tab

The docker container exec browser self-hosted terminal lives on each container’s Exec tab, and you can reach it three ways:

  • Click the >_ button on the container card (tooltip: “Open a shell inside this container (exec)”).
  • Right-click the card and choose Exec.
  • Open the container modal and switch to the Exec tab.

4. Choose the command 🐚

The Command field defaults to /bin/sh, which almost every Linux image ships. Change it to /bin/bash if the image has Bash, or point it at a tool bundled in the image, such as redis-cli or psql -U postgres, to skip the shell entirely.

⚠️ The command is split on spaces, with no shell quoting. psql -U postgres works, but sh -c "ls /data" arrives as broken arguments. For anything with quotes or pipes, open a shell first and type it there.

5. Connect

Click Connect. The reminder “You are about to open a shell inside this container. Every session is audited.” sits next to the button. The status moves from connecting to connected, and your prompt appears. That’s the docker container exec browser self-hosted shell, live. 🎉

Inside the Exec Window

The docker container exec browser self-hosted window is xterm.js 5.5 with its fit add-on, the same terminal the host shell uses:

  • Status and buttons: idle, connecting, connected, closed or error, next to Connect, Disconnect or Reconnect. The Command field locks while a session is open.
  • Scrollback: 5,000 lines.
  • Copy and paste: Ctrl+Shift+C copies and Ctrl+Shift+V pastes. Right-click copies a selection or pastes when nothing is selected. Plain Ctrl+C still interrupts the running process.
  • Live resize: resize the browser and the PTY follows, so htop and vim redraw correctly.
  • Close reasons: printed in grey, such as [ Shell exited. ] or [ Closed: idle timeout. ].

⚠️ Switching to another modal tab ends the session. Leaving Exec for Logs, or closing the modal, tears down the socket. Nothing keeps running in the background except processes you deliberately detached inside the container.

Concurrency Caps and the Idle Timeout ⏱️

Every docker container exec browser self-hosted session counts against its own limits, separate from the host terminal’s:

  • MaxSessionsPerUser = 3: open exec sessions per user, across all connections.
  • MaxSessionsPerHost = 5: open exec sessions against one connection.
  • IdleTimeoutSeconds = 600: the server closes a session after 10 minutes with no bytes in either direction. 0 disables it.
  • TicketTtlSeconds = 30: how long a minted ticket stays valid.
  • DefaultCommand = /bin/sh: used when the Command field is blank.

Over a cap, the socket closes immediately with a reason like You already have 3 exec session(s) open (limit 3). To change the limits, add them to the .env next to docker-compose.yml:

STASHBOARD_Stashboard__ContainerExec__MaxSessionsPerUser=3
STASHBOARD_Stashboard__ContainerExec__MaxSessionsPerHost=5
STASHBOARD_Stashboard__ContainerExec__IdleTimeoutSeconds=600
STASHBOARD_Stashboard__ContainerExec__DefaultCommand=/bin/sh

Then recreate the container:

docker compose up -d

⚠️ Don’t set the idle timeout to 0. A forgotten tab would hold a root shell inside your container open indefinitely.

The Session Audit Viewer: Docker Container Exec Browser Self-Hosted History 📜

Before V5.8, shell sessions were recorded but only readable by querying the database. The Audit page in the sidebar now shows them, read-only and newest-first. Two tabs matter here:

  • Host terminal: connection name, SSH user and host, start, end, duration, bytes In (from the browser) and Out (to the browser), and the end reason.
  • Container exec: connection, container, command, start, end, duration, In, Out and end reason.

The same page also holds update attempts, image prune runs, Compose changes and the Proxmox trails. View session history under the exec panel jumps to Audit → Container exec, filtered to that connection, with a Clear filter link.

Each docker container exec browser self-hosted row is written when the session starts, before the daemon is contacted, and finalised when it ends. An open session shows an Active badge. The end reasons are:

  • Closed by client: you clicked Disconnect, closed the modal or switched tabs.
  • Remote closed: the process exited, for example after you typed exit.
  • Idle timeout: 10 minutes of silence.
  • Closed by server: Stashboard stopped gracefully.
  • Error: the exec couldn’t start, with the daemon’s message shown inline.
  • Interrupted (server restart): a startup sweep closes rows a crash left open. Their duration reads 0s, since the real end time is unknown.

A few more details. Each account sees only the sessions it started. Rows keep the connection and container names as they were at the time, so history survives a rename or a deleted connection. Timestamps display in your browser’s local time, and the page shows the newest 200 rows per tab. Every session also logs Container exec opened and Container exec closed lines, which you can read with docker logs stashboard-app.

Export the trail with a read-only API token

The audit endpoints are plain GETs, so a Read scoped personal access token can pull docker container exec browser self-hosted history into a SIEM, a spreadsheet or a cron job. Pages hold up to 200 rows; use skip for older ones. This example uses jq:

curl -s -H "Authorization: Bearer sb_pat_..." \
  "https://stashboard.example.com/api/docker/container-exec/sessions?take=50" \
  | jq -r '.[] | [.startedUtc, .connectionName, .containerName, .command, .endReason] | @tsv'

Swap the path for /api/docker/host-shell/sessions to get host-terminal rows, and add &connectionId=… to narrow either one.

⚠️ The audit records the starting command, not your keystrokes. A session that opened /bin/sh shows /bin/sh, whatever you typed afterwards. Bytes tell you how much happened, not what.

How to Verify Your Docker Container Exec Browser Self-Hosted Setup ✅

Run these checks once, before you rely on the docker container exec browser self-hosted shell:

  1. Gate check: turn off Settings → Container exec and open a container’s Exec tab. You should see “Container exec is disabled on this server” and no terminal. Turn it back on.
  2. Right place: connect and run hostname; whoami. Inside a container, the hostname is usually the 12-character container ID (unless the Compose file sets hostname:), and whoami shows the container’s user.
  3. Live resize: run stty size, resize the browser window, and run it again. The numbers should change.
  4. Audit row: open Audit → Container exec in a second tab and look for the Active badge. Type exit, reload, and the row should read Remote closed with a duration and byte counts.
  5. Token refusal: try to mint a ticket with a personal access token. Expect 403 Forbidden.
curl -i -X POST -H "Authorization: Bearer sb_pat_..." \
  -H "Content-Type: application/json" -d '{"command":"/bin/sh"}' \
  https://stashboard.example.com/api/docker/connections/CONNECTION_ID/containers/CONTAINER_NAME/exec/ticket

Troubleshooting 🔧

Most docker container exec browser self-hosted failures come down to one of these:

  • “Container exec is disabled on this server.” Enable Settings → Container exec.
  • “Container exec is not enabled for this connection.” Tick Allow container exec in the connection’s Edit form.
  • “Container is not running.” Exec attaches to a live container. Start it, then reopen the tab.
  • The session closes straight after connecting. The command probably doesn’t exist in the image. /bin/bash on Alpine is the classic case, and the daemon typically prints an OCI runtime exec failed … no such file or directory line. Switch to /bin/sh.
  • No shell at all. Distroless and scratch images ship none, so exec has nothing to run. Use the Logs tab instead.
  • The WebSocket never connects. Your reverse proxy must forward the Upgrade and Connection headers. The Troubleshooting section of the host terminal guide has the nginx and Nginx Proxy Manager settings.
  • Ctrl+Shift+V does nothing. Paste uses the async Clipboard API, which works only over HTTPS or on localhost.

Security Angle: Risks of Browser Exec ⚠️

Docker container exec browser self-hosted access is remote code execution inside your workloads by design. Know what you’re turning on:

  • Root in the container is the default. Stashboard doesn’t pick a user; exec runs as whatever the image or Compose file sets, and that’s usually root.
  • Some containers are the host. ⚠️ Exec into a privileged container, or one with /var/run/docker.sock mounted (Portainer, Dozzle, Watchtower, or Stashboard itself if you mounted the socket), and you control the daemon, which means root on the host. Docker’s daemon attack surface notes explain why.
  • Secrets are one command away. env prints every password and API key passed as an environment variable.
  • Your Stashboard login is now an exec login. There’s no separate admin role, so protect the account with TOTP (V10.3) or SSO (V10.5), and reach Stashboard over a VPN as in the secure remote access guide.
  • Switching it off doesn’t kill open sessions. Gates are checked at connect time, so an open docker container exec browser self-hosted shell runs until it ends, times out or Stashboard restarts.
  • Hot fixes don’t survive. Changes made inside a container vanish when it’s recreated, for example by a one-click update. Fix the image or the Compose file instead.

For hardening the containers themselves, work through the Docker security checklist.

Docker Container Exec Browser Self-Hosted FAQ

Does docker container exec browser self-hosted access need SSH?

No. Exec goes through the Docker daemon, so it works on local-socket, TCP+TLS and SSH tunnel connections alike.

Which user does the docker container exec browser self-hosted shell run as?

The container’s configured user, from the image’s USER or the Compose user: key. Stashboard has no user picker.

Can I exec into a stopped container?

No. The panel shows “Container is not running” until you start it.

Does the audit viewer record the commands I type?

No. It records the starting command, timing, byte counts and end reason. Capture keystrokes inside the container if you need them.

Can I script docker container exec browser self-hosted sessions with an API token?

No, ticket requests from personal access tokens get a 403. A Read token can still export the audit trail.

Bottom Line

The Exec tab turns “SSH to the host, find the container, run docker exec” into two clicks, without handing out host credentials. Both gates must pass, tickets are single-use and bound to one container and command, caps and the idle timeout run server-side, and the Audit page shows every session. Enable docker container exec browser self-hosted access on one connection, run the checks above, and keep it away from containers that hold the Docker socket. 🚀

New to Stashboard? Start with the homelab dashboard overview. Stashboard is open source: code on GitHub, image on Docker Hub.

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.