Overview
Dockhand supports TLS (Transport Layer Security) for secure connections to remote Docker daemons. This is essential when managing Docker hosts over the network.
TLS configuration is per-environment. Each Docker environment can have its own TLS certificates.
When to Use TLS
Use TLS when connecting to Docker over TCP:
Remote Docker hosts - Docker daemon on different server
Docker over network - Non-local socket connections
Production deployments - Secure communication required
Internet-exposed Docker API - Public Docker endpoints
Don’t need TLS for :
Local Unix socket (/var/run/docker.sock)
Docker Desktop
Same-host containers
Environment Configuration
Configure TLS per-environment in the Dockhand UI:
Go to Settings > Environments
Create or edit an environment
Set Connection Type to “Direct (TCP)”
Set Protocol to https
Enter Host and Port (default: 2376)
Provide TLS certificates:
CA Certificate (ca.pem)
Client Certificate (cert.pem)
Client Key (key.pem)
Optionally enable Skip Verify (not recommended)
Environment Schema
From schema/index.ts:23-32:
export const environments = sqliteTable ( 'environments' , {
id: integer ( 'id' ). primaryKey ({ autoIncrement: true }),
name: text ( 'name' ). notNull (). unique (),
host: text ( 'host' ),
port: integer ( 'port' ). default ( 2375 ),
protocol: text ( 'protocol' ). default ( 'http' ),
tlsCa: text ( 'tls_ca' ), // CA certificate (PEM)
tlsCert: text ( 'tls_cert' ), // Client certificate (PEM)
tlsKey: text ( 'tls_key' ), // Client private key (PEM - encrypted)
tlsSkipVerify: integer ( 'tls_skip_verify' , { mode: 'boolean' }). default ( false ),
// ...
});
Certificate Requirements
CA Certificate (ca.pem)
Root certificate authority that signed the server’s certificate.
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKl...
...
-----END CERTIFICATE-----
Client Certificate (cert.pem)
Client authentication certificate.
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKl...
...
-----END CERTIFICATE-----
Client Key (key.pem)
Private key for client certificate.
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAr8zSLI...
...
-----END RSA PRIVATE KEY-----
The client private key is encrypted and stored securely in the database. Never share your private keys.
Generating TLS Certificates
Using Docker’s Script
Docker provides a script to generate TLS certificates:
# Clone Docker's cert generation script
curl -fsSL https://raw.githubusercontent.com/docker/docker/master/contrib/dind/generate-tls.sh -o generate-tls.sh
chmod +x generate-tls.sh
# Generate certificates
./generate-tls.sh
Generates:
ca.pem - CA certificate
cert.pem - Client certificate
key.pem - Client private key
server-cert.pem - Server certificate
server-key.pem - Server private key
Using OpenSSL
Manual certificate generation:
# 1. Generate CA
openssl genrsa -aes256 -out ca-key.pem 4096
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
# 2. Generate server key and certificate
openssl genrsa -out server-key.pem 4096
openssl req -subj "/CN=docker-host" -sha256 -new -key server-key.pem -out server.csr
# Add SANs
echo "subjectAltName = DNS:docker-host,IP:192.168.1.100,IP:127.0.0.1" >> extfile.cnf
echo "extendedKeyUsage = serverAuth" >> extfile.cnf
openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem \
-CAcreateserial -out server-cert.pem -extfile extfile.cnf
# 3. Generate client key and certificate
openssl genrsa -out key.pem 4096
openssl req -subj '/CN=client' -new -key key.pem -out client.csr
echo "extendedKeyUsage = clientAuth" > extfile-client.cnf
openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem \
-CAcreateserial -out cert.pem -extfile extfile-client.cnf
# 4. Set permissions
chmod 0400 ca-key.pem key.pem server-key.pem
chmod 0444 ca.pem server-cert.pem cert.pem
Configuring Docker Daemon
Enable TLS on the Docker daemon:
Using daemon.json
{
"hosts" : [ "unix:///var/run/docker.sock" , "tcp://0.0.0.0:2376" ],
"tls" : true ,
"tlsverify" : true ,
"tlscacert" : "/etc/docker/certs/ca.pem" ,
"tlscert" : "/etc/docker/certs/server-cert.pem" ,
"tlskey" : "/etc/docker/certs/server-key.pem"
}
Using systemd
Edit Docker service:
sudo systemctl edit docker.service
Add:
[Service]
ExecStart =
ExecStart =/usr/bin/dockerd \
--tlsverify \
-- tlscacert =/etc/docker/certs/ca.pem \
-- tlscert =/etc/docker/certs/server-cert.pem \
-- tlskey =/etc/docker/certs/server-key.pem \
- H =unix:///var/run/docker.sock \
- H =tcp://0.0.0.0:2376
Reload and restart:
sudo systemctl daemon-reload
sudo systemctl restart docker
Stack Deployment with TLS
When deploying Docker Compose stacks to TLS-enabled environments, Dockhand:
Creates temporary directory for certificates
Writes PEM files: ca.pem, cert.pem, key.pem
Sets environment variables:
DOCKER_TLS=1
DOCKER_CERT_PATH=/path/to/certs
DOCKER_TLS_VERIFY=1 (or 0 if skip verify)
Runs docker compose command
Cleans up temporary certificates
From stacks.ts:1026-1059:
// Handle TLS certificates for remote Docker connections
let tlsCertDir : string | undefined ;
if ( tlsConfig ) {
// Create temp directory for TLS certs in DATA_DIR
const dataDir = resolve ( process . env . DATA_DIR || './data' );
tlsCertDir = join ( dataDir , 'tmp' , `tls- ${ stackName } - ${ Date . now () } ` );
mkdirSync ( tlsCertDir , { recursive: true });
// Track for cleanup
activeTlsDirs . add ( tlsCertDir );
// Write certificate files
const { ca , cert , key } = tlsConfig ;
if ( ca ) {
const cleanedCa = cleanPem ( ca );
if ( cleanedCa ) writeFileSync ( join ( tlsCertDir , 'ca.pem' ), cleanedCa );
}
if ( cert ) {
const cleanedCert = cleanPem ( cert );
if ( cleanedCert ) writeFileSync ( join ( tlsCertDir , 'cert.pem' ), cleanedCert );
}
if ( key ) {
const cleanedKey = cleanPem ( key );
if ( cleanedKey ) writeFileSync ( join ( tlsCertDir , 'key.pem' ), cleanedKey );
}
// Set Docker TLS environment variables
spawnEnv . DOCKER_TLS = '1' ;
spawnEnv . DOCKER_CERT_PATH = tlsCertDir ;
spawnEnv . DOCKER_TLS_VERIFY = tlsConfig . skipVerify ? '0' : '1' ;
}
Skip TLS Verification
You can disable certificate verification:
environments :
- name : "Dev Docker"
protocol : https
tlsSkipVerify : true
Sets DOCKER_TLS_VERIFY=0.
Not recommended for production. Skip verify disables certificate validation, allowing man-in-the-middle attacks.
Use only for:
Self-signed certificates in development
Testing TLS configuration
Internal networks with trusted certificates
Certificate Encryption
Dockhand encrypts sensitive TLS data before storing in database:
TLS Private Key (tlsKey) - Always encrypted
TLS Certificate (tlsCert) - Stored as plain text (public data)
CA Certificate (tlsCa) - Stored as plain text (public data)
From encryption.ts:362-363 and 505-506:
// Check if tlsKey is encrypted
if ( env . tlsKey && isEncrypted ( env . tlsKey )) {
allEncrypted . push ({ table: 'environments' , id: env . id , field: 'tlsKey' , value: env . tlsKey });
}
// Encrypt tlsKey if not encrypted
if ( env . tlsKey && ! isEncrypted ( env . tlsKey )) {
updates . tlsKey = encrypt ( env . tlsKey );
}
Encryption key: See Environment Variables - ENCRYPTION_KEY
Testing TLS Connection
Test TLS connection from command line:
# Using curl
curl --cacert ca.pem --cert cert.pem --key key.pem https://docker-host:2376/version
# Using Docker CLI
export DOCKER_HOST = tcp :// docker-host : 2376
export DOCKER_TLS_VERIFY = 1
export DOCKER_CERT_PATH = / path / to / certs
docker version
docker ps
Test in Dockhand:
Add environment with TLS configuration
Click Test Connection
Check for success or error messages
Troubleshooting
Certificate Verification Failed
Error : unable to verify the first certificate
Solutions :
Ensure CA certificate matches server certificate’s CA
Verify certificate chain is complete
Check certificate hasn’t expired: openssl x509 -in cert.pem -noout -dates
Invalid Certificate
Error : certificate is valid for X, not Y
Solution :
Regenerate server certificate with correct hostname/IP in SANs
Add DNS:hostname and IP:x.x.x.x to certificate’s Subject Alternative Names
Connection Refused
Error : connect ECONNREFUSED
Solutions :
Check Docker daemon is listening on TCP: netstat -tlnp | grep 2376
Verify firewall allows port 2376
Test with telnet: telnet docker-host 2376
Wrong Protocol
Error : write EPROTO
Solutions :
Ensure protocol is set to https (not http)
Check Docker daemon has TLS enabled
Verify port 2376 (TLS) not 2375 (unencrypted)
Error : error:0909006C:PEM routines
Solutions :
Verify PEM files have correct headers/footers:
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
Check no extra whitespace or newlines
Ensure proper Base64 encoding
Dockhand includes PEM cleaning utility (utils/pem.ts:3):
/**
* TLS implementations are strict about PEM format - they fail when certificates have
* extra whitespace, CRLF line endings, or missing newlines
*/
Best Practices
Strong Keys Use 4096-bit RSA keys for better security.
Certificate Expiry Set expiry dates and rotate certificates before expiration.
Verify Enabled Always enable verification in production (don’t skip verify).
Firewall Rules Restrict port 2376 to trusted IPs only.
Security Considerations
Protect Private Keys
Never commit private keys to Git
Use .gitignore for *.pem, *.key files
Restrict file permissions: chmod 400 key.pem
Rotate keys periodically
Network Security
Use firewall to limit access to port 2376
Consider VPN for Docker access over internet
Use Docker contexts for multiple environments
Dockhand Storage
Private keys are encrypted in database
Back up ENCRYPTION_KEY securely
Use PostgreSQL with SSL for database connection
Enable authentication in Dockhand (LDAP/OIDC)
Docker Context Alternative
Docker contexts provide an alternative to TLS configuration:
# Create context
docker context create remote-docker \
--docker "host=tcp://docker-host:2376,ca=/path/to/ca.pem,cert=/path/to/cert.pem,key=/path/to/key.pem"
# Use context
docker context use remote-docker
docker ps
Dockhand doesn’t use Docker contexts directly but uses the same TLS certificate files.