Skip to content
FinsysPublic

About

The agent for Dockhand

Resources

Security policy

Stars

671 stars

Watchers

5 watching

Forks

Repository files navigation

Hawser

Hawser Logo

GitHub Release Build Release Go Version Docker Image License

Remote Docker agent for Dockhand - manage Docker hosts anywhere.

Overview

Hawser is a lightweight Go agent that enables Dockhand to manage Docker hosts in various network configurations. It supports two operational modes:

  • Standard Mode: Agent listens for incoming connections (ideal for LAN/homelab with static IPs)
  • Edge Mode: Agent initiates outbound WebSocket connection to Dockhand (ideal for VPS, NAT, dynamic IP)

Quick Start

Binary

Download the latest release from GitHub Releases.

Standard Mode:

Standard mode exposes the Docker API. When binding a non-loopback address (the 0.0.0.0 default), a TOKEN is required — without one, Hawser refuses to start to avoid exposing an unauthenticated Docker API to the network:

TOKEN=your-secret-token hawser

Hawser takes all its settings from environment variables (see Configuration); the only command-line flags are --help and --version. The port defaults to 2376 (PORT).

For a local-only agent you can bind loopback instead of setting a token:

BIND_ADDRESS=127.0.0.1 hawser

Standard Mode with TLS (recommended for production; the token is still required for non-loopback binds):

TLS_CERT=/path/to/server.crt TLS_KEY=/path/to/server.key TOKEN=your-secret-token hawser

Edge Mode:

DOCKHAND_SERVER_URL=wss://your-dockhand.example.com/api/hawser/connect TOKEN=your-token hawser

Edge Mode with Self-Signed Certificate:

CA_CERT=/path/to/dockhand-ca.crt DOCKHAND_SERVER_URL=wss://your-dockhand.example.com/api/hawser/connect TOKEN=your-token hawser

Edge Mode with TLS Skip Verify (insecure, for testing):

TLS_SKIP_VERIFY=true DOCKHAND_SERVER_URL=wss://your-dockhand.example.com/api/hawser/connect TOKEN=your-token hawser

Systemd Service

Quick Install

  1. Download and install the binary:
curl -fsSL https://raw.githubusercontent.com/Finsys/hawser/main/scripts/install.sh | bash

The installer honours HAWSER_VERSION (default: the latest release), STACKS_DIR (default /data/stacks) and INSTALL_DIR (default /usr/local/bin); pass them to bash, e.g. curl ... | HAWSER_VERSION=0.2.52 STACKS_DIR=/srv/stacks bash. STACKS_DIR sets the directory the service may write to, and is written into the config only when /etc/hawser/config does not exist yet: on an existing install, change STACKS_DIR in the config as well. On Alpine (OpenRC) it installs an init script instead of a systemd unit, with logs in /var/log/hawser.log.

  1. Configure the service:
sudo nano /etc/hawser/config

The installer's config contains TOKEN=change-me-before-starting. Replace it with your own token (or the Edge token from Dockhand) before starting the service.

Example config for Standard Mode:

# Standard mode - listen for connections
PORT=2376
# Required when binding a non-loopback address (the 0.0.0.0 default)
TOKEN=your-secret-token

Example config for Edge Mode:

# Edge mode - connect to Dockhand server
DOCKHAND_SERVER_URL=wss://your-dockhand.example.com/api/hawser/connect
TOKEN=your-agent-token
  1. Start the service:
sudo systemctl enable --now hawser

Upgrading

Run the installer again, then restart the service:

curl -fsSL https://raw.githubusercontent.com/Finsys/hawser/main/scripts/install.sh | bash
sudo systemctl restart hawser      # OpenRC: sudo rc-service hawser restart

The installer replaces the binary and rewrites the systemd unit (or the OpenRC init script), and keeps /etc/hawser/config. Replacing only the binary leaves the old unit in place, so changes to the unit (such as the writable Docker config directory needed for private registry pulls and compose builds) do not take effect. To pin a version, set HAWSER_VERSION, e.g. curl ... | HAWSER_VERSION=0.2.52 bash.

Full Systemd Service File

