Remote Docker: Contexts & SSH
Once your app runs on a VPS, you will often want to check its containers, read logs or run a quick docker compose ps. Logging in with SSH every time works, but there is a neater way: the docker command on your laptop can talk to the Docker daemon on the server directly, over the same SSH connection you already trust. Docker calls each daemon you can talk to a context.
This lesson shows how to create and switch contexts, run Compose against a remote server, avoid the path trap that catches almost everyone, and why you must never "solve" remote access by opening Docker's TCP port.
What a context is
The docker program you type is only a client. It sends every command to a daemon (dockerd) through an endpoint: usually a local Unix socket on Linux, or the Docker Desktop virtual machine on Windows and macOS. A context is a saved name for an endpoint, plus a few settings.
docker context lsNAME DESCRIPTION DOCKER ENDPOINT ERROR default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock
The * marks the active context. On Docker Desktop you will also see a context such as desktop-linux, which Docker Desktop creates and selects for you.
Create an SSH context
You need three things first: SSH login to the server with a key (see SSH & Key-based Login), Docker installed on the server, and the SSH user in the server's docker group. Then:
docker context create jaipur-prod \
--description "Shop VPS in Jaipur" \
--docker "host=ssh://deploy@shop.example.in"
docker --context jaipur-prod ps --format '{{.Names}}: {{.Status}}'jaipur-prod Successfully created context "jaipur-prod" shop-caddy-1: Up 3 days shop-app-1: Up 3 days (healthy) shop-db-1: Up 3 days (healthy)
The --context flag runs one command against the server; the containers listed are the ones from Deploy to a VPS with a Reverse Proxy. Behind the scenes Docker runs your normal ssh program, so everything in your ~/.ssh/config (key files, ports, jump hosts) applies. No new port is opened on the server.
Switching and checking the active context
docker context use jaipur-prod
docker context show
docker logs --tail 5 shop-app-1
docker context use defaultjaipur-prod Current context is now "jaipur-prod" jaipur-prod ... default Current context is now "default"
While jaipur-prod is active, every command goes to the server, including docker system prune and docker compose down. That is the main danger of contexts: you think you are cleaning up your laptop and you delete production containers. Three habits help:
- Prefer
docker --context NAME ...for one-off commands instead ofdocker context use. - Show the active context in your shell prompt, or run
docker context showbefore anything destructive. - Set the context for one terminal only with the
DOCKER_CONTEXTenvironment variable, for exampleexport DOCKER_CONTEXT=jaipur-prod. It overridesdocker context useand disappears when you close the terminal.
The older DOCKER_HOST variable also still works, for example DOCKER_HOST=ssh://deploy@shop.example.in docker ps, and it overrides the active context too. If commands go somewhere unexpected, check whether DOCKER_HOST is set.
Compose with a remote context
Compose follows the context like every other command, so you can manage the server's stack from your laptop:
docker --context jaipur-prod compose -p shop ps
docker --context jaipur-prod compose -p shop logs --since 15m appStarting or updating a stack remotely needs more care, because of how files are found:
| Part of the Compose file | Read from |
|---|---|
The Compose file itself, .env, env_file | Your laptop |
build: context (the source code) | Your laptop; sent to the remote daemon, which builds there |
Bind-mount sources such as ./Caddyfile:/etc/caddy/Caddyfile | The server's file system |
Secret files (secrets: file:) | The server's file system, because they are mounted like bind mounts |
So running up -d remotely from a laptop folder whose ./Caddyfile does not exist at the same path on the server fails, or worse, Docker creates an empty folder called Caddyfile on the server and mounts that. For production stacks with bind mounts and secret files, keep the files on the server and run Compose there (by SSH or by a deploy script, as in the VPS lesson). Contexts are perfect for reading, checking and small operations; image-only stacks without host files can be deployed remotely too.
Contexts in CI
A deploy job can use a context instead of running a remote script. After the SSH key and known_hosts setup shown in CI/CD: Build & Push with GitHub Actions:
docker context create prod --docker "host=ssh://deploy@shop.example.in"
docker --context prod pull ghcr.io/priya-sharma/shop-api:2.1.0
docker --context prod compose -p shop up -d --wait appThe second command makes the server pull the image from the registry; nothing is copied through the CI runner. For stacks that depend on files kept on the server, calling the server's deploy script over SSH remains simpler.
Common mistakes
- Running destructive commands in the wrong context. Use
--contextfor single commands and checkdocker context show. - Expecting bind mounts to use files from your laptop. They use paths on the remote host.
- "permission denied while trying to connect to the Docker daemon socket" over SSH. The SSH user is not in the server's
dockergroup. - Exposing port 2375. It gives root access to anyone; use SSH.
- A forgotten
DOCKER_HOSTvariable silently overriding your context.
Key points
- A context is a named Docker endpoint;
docker context lslists them and*marks the active one. docker context create NAME --docker "host=ssh://user@server"uses your normal SSH setup and opens no new ports.--contextruns one command remotely;docker context useandDOCKER_CONTEXTswitch for longer, andDOCKER_HOSToverrides both.- Remote Compose reads the Compose file and
.envlocally, but bind-mount and secret file paths refer to the server. - Never expose the daemon on TCP 2375; use SSH contexts.
Exercise
Isha manages two servers: staging.example.in and api.example.in, both with a deploy user. Create one context for each, list the running containers on staging only, show the last 20 log lines of results-api-1 on production without switching your default context, and set staging as the context for your current terminal only.
Show solution
docker context create staging --docker "host=ssh://deploy@staging.example.in"
docker context create production --docker "host=ssh://deploy@api.example.in"
docker --context staging ps
docker --context production logs --tail 20 results-api-1
export DOCKER_CONTEXT=staging--context affects only the command it is given, so the default context stays unchanged. DOCKER_CONTEXT applies to the current terminal session and overrides whatever docker context use selected.
Quick Quiz
1. What does a Docker context store?
The docker client sends every command to the endpoint of the active context.
2. Your active context is a remote server and you run docker system prune -a. What happens?
Every command follows the active context.
3. Using a remote context, a Compose file mounts ./nginx.conf:/etc/nginx/nginx.conf. Where must nginx.conf exist?
Bind-mount sources are paths on the host running the daemon.
4. Why should you never expose the Docker daemon on TCP port 2375?
Use SSH contexts instead.
Learn Docker & DevOps with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
