A fresh container has nothing listening on port 22, and that catches people out the first time they type ssh against one. It feels like a small server. It isn’t.
The Docker daemon blocks outside access to every unpublished port by default, for both IPv4 and IPv6 (Docker documentation, 2026), so a container with no port mapping accepts no ssh connection from outside the Docker host.
Most of the time you don’t need one anyway. docker exec asks Docker Engine for a shell, and it works the same on local and remote hosts. openssh-server only earns its place when a client has to log in over a published port with its own ssh keys.
What does it mean to ssh into a Docker container?
People use the phrase loosely. Sometimes they mean docker exec, where the Docker CLI asks Docker Engine to start a shell inside a running container. Other times they mean a real ssh login, handled by an sshd process running in the container itself.
A default container has no sshd and no listener on port 22, so a plain ssh command against it fails. It runs one foreground process, set by its entrypoint, and nothing else.
A Docker container is an isolated process with its own filesystem, not a virtual machine, so nothing in it expects a remote login unless the image adds one.
In practice you end up choosing among these routes:
- docker exec starts a new process, usually a shell, inside a running container through Docker Engine.
- docker attach hooks your terminal to the container’s main process.
- An sshd process inside the container gives you a real ssh login on a published port, served by OpenSSH.
- docker context over ssh points the local Docker CLI at a remote Docker Engine through an ssh connection.
Only the sshd route changes the image. Everything else works on containers exactly as they already run.
Which method should you use to access a container shell?
docker exec for local debugging and most daily access. sshd only when a client must log in over the network with its own ssh keys. And if the container lives on a remote Docker host, docker context over ssh.
| Method | Setup effort | Image changes | Best fit |
|---|---|---|---|
| docker exec | None | None | Local debugging |
| docker attach | None | None | Watching the main process |
| sshd in the container | High | openssh-server, keys, port | Network login from any ssh client |
| docker context over ssh | Medium | None | Remote Docker host |
A quick cheat sheet for Docker flags covers the CLI-based routes.
docker exec needs no image change and no open port, though the Docker CLI has to be on the host. docker attach gives you a live view of the main process output, and the container stops when that process exits, which is an easy way to ruin your afternoon.
sshd lets you log in from any ssh client without the Docker CLI. In return you take on a second process, key management, and a bigger attack surface. docker context over ssh gives you one CLI for local and remote engines, but you need ssh access to the Docker host itself.
sshd is not the default pick.
Adding it puts a second process in a container built to run one, which complicates container logs, restarts, and image updates.
Where the sources disagree
Jerome Petazzoni’s 2014 post argues against running an SSH server in containers, citing key management, security upgrades across every container, and the need for a process manager. A Dash0 guide from June 2026 reaches the same verdict and points to docker exec first.
Phusion’s baseimage-docker ships an SSH server anyway, disabled by default, and offers it as an alternative because docker exec comes with caveats.
The gap comes from scope. baseimage-docker already runs runit as a process supervisor, so the cost Petazzoni describes is already paid.
What do you need before you connect to a container?
Docker Engine has to be installed and its daemon running. The walkthrough on installing Docker covers each platform.
On Windows and macOS, Docker Desktop supplies Docker Engine and the docker CLI, and the same syntax applies.
If commands hang or error, check whether Docker is running before debugging the container.
Beyond that, you want the following on hand:
- A container in running status. docker ps lists it with status Up, and a paused container fails with an error telling you to unpause it before exec.
- The container id or container name. docker ps shows the 12-character short id and the name, and either one goes into every later command.
- An ssh client, if you’re taking the sshd route. That means the ssh binary from OpenSSH, on the host or on another machine.
- A quick idea of the base image, since Ubuntu and Debian use apt-get while Alpine Linux uses apk. It also decides which shell exists.
A shell opened with docker exec lasts only while the container’s primary process (PID 1) runs, and it does not return after a restart (Docker documentation, 2026).
A container that exits immediately after docker run has no main process left to exec into, so read its output with docker logs.
Starting a test container with docker run -d -it alpine sh keeps it alive, because -i holds stdin open for the sh process.
How do you open a shell with docker exec?