If you prefer to set up the systemd service manually, here's the complete service file (the same unit the installer writes):

/etc/systemd/system/hawser.service

[Unit]
Description=Hawser - Remote Docker Agent for Dockhand
Documentation=https://github.com/Finsys/hawser
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service

[Service]
Type=simple
ExecStart=/usr/local/bin/hawser
Restart=always
RestartSec=10
EnvironmentFile=/etc/hawser/config

# Security hardening
NoNewPrivileges=false
ProtectSystem=strict
ProtectHome=true
# docker login, compose and buildx need a writable config dir and temp dir, which the
# two lines above take away. /run/hawser is tmpfs: registry logins never reach the disk.
RuntimeDirectory=hawser
Environment=DOCKER_CONFIG=/run/hawser TMPDIR=/run/hawser
ReadWritePaths=/var/run/docker.sock -/data/stacks

[Install]
WantedBy=multi-user.target

ProtectSystem=strict and ProtectHome=true leave the agent no writable Docker config directory and a read-only /tmp, which docker login, docker compose build and buildx all need. RuntimeDirectory=hawser creates /run/hawser on tmpfs, and DOCKER_CONFIG and TMPDIR point there, so registry logins and buildx state stay in memory and are gone after a restart (the agent logs in again before every pull and build). HOME and /tmp are left alone, so ${HOME} in a compose file and files under /tmp resolve as before; ${TMPDIR} and ${DOCKER_CONFIG} in a compose file resolve to /run/hawser. To run without them, remove the two lines with sudo systemctl edit --full hawser; compose builds and private registry pulls then fail. Re-running the installer rewrites the unit.

/etc/hawser/config (Standard Mode example):

# Hawser Configuration
# See https://github.com/Finsys/hawser for documentation

# Standard Mode
PORT=2376

# Required when binding a non-loopback address (the 0.0.0.0 default).
# Hawser refuses to start on a non-loopback bind without a token.
# Alternatively set BIND_ADDRESS=127.0.0.1 for a local-only agent.
TOKEN=your-secret-token

# Docker socket path
DOCKER_SOCKET=/var/run/docker.sock

# Agent identification (optional)
# AGENT_NAME=my-server

# TLS configuration (optional)
# TLS_CERT=/etc/hawser/server.crt
# TLS_KEY=/etc/hawser/server.key

/etc/hawser/config (Edge Mode example):

# Hawser Configuration
# See https://github.com/Finsys/hawser for documentation

# Edge Mode - connect to Dockhand server
DOCKHAND_SERVER_URL=wss://your-dockhand.example.com/api/hawser/connect
TOKEN=your-agent-token

# Docker socket path
DOCKER_SOCKET=/var/run/docker.sock

# Agent identification (optional)
# AGENT_NAME=my-server

# Edge mode only listens for its health check (on PORT, default 2376).
# Restrict it to localhost so the host has no externally-reachable surface:
# BIND_ADDRESS=127.0.0.1

# TLS configuration for self-signed Dockhand (optional)
# CA_CERT=/etc/hawser/dockhand-ca.crt
# TLS_SKIP_VERIFY=false

# Docker API request timeout in seconds (optional, both modes)
# REQUEST_TIMEOUT=30

# Connection settings (optional, Edge mode)
# HEARTBEAT_INTERVAL=30
# COMPOSE_TIMEOUT=900
# RECONNECT_DELAY=1
# MAX_RECONNECT_DELAY=60
# WELCOME_TIMEOUT=30
# MAX_MESSAGE_SIZE_MB=256

Manual installation steps:

# 1. Download binary (release archives carry the version; use arm64, or arm for ARMv7, as needed)
VER=0.2.52
curl -fsSL https://github.com/Finsys/hawser/releases/download/v${VER}/hawser_${VER}_linux_amd64.tar.gz | tar xz hawser
sudo install -m 755 hawser /usr/local/bin/hawser

# 2. Create config and stacks directories
#    The service may only write to the stacks directory named in ReadWritePaths.
#    For a different STACKS_DIR, use that path there (or run the installer with
#    STACKS_DIR=/your/path, which does both).
sudo mkdir -p /etc/hawser /data/stacks

