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’s file browser provides a web-based interface for managing files inside running containers and Docker volumes. Browse directories, edit files, upload/download content, and manage permissions without shell access.
Container File Browser
Browsing Container Files
Access the filesystem of any running container:
GET /api/containers/{id}/files?path=/app&env={environmentId}
Response:
{
"path": "/app",
"entries": [
{
"name": "config",
"type": "directory",
"size": 4096,
"permissions": "drwxr-xr-x",
"owner": "root",
"group": "root",
"modified": "2024-03-04T10:30:00Z"
},
{
"name": "app.js",
"type": "file",
"size": 15234,
"permissions": "-rw-r--r--",
"owner": "node",
"group": "node",
"modified": "2024-03-04T10:25:00Z"
}
]
}
Implementation
import { listContainerDirectory } from '$lib/server/docker';
import { authorize } from '$lib/server/authorize';
export const GET: RequestHandler = async ({ params, url, cookies }) => {
const auth = await authorize(cookies);
const path = url.searchParams.get('path') || '/';
const envId = url.searchParams.get('env');
const envIdNum = envId ? parseInt(envId) : undefined;
// Permission check with environment context
if (auth.authEnabled && !await auth.can('containers', 'view', envIdNum)) {
return json({ error: 'Permission denied' }, { status: 403 });
}
try {
const result = await listContainerDirectory(
params.id,
path,
envIdNum
);
return json(result);
} catch (error: any) {
console.error('Error listing container directory:', error);
return json({ error: error.message || 'Failed to list directory' }, { status: 500 });
}
};
File Operations
Reading File Content
Read the contents of a text file:
GET /api/containers/{id}/files/content?path=/app/config.json&env={environmentId}
Response:
{
"content": "{\n \"port\": 3000,\n \"host\": \"0.0.0.0\"\n}",
"path": "/app/config.json"
}
Writing File Content
Update or create a file:
PUT /api/containers/{id}/files/content?path=/app/config.json&env={environmentId}
{
"content": "{\n \"port\": 8080,\n \"host\": \"0.0.0.0\"\n}"
}
File Size Limits
// Max file size for reading/writing (1MB)
const MAX_FILE_SIZE = 1024 * 1024;
export const GET: RequestHandler = async ({ params, url, cookies }) => {
const content = await readContainerFile(
params.id,
path,
envIdNum
);
// Check if content is too large
if (content.length > MAX_FILE_SIZE) {
return json({ error: 'File is too large to edit (max 1MB)' }, { status: 413 });
}
return json({ content, path });
};
Creating Files/Directories
POST /api/containers/{id}/files/create?path=/app/newdir&env={environmentId}
{
"type": "directory",
"name": "logs"
}
Or create a file:
{
"type": "file",
"name": "config.yaml",
"content": "port: 8080\nhost: 0.0.0.0"
}
Deleting Files
DELETE /api/containers/{id}/files/delete?path=/app/old-file.txt&env={environmentId}
Renaming Files
POST /api/containers/{id}/files/rename?path=/app/old-name.txt&env={environmentId}
{
"newName": "new-name.txt"
}
Changing Permissions
POST /api/containers/{id}/files/chmod?path=/app/script.sh&env={environmentId}
File Upload/Download
Uploading Files
Upload one or more files to a container:
POST /api/containers/{id}/files/upload?path=/app/uploads&env={environmentId}
The implementation creates a TAR archive for Docker API:
/**
* Create a simple tar archive from a single file
* TAR format: 512-byte header followed by file content padded to 512 bytes
*/
function createTarArchive(filename: string, content: Uint8Array): Uint8Array {
// TAR header is 512 bytes
const header = new Uint8Array(512);
const encoder = new TextEncoder();
// File name (100 bytes)
const nameBytes = encoder.encode(filename.slice(0, 99));
header.set(nameBytes, 0);
// File mode (8 bytes) - 0644
header.set(encoder.encode('0000644\0'), 100);
// File size in octal (12 bytes)
const sizeOctal = content.length.toString(8).padStart(11, '0');
header.set(encoder.encode(sizeOctal + '\0'), 124);
// Calculate checksum
let checksum = 0;
for (let i = 0; i < 512; i++) {
checksum += header[i];
}
const checksumOctal = checksum.toString(8).padStart(6, '0') + '\0 ';
header.set(encoder.encode(checksumOctal), 148);
// Combine header, content, and padding
const paddingSize = (512 - (content.length % 512)) % 512;
const padding = new Uint8Array(paddingSize);
const endMarker = new Uint8Array(1024);
const totalSize = header.length + content.length + paddingSize + endMarker.length;
const tar = new Uint8Array(totalSize);
let offset = 0;
tar.set(header, offset);
offset += header.length;
tar.set(content, offset);
offset += content.length;
tar.set(padding, offset);
offset += paddingSize;
tar.set(endMarker, offset);
return tar;
}
export const POST: RequestHandler = async ({ params, url, request, cookies }) => {
const formData = await request.formData();
const files = formData.getAll('files') as File[];
const uploaded: string[] = [];
const errors: string[] = [];
for (const file of files) {
try {
const content = new Uint8Array(await file.arrayBuffer());
const tar = createTarArchive(file.name, content);
await putContainerArchive(
params.id,
path,
tar,
envId ? parseInt(envId) : undefined
);
uploaded.push(file.name);
} catch (err: any) {
errors.push(`${file.name}: ${err.message}`);
}
}
return json({
success: true,
uploaded,
errors: errors.length > 0 ? errors : undefined
});
};
Downloading Files
GET /api/containers/{id}/files/download?path=/app/logs/app.log&env={environmentId}
Returns the file as a download with appropriate Content-Disposition header.
Volume File Browser
Browsing Volume Files
Browse files in a Docker volume:
GET /api/volumes/{name}/browse?path=/data&env={environmentId}
Response:
{
"path": "/data",
"entries": [
{
"name": "database",
"type": "directory",
"size": 4096,
"permissions": "drwxr-xr-x"
},
{
"name": "backup.sql",
"type": "file",
"size": 104857600,
"permissions": "-rw-r--r--"
}
],
"usage": [
{
"containerId": "abc123",
"containerName": "postgres",
"mountPath": "/var/lib/postgresql/data"
}
],
"isInUse": true,
"helperId": "dockhand-volume-browse-abc123"
}
Volume Browser Implementation
Volumes are accessed through temporary helper containers:
export const GET: RequestHandler = async ({ params, url, cookies }) => {
const auth = await authorize(cookies);
const envId = url.searchParams.get('env');
const envIdNum = envId ? parseInt(envId) : undefined;
const path = url.searchParams.get('path') || '/';
// Permission check
if (auth.authEnabled && !await auth.can('volumes', 'inspect', envIdNum)) {
return json({ error: 'Permission denied' }, { status: 403 });
}
try {
// Check if volume is in use by any containers
const usage = await getVolumeUsage(params.name, envIdNum);
const isInUse = usage.length > 0;
// Mount read-only if in use, otherwise writable
const result = await listVolumeDirectory(params.name, path, envIdNum, isInUse);
// Helper container is cached and reused for subsequent requests
// Cache TTL handles cleanup automatically
return json({
path: result.path,
entries: result.entries,
usage,
isInUse,
// Expose helper container ID so frontend can use container file endpoints
helperId: result.containerId
});
} catch (error: any) {
console.error('Failed to browse volume:', error);
if (error.message?.includes('No such file or directory')) {
return json({ error: 'Directory not found', path }, { status: 404 });
}
return json({
error: 'Failed to browse volume',
details: error.message || String(error)
}, { status: 500 });
}
};
Helper Container Lifecycle
- On first browse request: Create a temporary container with volume mounted
- Subsequent requests: Reuse the same container (cached for 30 minutes)
- Automatic cleanup: Helper containers are removed after TTL expires
- Manual cleanup: Helper containers can be released immediately
Release Helper Container
POST /api/volumes/{name}/browse/release?env={environmentId}
Manually release the helper container to free resources immediately.
File Editor Integration
The frontend integrates a code editor (Monaco/CodeMirror) for editing files:
Supported File Types
- Configuration: JSON, YAML, TOML, INI
- Code: JavaScript, TypeScript, Python, Go, Rust, Java
- Markup: HTML, XML, Markdown
- Styles: CSS, SCSS, LESS
- Shell: Bash, Zsh, Shell scripts
- Logs: Plain text with syntax highlighting
Editor Features
- Syntax highlighting
- Auto-completion
- Line numbers
- Search and replace
- Multiple cursor support
- Bracket matching
- Code folding
- Diff view (compare with previous version)
Security Considerations
Permission Requirements
// View files: requires 'containers:view' permission
GET /api/containers/{id}/files
// Edit files: requires 'containers:exec' permission
PUT /api/containers/{id}/files/content
POST /api/containers/{id}/files/upload
DELETE /api/containers/{id}/files/delete
Container Requirements
File operations only work on running containers:
if (container.state !== 'running') {
return json({ error: 'Container is not running' }, { status: 400 });
}
Read-Only Volumes
Volumes mounted read-only cannot be modified:
services:
app:
volumes:
- data:/app/data:ro # Read-only mount
Attempting to write will fail:
{
"error": "Read-only file system"
}
Common Use Cases
Editing Configuration
- Navigate to container’s config directory
- Open configuration file
- Edit in web-based editor
- Save changes
- Restart container if needed
Viewing Logs
- Browse to log directory
- Open log file
- Search for errors or patterns
- Download for local analysis
Uploading Certificates
- Navigate to certificate directory (e.g.,
/etc/ssl/certs)
- Upload new certificate files
- Set appropriate permissions (644 for certs, 600 for keys)
- Reload or restart service
Backup Configuration
- Browse to application directory
- Download configuration files
- Store locally or in version control
- Restore by uploading when needed
Debugging
- Check application files for corruption
- Verify configuration syntax
- Inspect generated files
- Compare with known good versions
Best Practices
File Operations
- Always backup before making changes
- Test changes in non-production first
- Use version control for configuration files
- Set proper permissions after uploads
- Validate syntax before saving configs
Volume Management
- Release helper containers when done browsing
- Use read-only mode for volumes in use
- Avoid large file operations (>100MB) through browser
- Download volumes for backup instead of browsing
Security
- Limit file browser access to authorized users
- Audit file changes through activity logs
- Never edit files containing secrets through browser
- Use proper file permissions (644 for files, 755 for scripts)
- Rotate secrets after viewing in file browser
Limitations
File Size Limits
- Reading: 1MB maximum for text files
- Uploading: Limited by browser and server settings
- Editing: Large files may cause browser performance issues
Container State
- Requires running containers
- Cannot access stopped containers
- Cannot access containers without shell
File System Support
- Works with standard Linux filesystems
- May have issues with special filesystems (proc, sys, dev)
- Binary files display as download only
Troubleshooting
Permission Denied
{"error": "Permission denied to read this file"}
Solutions:
- Check container user permissions
- Use
chmod endpoint to fix permissions
- Exec into container as root if needed
Container Not Running
{"error": "Container is not running"}
Solution: Start the container before accessing files
File Too Large
{"error": "File is too large to edit (max 1MB)"}
Solutions:
- Download file for local editing
- Use terminal/exec for large file operations
- Split file into smaller chunks
Volume Browse Timeout
{"error": "Failed to browse volume"}
Solutions:
- Check if volume exists
- Release and retry helper container
- Verify volume is not corrupted
API Reference
Container Files
# List directory
GET /api/containers/{id}/files?path={path}&env={envId}
# Read file content
GET /api/containers/{id}/files/content?path={path}&env={envId}
# Write file content
PUT /api/containers/{id}/files/content?path={path}&env={envId}
# Upload files
POST /api/containers/{id}/files/upload?path={path}&env={envId}
# Download file
GET /api/containers/{id}/files/download?path={path}&env={envId}
# Create file/directory
POST /api/containers/{id}/files/create?path={path}&env={envId}
# Delete file
DELETE /api/containers/{id}/files/delete?path={path}&env={envId}
# Rename file
POST /api/containers/{id}/files/rename?path={path}&env={envId}
# Change permissions
POST /api/containers/{id}/files/chmod?path={path}&env={envId}
Volume Files
# Browse volume
GET /api/volumes/{name}/browse?path={path}&env={envId}
# Read file from volume (via helper container)
GET /api/volumes/{name}/browse/content?path={path}&env={envId}
# Release helper container
POST /api/volumes/{name}/browse/release?env={envId}