Docker Over SSH vs TLS: Which Is Safer?
The Docker SSH vs TLS question shows up the moment you need to reach a daemon on a remote server. For most people, SSH is the safer default. It reuses the authentication, firewall rules, and audit logging you already trust for shell access, and it exposes no new network port. TLS is the right call when a machine or orchestrator needs to talk to the Docker daemon without a human in the loop, but it puts a root-equivalent API on the network and you own every certificate.
The wrong answer, and the one that keeps showing up in breach reports, is exposing the Docker daemon on plain TCP with no encryption at all. We'll get to why that's so dangerous.
How the two connections actually work
The Docker CLI on your laptop talks to a daemon (dockerd) somewhere. Locally that happens over a Unix socket at /var/run/docker.sock. Remotely you need a transport, and SSH and TLS are the two sane ones.
SSH tunnels the Docker API over an ordinary SSH connection. The CLI opens an SSH session to the host, runs the remote docker binary, and pipes the API through. No Docker port is opened on the network. Authentication, encryption, and access control are whatever your sshd already enforces.
TLS makes dockerd itself listen on a TCP port (2376 by convention) and speak HTTPS with mutual certificate authentication. The client proves its identity with a client certificate, the daemon proves its identity with a server certificate, and both are signed by a CA you create. This is mutual TLS (mTLS), not the one-sided TLS a browser uses.
The threat model that matters
Either transport, done right, encrypts traffic and authenticates the client. The real security difference is what an attacker gets when something slips.
Anyone who can talk to the Docker daemon can run a container that mounts the host filesystem and hands themselves root:
docker run -v /:/host -it alpine chroot /host sh
That's not a bug. It's what the Docker API is for. So the real question is practical: how hard is it to reach the daemon, and how well do you know who did?
Setting up Docker over SSH
This is the least amount of new surface. If you can already ssh user@host, you're most of the way there. The remote user needs to be able to run docker (usually by being in the docker group, which is root-equivalent, so treat that account accordingly).
Create a context and point it at the host:
docker context create prod --docker "host=ssh://deploy@10.0.0.5"
docker context use prod
docker ps
Or set it for a single command without switching contexts:
DOCKER_HOST=ssh://deploy@10.0.0.5 docker ps
A few things make this smooth and safer. Use key authentication, not passwords (here's the case for SSH keys over passwords). Turn on connection multiplexing in ~/.ssh/config so every docker command doesn't pay the full handshake cost:
Host prod
HostName 10.0.0.5
User deploy
ControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 10m
What you get for free: sshd already logs every login, your firewall already governs port 22, fail2ban or similar is probably already watching it, and you can revoke a key in one place. No certificate infrastructure to build or rotate. For the full walkthrough, see our guide to remote Docker management over SSH.
The one real cost: SSH needs the docker CLI present and a shell on the remote host. That's fine for servers, awkward for a bare orchestrator node that has no interactive users by design.
Setting up Docker over TLS
TLS is more work, and the work is the point. You generate a CA, a server certificate for the daemon, and a client certificate for each machine allowed to connect. Docker's documentation has the full openssl sequence; the shape of it is:
# CA
openssl genrsa -aes256 -out ca-key.pem 4096
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
# server cert, signed by the CA, with the host's DNS name / IP in the SAN
# client cert, signed by the same CA, with extendedKeyUsage=clientAuth
Then start the daemon with TLS verification on. In /etc/docker/daemon.json:
{
"tls": true,
"tlsverify": true,
"tlscacert": "/etc/docker/ca.pem",
"tlscert": "/etc/docker/server-cert.pem",
"tlskey": "/etc/docker/server-key.pem",
"hosts": ["tcp://0.0.0.0:2376", "fd://"]
}
tlsverify: true is the load-bearing line. Without it the daemon still encrypts but accepts any client. With it, only certificates signed by your CA get in.
The client connects like this:
docker --tlsverify \
--tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem \
-H tcp://10.0.0.5:2376 ps
Or via environment variables:
export DOCKER_HOST=tcp://10.0.0.5:2376
export DOCKER_TLS_VERIFY=1
export DOCKER_CERT_PATH=~/.docker/certs/prod
docker ps
TLS wins on a couple of things. There's no interactive shell involved, so it fits headless CI and automation cleanly. And it's a persistent HTTPS connection, so for chatty tooling it can feel snappier than spawning SSH sessions.
But you now own a PKI. Certificates expire and clients break at the worst time. There's no built-in revocation list in the common setup, so pulling access from one client means re-issuing certs. And you've opened a port whose whole job is to accept root-equivalent commands. If it ever ends up reachable from the internet, you have a very bad day.
Docker SSH vs TLS, side by side
| SSH | TLS (mTLS) | |
|---|---|---|
| New network port | None (reuses 22) | 2376 must be open |
| Auth | Your existing SSH keys | Per-client certificates you issue |
| Revocation | Remove a key | Re-issue / rotate certs |
| Audit logging | Built into sshd | You add it |
| Needs remote CLI + shell | Yes | No |
| Best for | Humans, day-to-day ops | CI, automation, orchestrators |
| Setup effort | Minutes | An afternoon plus ongoing rotation |
Never do this
The daemon can also listen on tcp://0.0.0.0:2375 with no TLS. Some tutorials and Docker Desktop toggles still make this a one-click option. Do not.
An open 2375 is an unauthenticated root shell on the internet. Scanners find these in minutes, and the standard move is to launch a cryptominer or mount the host and drop a key. If you see this in a config, treat it as an incident, not a preference:
{ "hosts": ["tcp://0.0.0.0:2375"] } // remove this
If you only remember one thing from this article, remember that unencrypted 2375 is the actual danger, and both SSH and TLS exist to keep you off it.
Doing this from your phone
When an alert fires at 3am, you're not going to generate a client certificate on your phone to check why a container restarted. This is where the transport choice gets practical. SSH-based access travels well because the key already lives on your device. Docker HQ connects to your hosts over SSH so you can list containers, tail logs, open a shell, and watch host CPU and memory from your phone, then get a push alert the moment a host or container goes down. If you're weighing mobile access, our piece on how to SSH into a server from your phone covers the key handling. Pricing and team RBAC details are on the plans page.
Frequently asked questions
Is Docker over SSH slower than TLS?
A little, per command, because SSH sets up a session each time. Enable ControlMaster multiplexing in your SSH config and the difference mostly disappears for interactive use. For high-frequency automated calls, TLS's persistent connection has a real edge.
Do I need TLS if I'm already using SSH?
No. They're two ways to solve the same problem, and running both just doubles your attack surface. Pick SSH for human operators and TLS for headless automation that can't use SSH cleanly. Most small teams never need TLS at all.
What port does each use?
SSH uses your existing SSH port, normally 22, and opens no Docker-specific port. TLS makes the daemon listen on 2376 by convention. Plain, unencrypted Docker uses 2375, which you should never expose.
Is exposing the Docker socket over the network ever safe?
Not directly. The socket and the daemon API grant root-equivalent control of the host. If a container genuinely needs Docker access, prefer a scoped proxy like docker-socket-proxy that whitelists specific API calls, rather than handing over the raw socket or the open TCP port.
Start by checking what your daemon is actually listening on right now. Run sudo ss -tlnp | grep dockerd on the host, and if you see 2375 anywhere, close it before you do anything else.