# 3. Create config file (edit with your settings)
sudo tee /etc/hawser/config << 'EOF'
PORT=2376
TOKEN=your-secret-token
DOCKER_SOCKET=/var/run/docker.sock
EOF
# The config can hold a TOKEN, so restrict it to root
sudo chmod 600 /etc/hawser/config
# If you run hawser as a non-root user, also chown it to that user (keep mode 600):
#   sudo chown <user> /etc/hawser/config

# 4. Create systemd service file
sudo tee /etc/systemd/system/hawser.service << 'EOF'
[Unit]
Description=Hawser - Remote Docker Agent for Dockhand
Documentation=https://github.com/Finsys/hawser
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service

[Service]
Type=simple
ExecStart=/usr/local/bin/hawser
Restart=always
RestartSec=10
EnvironmentFile=/etc/hawser/config

# Security hardening
NoNewPrivileges=false
ProtectSystem=strict
ProtectHome=true
# docker login, compose and buildx need a writable config dir and temp dir, which the
# two lines above take away. /run/hawser is tmpfs: registry logins never reach the disk.
RuntimeDirectory=hawser
Environment=DOCKER_CONFIG=/run/hawser TMPDIR=/run/hawser
ReadWritePaths=/var/run/docker.sock -/data/stacks

[Install]
WantedBy=multi-user.target
EOF

# 5. Enable and start the service
sudo systemctl daemon-reload
sudo systemctl enable --now hawser

# 6. Check status
sudo systemctl status hawser
sudo journalctl -u hawser -f

Docker

Important: If your compose stacks use relative file bind mounts (e.g., ./config.conf:/app/config.conf), you must use a host path bind mount for STACKS_DIR — not a named volume. The path inside the container must match the host path, because Docker daemon resolves bind mount sources on the host filesystem.

Example: -v /opt/hawser-stacks:/opt/hawser-stacks -e STACKS_DIR=/opt/hawser-stacks

If your stacks only use named volumes or absolute paths, a named volume (-v hawser_stacks:/data/stacks) works fine.

Read-only containers: with --read-only, docker login and docker compose build have no writable Docker config directory or temp directory, so private registry pulls and image builds fail. Give them a tmpfs: --tmpfs /run/hawser -e DOCKER_CONFIG=/run/hawser -e TMPDIR=/run/hawser.

Standard Mode - Agent listens for connections:

A TOKEN is required because the container publishes the port on a non-loopback address; without one the agent refuses to start (it would expose an unauthenticated Docker API to the network):

docker run -d \
  --name hawser \
  --restart unless-stopped \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /opt/hawser-stacks:/opt/hawser-stacks \
  -e STACKS_DIR=/opt/hawser-stacks \
  -e TOKEN=your-secret-token \
  -p 2376:2376 \
  ghcr.io/finsys/hawser:latest

Standard Mode with TLS (recommended for production):

docker run -d \
  --name hawser \
  --restart unless-stopped \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /opt/hawser-stacks:/opt/hawser-stacks \
  -e STACKS_DIR=/opt/hawser-stacks \
  -v /path/to/certs:/certs:ro \
  -p 2376:2376 \
  -e TLS_CERT=/certs/server.crt \
  -e TLS_KEY=/certs/server.key \
  -e TOKEN=your-secret-token \
  ghcr.io/finsys/hawser:latest

Edge Mode - Agent connects to Dockhand:

docker run -d \
  --name hawser \
  --restart unless-stopped \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /opt/hawser-stacks:/opt/hawser-stacks \
  -e STACKS_DIR=/opt/hawser-stacks \
  -e DOCKHAND_SERVER_URL=wss://your-dockhand.example.com/api/hawser/connect \
  -e TOKEN=your-agent-token \
  ghcr.io/finsys/hawser:latest

Edge Mode with Self-Signed Certificate:

