Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Finsys/dockhand/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Dockhand provides browser-based terminal access to running containers using xterm.js and WebSocket connections. Execute commands, run interactive shells, and manage containers without SSH or local Docker CLI access.
Terminal Access
Creating an Exec Session
Start a shell session in a container:
POST /api/containers/{id}/exec?envId={environmentId}
Request body:
{
"shell": "/bin/bash",
"user": "root"
}
Response:
{
"execId": "abc123def456",
"connectionInfo": {
"type": "socket",
"host": "localhost",
"port": 2375
}
}
Implementation
import { createExec, getDockerConnectionInfo } from '$lib/server/docker';
import { authorize } from '$lib/server/authorize';
export const POST: RequestHandler = async ({ params, request, cookies, url }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !auth.isAuthenticated) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
const containerId = params.id;
const envIdParam = url.searchParams.get('envId');
const envId = envIdParam ? parseInt(envIdParam, 10) : undefined;
// Permission check with environment context
if (!await auth.can('containers', 'exec', envId)) {
return json({ error: 'Permission denied' }, { status: 403 });
}
try {
const body = await request.json().catch(() => ({}));
const shell = body.shell || '/bin/sh';
const user = body.user || 'root';
// Create exec instance
const exec = await createExec({
containerId,
cmd: [shell],
user,
envId
});
// Get connection info for the frontend
const connectionInfo = await getDockerConnectionInfo(envId);
return json({
execId: exec.Id,
connectionInfo: {
type: connectionInfo.type,
host: connectionInfo.host,
port: connectionInfo.port
}
});
} catch (error: any) {
console.error('Failed to create exec:', error);
return json(
{ error: error.message || 'Failed to create exec instance' },
{ status: 500 }
);
}
};
WebSocket Connection
Connecting to Terminal
Once an exec session is created, connect via WebSocket:
// Frontend WebSocket connection
const ws = new WebSocket(
`ws://${connectionInfo.host}:${connectionInfo.port}/exec/${execId}/attach?stream=1&stdin=1&stdout=1&stderr=1`
);
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
console.log('Terminal connected');
};
ws.onmessage = (event) => {
// Docker multiplexes streams with 8-byte headers
const data = parseDockerStream(event.data);
terminal.write(data);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = () => {
console.log('Terminal disconnected');
};
Docker multiplexes stdout/stderr using 8-byte headers:
Header = [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4}
STREAM_TYPE:
0: stdin
1: stdout
2: stderr
3: systemerr
SIZE1-4: uint32 (big-endian) payload size
Parser implementation:
function parseDockerStream(buffer: ArrayBuffer): string {
const view = new DataView(buffer);
let output = '';
let offset = 0;
while (offset < buffer.byteLength) {
if (buffer.byteLength - offset < 8) break;
// Read header
const streamType = view.getUint8(offset);
const payloadSize = view.getUint32(offset + 4, false); // big-endian
offset += 8;
if (buffer.byteLength - offset < payloadSize) break;
// Read payload
const payloadBytes = new Uint8Array(buffer, offset, payloadSize);
const text = new TextDecoder().decode(payloadBytes);
// Color stderr output in red (optional)
if (streamType === 2) {
output += `\x1b[31m${text}\x1b[0m`;
} else {
output += text;
}
offset += payloadSize;
}
return output;
}
Terminal Emulator
xterm.js Integration
The frontend uses xterm.js for terminal emulation:
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { WebLinksAddon } from 'xterm-addon-web-links';
const terminal = new Terminal({
cursorBlink: true,
fontSize: 14,
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
theme: {
background: '#1e1e1e',
foreground: '#d4d4d4',
cursor: '#ffffff',
selection: '#264f78',
black: '#000000',
red: '#cd3131',
green: '#0dbc79',
yellow: '#e5e510',
blue: '#2472c8',
magenta: '#bc3fbc',
cyan: '#11a8cd',
white: '#e5e5e5'
},
scrollback: 10000,
allowProposedApi: true
});
// Fit terminal to container
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
// Enable clickable links
const webLinksAddon = new WebLinksAddon();
terminal.loadAddon(webLinksAddon);
// Mount to DOM
terminal.open(document.getElementById('terminal'));
fitAddon.fit();
// Handle user input
terminal.onData((data) => {
ws.send(data);
});
// Auto-resize
window.addEventListener('resize', () => {
fitAddon.fit();
});
Shell Selection
Detecting Available Shells
Automatically detect which shell is available in the container:
const SHELL_PRIORITY = [
'/bin/bash',
'/bin/zsh',
'/bin/sh',
'/bin/ash',
'/bin/dash'
];
async function detectShell(containerId: string): Promise<string> {
for (const shell of SHELL_PRIORITY) {
try {
const result = await execCommand(containerId, ['test', '-f', shell]);
if (result.exitCode === 0) {
return shell;
}
} catch {
continue;
}
}
return '/bin/sh'; // Default fallback
}
Shell-Specific Features
const SHELL_FEATURES = {
'/bin/bash': {
autocompletion: true,
history: true,
colorPrompt: true,
initCommand: 'export PS1="\\[\\033[01;32m\\]\\u@\\h\\[\\033[00m\\]:\\[\\033[01;34m\\]\\w\\[\\033[00m\\]\\$ "'
},
'/bin/zsh': {
autocompletion: true,
history: true,
colorPrompt: true,
initCommand: 'export PS1="%F{green}%n@%m%f:%F{blue}%~%f%# "'
},
'/bin/sh': {
autocompletion: false,
history: false,
colorPrompt: false,
initCommand: ''
}
};
User Selection
Running as Different Users
Execute shell as any user in the container:
// Root user (default)
{
"user": "root"
}
// Application user
{
"user": "node"
}
// Specific UID:GID
{
"user": "1000:1000"
}
Permission Implications
# Root user - full access
$ rm -rf /app/critical-file # ✓ Allowed
# Non-root user - limited access
$ rm -rf /app/critical-file # ✗ Permission denied
Advanced Features
Command History
Shells with history support maintain command history:
# Bash/Zsh history
$ history
$ !123 # Re-run command 123
$ !! # Re-run last command
$ !$ # Last argument of previous command
Tab Completion
Bash and Zsh support tab completion:
$ cd /ap<TAB> # Completes to /app/
$ docker ps<TAB> # Shows docker ps options
Copy/Paste Support
// Configure xterm.js copy/paste
terminal.attachCustomKeyEventHandler((event) => {
// Ctrl+C: Copy (when text selected)
if (event.ctrlKey && event.key === 'c' && terminal.hasSelection()) {
document.execCommand('copy');
return false;
}
// Ctrl+V: Paste
if (event.ctrlKey && event.key === 'v') {
navigator.clipboard.readText().then((text) => {
ws.send(text);
});
return false;
}
return true;
});
Terminal Resize
Dynamically resize terminal to match window:
import { FitAddon } from 'xterm-addon-fit';
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
// Fit on mount
fitAddon.fit();
// Fit on window resize
const resizeObserver = new ResizeObserver(() => {
fitAddon.fit();
// Notify Docker of new size
const { rows, cols } = terminal;
fetch(`/api/containers/${containerId}/exec/${execId}/resize`, {
method: 'POST',
body: JSON.stringify({ rows, cols })
});
});
resizeObserver.observe(terminalContainer);
Multiple Terminals
Tabbed Interface
Open multiple terminals to the same or different containers:
interface TerminalTab {
id: string;
containerId: string;
containerName: string;
shell: string;
user: string;
terminal: Terminal;
ws: WebSocket;
}
const tabs: TerminalTab[] = [];
function addTab(containerId: string, containerName: string) {
const tab = {
id: generateId(),
containerId,
containerName,
shell: '/bin/bash',
user: 'root',
terminal: new Terminal(),
ws: null
};
tabs.push(tab);
connectTerminal(tab);
}
function closeTab(tabId: string) {
const tab = tabs.find(t => t.id === tabId);
if (tab) {
tab.ws?.close();
tab.terminal.dispose();
tabs.splice(tabs.indexOf(tab), 1);
}
}
Security Considerations
Authentication
All terminal sessions require authentication:
if (auth.authEnabled && !auth.isAuthenticated) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
Authorization
Users need containers:exec permission:
if (!await auth.can('containers', 'exec', envId)) {
return json({ error: 'Permission denied' }, { status: 403 });
}
Audit Logging
All exec sessions are logged:
await auditLog({
action: 'container_exec',
entityType: 'container',
entityId: containerId,
entityName: containerName,
userId: auth.userId,
details: {
shell,
user,
timestamp: new Date().toISOString()
}
});
Session Timeout
Terminal sessions timeout after inactivity:
const SESSION_TIMEOUT = 30 * 60 * 1000; // 30 minutes
let lastActivity = Date.now();
ws.onmessage = (event) => {
lastActivity = Date.now();
handleMessage(event);
};
const timeoutCheck = setInterval(() => {
if (Date.now() - lastActivity > SESSION_TIMEOUT) {
console.log('Session timeout');
ws.close();
clearInterval(timeoutCheck);
}
}, 60000); // Check every minute
Best Practices
Terminal Usage
- Close sessions when done to free resources
- Use appropriate user - avoid root when possible
- Don’t run long tasks - use scheduled jobs instead
- Monitor resource usage - terminals consume memory
- Audit sensitive operations - track who did what
Shell Selection
- Try bash first - most feature-rich
- Fall back to sh - guaranteed to exist
- Test shell availability before creating exec
- Match container’s default - respect Dockerfile USER
- Limit scrollback - reduce memory usage
- Close inactive terminals - free connections
- Use connection pooling - reuse WebSocket connections
- Throttle output - prevent browser overload
// Throttle terminal output
let outputBuffer = '';
let throttleTimeout: number | null = null;
ws.onmessage = (event) => {
outputBuffer += parseDockerStream(event.data);
if (!throttleTimeout) {
throttleTimeout = setTimeout(() => {
terminal.write(outputBuffer);
outputBuffer = '';
throttleTimeout = null;
}, 16); // ~60fps
}
};
Troubleshooting
Connection Failed
Error: Failed to connect to WebSocket
Solutions:
- Check container is running
- Verify Docker daemon is accessible
- Check firewall rules
- Ensure WebSocket proxy is configured correctly
Shell Not Found
Error: exec: "/bin/bash": stat /bin/bash: no such file or directory
Solution: Try different shell (/bin/sh, /bin/ash)
Permission Denied
Error: OCI runtime exec failed: exec failed: unable to start container process: exec: "/bin/bash": permission denied
Solutions:
- Check container security settings
- Try running as root user
- Verify container is not in restricted mode
Terminal Not Rendering
Terminal appears blank or garbled
Solutions:
- Resize terminal window
- Send newline character to trigger refresh
- Check xterm.js theme configuration
- Clear terminal and reconnect
Limitations
Container Requirements
- Container must be running
- Shell must be present in container
- Container must allow exec (no
--no-new-privileges flag)
Browser Limitations
- Requires WebSocket support
- May have issues with mobile browsers
- Limited clipboard integration
- No true terminal emulation (only ANSI escape codes)
Docker API Limitations
- No TTY resizing after creation (must recreate exec)
- Limited signal support
- No job control (background processes)
API Reference
# Create exec session
POST /api/containers/{id}/exec?envId={environmentId}
# Attach to exec (WebSocket)
WS /exec/{execId}/attach?stream=1&stdin=1&stdout=1&stderr=1
# Resize terminal
POST /api/containers/{id}/exec/{execId}/resize
# Inspect exec
GET /api/containers/{id}/exec/{execId}