Remote Docker agent for Dockhand - manage Docker hosts anywhere.
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)
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 hawserHawser 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 hawserStandard 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 hawserEdge Mode:
DOCKHAND_SERVER_URL=wss://your-dockhand.example.com/api/hawser/connect TOKEN=your-token hawserEdge 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 hawserEdge 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- Download and install the binary:
curl -fsSL https://raw.githubusercontent.com/Finsys/hawser/main/scripts/install.sh | bashThe 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.
- Configure the service:
sudo nano /etc/hawser/configThe 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-tokenExample 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- Start the service:
sudo systemctl enable --now hawserRun 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 restartThe 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.
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.targetProtectSystem=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=256Manual 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 -fImportant: If your compose stacks use relative file bind mounts (e.g.,
./config.conf:/app/config.conf), you must use a host path bind mount forSTACKS_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-stacksIf 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 loginanddocker compose buildhave 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:latestStandard 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:latestEdge 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:latestEdge 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:latestFor 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:localNote: The default Dockerfile is used by GoReleaser for release builds and expects a pre-built binary. Use Dockerfile.dev for building from source.
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 .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
wgetagainst the/_hawser/healthendpoint 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 | jqCustom 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:latestHawser 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) | - |
Hawser automatically detects the operational mode:
- If
DOCKHAND_SERVER_URLis set → Edge Mode. It must start withws://orwss://, andTOKEN(orTOKEN_FILE) is required; without it Hawser refuses to start. - Otherwise → Standard Mode
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:latestDebug mode logs all Docker API requests, which is useful for troubleshooting connectivity issues.
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
Hawser includes Docker Compose support for stack operations:
up- Deploy stack (optionally building images first, with a pull policy or force-recreate)down- Remove stackpull- Pull imagesbuild- Build images, optionally pulling newer base images (--pull) or without cacherestart,stop,start- Lifecycle operationsps- List serviceslogs- View logs
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.
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=1When disabled, disk metrics will show as 0 in Dockhand (displayed as "N/A").
- 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
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.
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) |
# 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}-
Docker Socket Access: Hawser requires access to the Docker socket, which provides full control over Docker. Run with appropriate access controls.
-
Network Security:
- Standard mode: Use TLS and/or token authentication
- Edge mode: Use WSS (TLS-encrypted WebSocket)
-
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.
# 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 ./hawserdocker build -f Dockerfile.dev -t hawser .The plain Dockerfile is for release builds and expects a binary pre-built by GoReleaser.
Contributions are welcome! Please read the contributing guidelines before submitting a pull request.
MIT License - see LICENSE for details.
- Dockhand - Modern Docker management application
- Docker Engine API - Docker API documentation
Made with ❤️ and mass amounts of ☕ by Finsys for Dockhand