docker run -d \
  --name hawser \
  --restart unless-stopped \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /opt/hawser-stacks:/opt/hawser-stacks \
  -e STACKS_DIR=/opt/hawser-stacks \
  -v /path/to/dockhand-ca.crt:/certs/ca.crt:ro \
  -e DOCKHAND_SERVER_URL=wss://your-dockhand.example.com/api/hawser/connect \
  -e TOKEN=your-agent-token \
  -e CA_CERT=/certs/ca.crt \
  ghcr.io/finsys/hawser:latest

Building Docker image locally

For local development or custom builds, use the multi-stage Dockerfile.dev which builds from source:

# Clone repository
git clone https://github.com/Finsys/hawser.git
cd hawser

# Build from source (recommended for local development)
docker build -f Dockerfile.dev -t hawser:local .

# Run locally built image - Standard mode
docker run -d \
  --name hawser \
  --restart unless-stopped \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /opt/hawser-stacks:/opt/hawser-stacks \
  -e STACKS_DIR=/opt/hawser-stacks \
  -e TOKEN=your-secret-token \
  -p 2376:2376 \
  hawser:local

# Run locally built image - Edge mode
docker run -d \
  --name hawser \
  --restart unless-stopped \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /opt/hawser-stacks:/opt/hawser-stacks \
  -e STACKS_DIR=/opt/hawser-stacks \
  -e DOCKHAND_SERVER_URL=wss://your-dockhand.example.com/api/hawser/connect \
  -e TOKEN=your-agent-token \
  hawser:local

Note: The default Dockerfile is used by GoReleaser for release builds and expects a pre-built binary. Use Dockerfile.dev for building from source.

Multi-architecture builds

The official images on ghcr.io/finsys/hawser are multi-arch (amd64, arm64 and armv7; the armv7 image uses an Alpine base). For local multi-arch builds:

# Create a builder (first time only)
docker buildx create --name mybuilder --use

# Build for multiple platforms
docker buildx build -f Dockerfile.dev \
  --platform linux/amd64,linux/arm64 \
  -t hawser:local \
  --load .

Docker health check

The Hawser Docker image includes a built-in health check that verifies Docker connectivity. This works in both Standard and Edge modes.

How it works:

  • The container runs wget against the /_hawser/health endpoint every 30 seconds
  • Both modes expose a minimal HTTP server on the configured port (default: 2376) for health checks
  • The health check verifies that Hawser can communicate with the Docker daemon

Health check response:

# Standard mode
curl http://localhost:2376/_hawser/health
{"status":"healthy"}

# Edge mode (includes connection status)
curl http://localhost:2376/_hawser/health
{"status":"healthy","mode":"edge","connected":true}