Run docker exec -it container_name sh (or bash). The -i flag keeps stdin open, the -t flag handles tty allocation, and typing exit closes the shell without stopping the container.
- Run docker ps and copy the container id or container name.
- Run docker exec -it container\_name bash, and switch to sh when the image has no bash.
- Work at the prompt like any other terminal.
- Type exit to leave. The container keeps running.
A few flags come up constantly. -u runs the shell as a chosen user or UID. -w sets the working directory (Docker Engine API 1.35 and later), and -e sets environment variables for that process only (API 1.25 and later). -d runs the command in the background.
The details of leaving cleanly are in this guide on exiting a Docker container.
One gotcha trips up almost everyone. This works:
docker exec -it my_container sh -c "echo a && echo b"And this fails:
docker exec -it my_container "echo a && echo b"The command must be an executable, so chained or quoted strings go through sh -c (Docker documentation, 2026).
How does docker compose exec work for Compose services?
Instead of a container name, docker compose exec takes a service name. Running docker compose exec web sh opens a shell in the web service.
Services defined in Docker Compose keep stable names across recreation, which makes them safer targets than generated container names.
- TTY and interactive mode are on by default.
- Add -T to disable the TTY in scripts and CI.
- –index picks one replica when a service runs several.
Plain docker exec needs –interactive –tty to match that default (Docker documentation, 2026).
Why is docker attach a different command?
docker attach doesn’t start anything new. It connects your terminal to the container’s main process, so you share that process with whatever else is watching it.
| Behavior | docker exec | docker attach |
|---|---|---|
| Target | New process | Main process (PID 1) |
| Leaving | exit, container keeps running | Ctrl-p Ctrl-q, needs -i and -t |
| Extra sessions | Independent processes | Same process, shared streams |
While a client is attached, Docker holds a buffer of about 1 MB for the stdio stream, and a full buffer slows the output process (Docker documentation, 2026). Chatty processes belong in docker logs instead.
Type exit in an attached shell that is PID 1, and the container stops with that exit code. Without -i and -t, Ctrl-c ends the container.
Which shell does the container have: bash, sh, or ash?
Ubuntu and Debian images ship bash. Alpine Linux images come with the BusyBox sh (ash) and no bash at all. Try bash first and fall back to sh. Distroless and scratch-based images often have no shell to find.
The official alpine image is described as 5 MB in size, built on musl libc and BusyBox (Docker Hub, 2026). In Docker’s own usage example, a MySQL client image weighs 36.8 MB on Alpine against about 145 MB on Ubuntu (Docker Hub).
On an Ubuntu image, bash is present, so docker exec -it name bash works. Debian behaves the same way.
Alpine is different. /bin/sh points to ash, so docker exec -it name sh works, and bash only arrives after apk add bash.
A wrong shell path returns an error like this: OCI runtime exec failed, exec: “bash”: executable file not found in $PATH.
Run docker exec name cat /etc/os-release to read the distribution and pick the shell. Minimal images with no shell fail the same way for every path.
Once the bash shell opens, a cheat sheet for Bash covers history, job control, and redirection.
How do you run sshd inside a Docker container?
Install openssh-server in the Dockerfile, create the privilege separation directory, set the sshd\_config directives, and start sshd in the foreground as the main process. Then publish port 22 with a port mapping at docker run.
- Start the Dockerfile from an Ubuntu, Debian, or Alpine Linux base.
- Install the server with apt-get install -y openssh-server on Debian and Ubuntu, or apk add openssh on Alpine.
- Create the privilege separation directory with mkdir -p /run/sshd.
- Generate host keys with ssh-keygen -A, or leave that to the entrypoint script.
- Set PubkeyAuthentication yes and PasswordAuthentication no in sshd\_config.
- Add EXPOSE 22 and end the file with CMD [“/usr/sbin/sshd”, “-D”, “-e”].
- Build the image, then run docker run -d -p 2222:22 –name sshbox image\_name.
EXPOSE 22 documents the port and publishes nothing. The published port comes from -p 2222:22 at docker run.
sshd -D holds the foreground, so Docker sees a live main process. Leave it off and sshd forks into the background, the entrypoint returns, and the container exits immediately.
Host keys need a decision too. ssh-keygen -A at build time bakes identical host keys into every container from that image, so generate them at startup when each container needs its own identity.
Petazzoni (2014) notes that sshd next to an application needs a process manager such as Supervisor or Monit, because Docker watches one process. supervisord starts and restarts several processes, and the price is a container that runs a process tree instead of a single process.
The Dockerfile bakes these steps into a Docker image, so every container started from it carries the same sshd and the same host keys.
How do you set up key-based authentication for the container?
Generate a key pair with ssh-keygen on the client, place the public key in the container’s authorized\_keys file, and set PasswordAuthentication no in sshd\_config. The private key stays on the client, and sshd accepts only holders of a matching key.
On the client, run ssh-keygen -t ed25519 to create the private key and the public key (the .pub file). Load the private key into ssh-agent with ssh-add so the passphrase gets typed once per session. When port 22 is published on 2222, connect with ssh -p 2222 dev@localhost.
Inside the container, the login user’s home needs a .ssh directory set to 700, with the public key appended to authorized\_keys and that file set to 600. Both must belong to the login user, because the default StrictModes check rejects keys with loose permissions or wrong ownership.
To add a key to a running container without a rebuild, pipe the .pub file through docker exec -i.
These sshd\_config lines do most of the hardening:
PasswordAuthentication no
PermitRootLogin no
PubkeyAuthentication yesBlocking root login forces attackers to guess a username as well as a key.
Baking a password into the image is the trap. A chpasswd line in the Dockerfile stays readable in image layers through docker history.
Petazzoni (2014) recommends storing credentials in a volume the container cannot write to, so a compromised container cannot corrupt them. Phusion’s baseimage-docker follows the key-only pattern and disables password and challenge-response authentication by default.
Rebuild a container with fresh host keys and the client greets you with “REMOTE HOST IDENTIFICATION HAS CHANGED”. Persist the host keys in a volume to keep the fingerprint stable.
How do you publish port 22 and connect from another machine?
Map a host port to container port 22 at docker run, connect to the Docker host’s address on that port, and bind the mapping to a specific IP when not every network needs access. A container on a remote Docker host needs docker context over ssh instead.
A port mapping without a host IP binds to every host address, so the container’s sshd is reachable from outside the host. Docker’s documentation calls published ports insecure by default for that reason.
Adding 127.0.0.1 to the mapping, as in -p 127.0.0.1:2222:22, restricts access to the Docker host itself. Remote clients then connect with ssh -p 2222 dev@host\_address on an unrestricted mapping.
| Route | Address | Reach | Limit |
|---|---|---|---|
| Published port | Host address plus mapped port | Any machine that reaches the host | Open to all interfaces unless bound |
| Bridge network IP | Container ip address from docker inspect | Linux Docker host only | Unreachable on Docker Desktop |
| docker context over ssh | ssh://user@host | Remote Docker host | Needs ssh access to the host, not the container |
Docker Desktop runs containers inside a virtual machine, so the bridge network stays invisible to the host. The Docker Desktop documentation states that it cannot route traffic to Linux containers, which rules out ssh to a container IP on Windows and macOS.
Publishing also creates a firewall rule on the host that maps the host port to the container port (Docker documentation, 2026). Restrict source addresses in the host firewall or the cloud security group.
A few figures worth keeping in mind:
- A mapping that names no host IP binds to 0.0.0.0 and [::] (Docker documentation, 2026).
- Before Docker Engine 28.0.0, hosts on the same L2 segment can reach ports published to localhost (Docker documentation, 2026).
- Docker CLI support for ssh endpoints starts with Docker 18.09 (Docker CLI source code).
How does docker context use ssh to reach a remote engine?
docker context over ssh points the local Docker CLI at a remote Docker Engine, so every docker command, exec included, runs on that host. The container needs no sshd and no published port.
- Create the context with docker context create –docker host=ssh://docker-user@host1.example.com my-remote-engine.
- Switch to it with docker context use my-remote-engine.
- Run docker exec -it container\_name sh, and the shell opens on the remote engine.
- Go back with docker context use default.
The ssh user needs permission to reach the Docker socket on the remote machine (Docker documentation, 2026). Setting DOCKER\_HOST=ssh://docker-user@host1.example.com gives a temporary connection without a stored context.
Traefik, a reverse proxy, uses the same ssh endpoint to reach its Docker provider on Docker 18.09 and later (Traefik documentation).
A few lines in ~/.ssh/config (ControlMaster auto, a ControlPath, and ControlPersist yes) reuse one ssh connection across repeated docker commands.
How do you fix connection refused, permission denied, and other errors?
Match the error to its layer. Connection refused means nothing listens on the port, permission denied (publickey) means sshd rejected the key, and container is not running means Docker Engine has no live process to exec into.
| Error | Layer | First check | Fix |
|---|---|---|---|
| Connection refused | Port or sshd | docker port container\_name 22 | Publish port 22 or start sshd |
| Connection timed out | Address or firewall | Host IP and mapped port | Correct the address, open the host port |
| Permission denied (publickey) | Authentication | ssh -v user@host | Fix key, user, or sshd\_config |
| Container is not running | Docker Engine | docker ps -a | docker start, then read the logs |
Refused means the host answered and no process listened. Timed out means the packets never arrived, which points at the address or a firewall.
Connection refused
Usually the entrypoint runs a different process, so sshd never started. Or the port mapping targets the wrong container port, and docker port prints nothing for 22. The third possibility is that sshd exited at startup, in which case docker logs shows “Missing privilege separation directory” or “no hostkeys available”.
Permission denied (publickey)
Start with ssh -v, which lists every key the client offers and shows which one the server refused. An empty ssh-agent offers nothing at all, and ssh-add -l shows what is loaded.
Also check whether the public key sits in root’s authorized\_keys while sshd\_config sets PermitRootLogin no, or whether the client uses a different username than the key’s owner.
Container is not running
docker exec fails with “Container is not running”. docker ps -a shows the Exited status with its exit code, and docker logs prints the last output before the stop.
Executable file not found
The error names the path Docker tried. An absolute path such as /bin/sh fixes a path problem, and it does not add a shell the image lacks.
Work from the container outward. Logs first, port mapping second, client verbosity last.
When should you not run sshd in a container?
Skip it for application images that run in production, for images with no shell, and anywhere the Docker CLI already reaches the container. sshd adds a listener, a credential store, and a security finding under the CIS Docker Benchmark.
- An application image in production runs one process for one purpose, and an extra listener works against that.
- sshd needs a login shell, and distroless or scratch images have none.
- On a host with Docker CLI access, docker exec reaches the same shell with no port.
- Audited hosts get flagged, because a benchmark scan reports ssh inside containers.
A container in a production environment should carry the smallest attack surface the application allows.
The CIS Docker Benchmark lists “Ensure ssh is not run within containers” as recommendation 5.6. Docker Bench for Security, Docker’s own audit script, reports the same item as check 5.6, ssh is not run within containers.
| Attribute | sshd in the container | docker exec |
|---|---|---|
| Network listener | Yes, a published port | None |
| Credentials | Keys stored per container | Docker socket permission on the host |
| Access control point | sshd\_config | Docker Engine |
Alternatives when exec is not enough
nsenter runs a program in the Linux namespaces of a target process (nsenter manual, 2026). Take the container PID from docker inspect, then run nsenter -t PID with the network and PID flags to keep the host’s tools available in an image with no shell.
docker debug opens a shell in any container or image, even one with no shell, without modifying the image, and its –host option accepts ssh:// endpoints (Docker documentation, 2026). Docker’s CLI reference no longer labels it Beta, but Docker’s pricing page is inconsistent about which plans include it, so check your plan before relying on it.
sshd fits when the container is the SSH service itself, such as a bastion, an SFTP endpoint, or a shared sandbox. Everywhere else, docker exec is the default and sshd is the exception.
FAQ on How To Ssh Into Docker Container
How do you exit a container without stopping it?
Type exit in a docker exec shell and the container keeps running. In a docker attach session, press Ctrl-p Ctrl-q to detach, or change the sequence with –detach-keys when it clashes with another tool.
How do you ssh into a Docker container on Windows or macOS?
Docker Desktop keeps the bridge network inside its virtual machine, so ssh to a container IP fails. Publish the port with -p 2222:22 and run ssh -p 2222 dev@localhost, while docker exec -it works unchanged from PowerShell, Terminal, or WSL.
How do you copy an ssh key into a running container?
Mount authorized\_keys read-only at docker run with -v, so the container cannot rewrite its own credentials. For a container already running, docker cp places the file, then docker exec sets ownership and mode 600 for the login user.
How do you open a shell in a Kubernetes pod or a Podman container?
kubectl exec -it pod\_name — sh opens a shell in a pod, and -c container\_name selects one container in a multi-container pod. podman exec -it container\_name sh mirrors docker exec, and a cheat sheet for kubectl lists the remaining flags.
How do you attach VS Code to a running container?
The Visual Studio Code Dev Containers extension adds Attach to Running Container to the command palette. Pick the container from the list, and the editor opens a window inside it with no sshd installed.
How do you run sshd next to the main process with supervisord?
Make supervisord the main process, with one program section for the application and one for sshd -D, and set nodaemon=true so it stays in the foreground. A wrapper entrypoint script that backgrounds sshd handles both processes as well, with no restart handling.
What Should You Try First When You SSH Into a Docker Container?
Start with docker exec. Move to docker context over ssh for remote hosts, and reach for sshd inside the image last, because each step adds a listener or a credential store the previous step avoids.
Docker’s documentation (2026) binds published ports to all host addresses by default, and Docker Desktop cannot route traffic to a bridge network container IP. On Windows and macOS, that leaves one route to an sshd inside the image, a published port, ideally bound to 127.0.0.1.
This order holds unless your Docker plan includes docker debug, which can reach images that have no shell (check Docker’s pricing page for current plan coverage; position verified September 2026).
A container that has stopped needs the walkthrough on restarting a Docker container before any of these routes work.
- MySQL Cheat Sheet - September 30, 2026
- How to Remove Duplicate Lines in Notepad++ (Sorted or Unsorted) - September 29, 2026
- Should Your Engineering Team Still Own the Marketing Website? - September 29, 2026