In Edge mode the endpoint answers 200 with "connected":false while the agent is reconnecting. In Standard mode with TLS_CERT/TLS_KEY set, the built-in check uses https. Do not set TLS_CERT in Edge mode: the Edge health endpoint is always plain HTTP (the connection to Dockhand is encrypted by wss://), so the check would fail.

Container status:

# Check container health status
docker inspect --format='{{.State.Health.Status}}' hawser
# healthy

# View health check logs
docker inspect --format='{{json .State.Health}}' hawser | jq

Custom health check (optional):

If you need custom health check settings, you can override the built-in health check:

docker run -d \
  --name hawser \
  --restart unless-stopped \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -e DOCKHAND_SERVER_URL=wss://your-dockhand.example.com/api/hawser/connect \
  -e TOKEN=your-agent-token \
  --health-cmd="wget -q --spider http://localhost:2376/_hawser/health || exit 1" \
  --health-interval=30s \
  --health-timeout=5s \
  --health-retries=3 \
  --health-start-period=10s \
  ghcr.io/finsys/hawser:latest

Configuration

Hawser is configured via environment variables:

Variable Description Default
DOCKHAND_SERVER_URL WebSocket URL for Edge mode -
TOKEN Authentication token. Required in Edge mode, and in Standard mode when binding a non-loopback address. -
TOKEN_FILE Path to a file containing the authentication token (takes precedence over TOKEN, e.g., for Docker Secrets). -
CA_CERT Path to CA certificate for Edge mode (self-signed Dockhand) -
TLS_SKIP_VERIFY Skip TLS verification for Edge mode (insecure) false
PORT HTTP server port (Standard mode) 2376
TLS_CERT Path to TLS certificate (Standard mode server cert) -
TLS_KEY Path to TLS private key (Standard mode server key) -
BIND_ADDRESS Address to bind to (use 127.0.0.1 to restrict to localhost) 0.0.0.0
ALLOW_INSECURE_NO_AUTH Permit Standard mode to bind a non-loopback address with no TOKEN (insecure; only for networks isolated by other means) false
DOCKER_SOCKET Docker socket path First existing of /var/run/docker.sock, ~/.docker/run/docker.sock, ~/.orbstack/run/docker.sock, /run/docker.sock
STACKS_DIR Directory for compose stack files (requires Dockhand 1.0.5+). Use a host path bind mount with matching paths if stacks use relative file bind mounts. /data/stacks
AGENT_ID Unique agent identifier. Set it to keep the same ID across restarts. New UUID on every start
AGENT_NAME Human-readable agent name Hostname
HEARTBEAT_INTERVAL Heartbeat interval in seconds (Edge mode) 30
REQUEST_TIMEOUT Request timeout in seconds (raise for slow storage such as ZFS syncfs, which can make a container recreate exceed 30s) 30
COMPOSE_TIMEOUT Compose operation timeout in seconds (Edge mode; up/down/pull can run far longer than a normal request) 900
RECONNECT_DELAY Initial reconnect delay (Edge mode) 1
MAX_RECONNECT_DELAY Maximum reconnect delay (Edge mode) 60
WELCOME_TIMEOUT Timeout in seconds waiting for welcome after hello (Edge mode) 30
MAX_MESSAGE_SIZE_MB Max inbound WebSocket message size in MiB (Edge mode). Bounds the stack-files payload a git deploy can send; raise for very large repos on a well-resourced agent, lower to cap memory on a small edge host. 256
LOG_LEVEL Logging level: debug, info, warn, error info
SKIP_DF_COLLECTION Skip disk usage collection (see below) -

Mode Detection

Hawser automatically detects the operational mode:

  • If DOCKHAND_SERVER_URL is set → Edge Mode. It must start with ws:// or wss://, and TOKEN (or TOKEN_FILE) is required; without it Hawser refuses to start.
  • Otherwise → Standard Mode

Log Levels

The LOG_LEVEL environment variable controls verbosity:

Level Description
debug All messages including Docker API calls (method, path, status codes)
info Standard operational messages (connections, startup, shutdown)
warn Warnings and errors (warning is accepted too)
error Errors only

Example: Debug mode

# Binary
LOG_LEVEL=debug TOKEN=your-secret-token hawser

# Docker
docker run -d \
  --name hawser \
  --restart unless-stopped \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -p 2376:2376 \
  -e TOKEN=your-secret-token \
  -e LOG_LEVEL=debug \
  ghcr.io/finsys/hawser:latest

Debug mode logs all Docker API requests, which is useful for troubleshooting connectivity issues.

Features

Docker API Proxy

Hawser provides full access to the Docker API:

  • Container management (create, start, stop, remove)
  • Image operations (pull, list, remove)
  • Volume and network management
  • Log streaming
  • Interactive exec sessions

Docker Compose Support

Hawser includes Docker Compose support for stack operations:

  • up - Deploy stack (optionally building images first, with a pull policy or force-recreate)
  • down - Remove stack
  • pull - Pull images
  • build - Build images, optionally pulling newer base images (--pull) or without cache
  • restart, stop, start - Lifecycle operations
  • ps - List services
  • logs - View logs

Host Metrics

In Edge mode, Hawser collects and reports host metrics every 30 seconds (Standard mode does not collect them):

  • CPU usage (total) and core count
  • Memory (total, used, available)
  • Disk usage (Docker data directory)
  • Network I/O statistics
  • Uptime

All of them except disk usage are read from /proc, so they are Linux-only.

Disabling Disk Usage Collection

On some systems, particularly NAS devices (Synology, QNAP, TrueNAS) or hosts with many mounted volumes, the disk usage collection can cause performance issues. The statfs system call used to check disk space can be slow when there are many mounted filesystems or network mounts.

To disable disk usage collection, set the SKIP_DF_COLLECTION environment variable to any non-empty value:

# Binary
SKIP_DF_COLLECTION=1 DOCKHAND_SERVER_URL=wss://your-dockhand.example.com/api/hawser/connect TOKEN=your-agent-token hawser

# Docker
docker run -d \
  --name hawser \
  --restart unless-stopped \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -e DOCKHAND_SERVER_URL=wss://your-dockhand.example.com/api/hawser/connect \
  -e TOKEN=your-agent-token \
  -e SKIP_DF_COLLECTION=1 \
  ghcr.io/finsys/hawser:latest

# Systemd config (/etc/hawser/config)
SKIP_DF_COLLECTION=1

When disabled, disk metrics will show as 0 in Dockhand (displayed as "N/A").

Reliability

  • Auto-reconnect: Edge mode automatically reconnects with exponential backoff and ±25% random jitter to prevent thundering herds when multiple agents reconnect simultaneously (e.g., after a Dockhand restart)
  • Heartbeat: Regular keepalive messages maintain connection health
  • Graceful shutdown: Clean shutdown on SIGTERM/SIGINT

Docker API Version Compatibility

Hawser automatically negotiates the Docker API version with the daemon. When running Docker Compose operations, Hawser sets the DOCKER_API_VERSION environment variable to match the daemon's reported API version. This ensures compatibility when the Docker CLI version differs from the daemon version - for example, when using an older Docker CLI with a newer Docker daemon that requires a higher minimum API version.

API Endpoints

Standard Mode

In Standard mode, Hawser proxies all Docker API endpoints plus the ones below. When TOKEN is set, every request except /_hawser/health must carry it in the X-Hawser-Token header. After 10 failed attempts within a minute, that IP gets 429 (even with the right token) until the minute has passed.

Endpoint Description
/_hawser/health Health check (no auth required)
/_hawser/info Agent information
POST /_hawser/compose Run a compose operation (used by Dockhand)

Health Check

# Standard mode
curl http://localhost:2376/_hawser/health
# {"status":"healthy"}

# Edge mode (includes WebSocket connection status)
curl http://localhost:2376/_hawser/health
# {"status":"healthy","mode":"edge","connected":true}

Security Considerations

  1. Docker Socket Access: Hawser requires access to the Docker socket, which provides full control over Docker. Run with appropriate access controls.

  2. Network Security:

    • Standard mode: Use TLS and/or token authentication
    • Edge mode: Use WSS (TLS-encrypted WebSocket)
  3. Token Security: Tokens are plain-text strings — any printable ASCII works. There are no format restrictions (hex, base64, etc.).

    Recommended: Use Dockhand's built-in token generator (when adding new Environment in Settings → Environments). It generates a cryptographically secure 32-byte base64url token and only shows it once.

    Manual generation (if not using Dockhand's UI):

    # Option 1: openssl (recommended, 32 bytes / 43 characters)
    openssl rand -base64 32
    
    # Option 2: pwgen (20+ characters recommended)
    pwgen -s 32 1
    
    # Option 3: /dev/urandom
    head -c 32 /dev/urandom | base64

    Use at least 24 characters; length and randomness matter more than character set. Dockhand stores Edge tokens hashed with Argon2id, and Standard-mode tokens encrypted, because it has to send them to the agent.

Building from Source

# Clone repository
git clone https://github.com/Finsys/hawser.git
cd hawser

# Build
go build -o hawser ./cmd/hawser

# Run (loopback bind needs no token; use TOKEN=... to bind all interfaces)
BIND_ADDRESS=127.0.0.1 ./hawser

Docker Build

docker build -f Dockerfile.dev -t hawser .

The plain Dockerfile is for release builds and expects a binary pre-built by GoReleaser.

Contributing

Contributions are welcome! Please read the contributing guidelines before submitting a pull request.

License

MIT License - see LICENSE for details.

Related


Made with ❤️ and mass amounts of ☕ by Finsys for Dockhand

About

The agent for Dockhand

Resources

Security policy

Stars

671 stars

Watchers

5 watching

Forks

Releases

Packages

Used by

Contributors

Languages