Ravindra BagaleCourses & study guides मराठी

Chapter 55: Docker Security Best Practices, Image Scanning with Trivy, Logging, Monitoring and Docker Swarm Basics

55.10 Docker contexts: control remote Docker over SSH

Friends, until now we logged in to each server with SSH and typed docker there. A Docker context lets the docker command on your laptop talk to Docker on a remote server, over plain SSH, safely. Today, step by step: create a context, switch between laptop, staging and production, deploy a Compose app remotely, use it from CI, and the safety rules so you never run a command on production by mistake.

1. Start from a story

At Urban Company, a developer named Shruti deployed a small internal dashboard to an EC2 server. Her routine was: scp the compose file, ssh in, git pull, docker compose up, and hope she was on the right server.

One day she opened the wrong terminal tab and ran docker compose down on the production server instead of staging. The dashboard was down for ten minutes.

Her lead showed her Docker contexts. She created two contexts, staging and prod, both over SSH with her key. Now she deploys from her laptop with docker --context staging compose up -d, and her shell prompt shows the active context in red when it is prod.

No more copying files to servers, no Docker port open to the internet, and no wrong-server surprises. Today we set it up.

docker CLI on your laptopyou type the commandActive contextwhich Docker to talk toSSH connectionyour key, port 22 onlydockerd on the serverruns the command thereOutput back to youas if it were local
Picture 1: how a context works

2. Prepare SSH step by step

Step 1: Key login first

Why: Contexts use your normal SSH.

What: The context runs ssh user@host docker system dial-stdio. If plain ssh works without a password prompt, the context will work.

Example 1: Test SSH

Type this:

ssh -i ~/.ssh/shop-staging.pem ubuntu@13.233.10.20 'docker version --format {{.Server.Version}}'

You will see:

27.3.1

What it means: Docker is reachable over SSH.

Example 2: Add to ssh config

Type this:

printf '%s\n' 'Host staging' '  HostName 13.233.10.20' '  User ubuntu' '  IdentityFile ~/.ssh/shop-staging.pem' 'Host prod' '  HostName 13.233.50.60' '  User ubuntu' '  IdentityFile ~/.ssh/shop-prod.pem' >> ~/.ssh/config && chmod 0600 ~/.ssh/config && ssh staging hostname

You will see:

ip-172-31-5-10

What it means: Short names, safe 0600 permissions.

Example 3: Key permissions

Type this:

chmod 0600 ~/.ssh/shop-staging.pem ~/.ssh/shop-prod.pem && ls -l ~/.ssh/*.pem

You will see:

-rw------- 1 me me 1674 shop-prod.pem
-rw------- 1 me me 1674 shop-staging.pem

What it means: SSH refuses keys others can read.

Step 2: User in the docker group

Why: No sudo over the context.

What: The remote user must be able to run docker without sudo. Remember that the docker group is root-equivalent, so only trusted users.

Example 4: Add the user

Type this:

ssh staging 'sudo usermod -aG docker ubuntu' && echo added

You will see:

added

What it means: Log in again for it to apply.

Example 5: Check

Type this:

ssh staging 'id -nG'

You will see:

ubuntu adm sudo docker

What it means: docker is listed.

Example 6: No TCP port

Type this:

ssh staging 'sudo ss -lntp | grep -c 2375'

You will see:

0

What it means: Docker is not listening on the network; SSH is the only way in.

Step 3: Speed up with SSH multiplexing

Why: Reuse one connection.

What: Each docker command opens a new SSH session. ControlMaster keeps one open and reuses it.

Example 7: Add to config

Type this:

printf '%s\n' 'Host *' '  ControlMaster auto' '  ControlPath ~/.ssh/cm-%r@%h:%p' '  ControlPersist 10m' >> ~/.ssh/config && echo ok

You will see:

ok

What it means: Connections kept for 10 minutes.

Example 8: Before

Type this:

time docker -H ssh://staging ps -q >/dev/null

You will see:

real    0m1.42s

What it means: Slow handshake each time.

Example 9: After

Type this:

time docker -H ssh://staging ps -q >/dev/null

You will see:

real    0m0.21s

What it means: Much faster.

3. Contexts step by step

Step 4: docker context ls

Why: The default context is local.

What: Every Docker CLI has a default context that points to the local socket.

Example 10: List

Type this:

docker context ls

You will see:

NAME        DESCRIPTION                               DOCKER ENDPOINT               ERROR
default *   Current DOCKER_HOST based configuration   unix:///var/run/docker.sock

What it means: The star marks the active one.

Example 11: Show active

Type this:

docker context show

You will see:

default

What it means: Name only.

Example 12: Where stored

Type this:

ls ~/.docker/contexts/meta | wc -l

You will see:

0

What it means: No custom contexts yet.

Step 5: docker context create

Why: Point a name at a server.

What: Create one context per server with an ssh:// endpoint and a description.

Example 13: Staging

Type this:

docker context create staging --description 'shop staging EC2' --docker host=ssh://staging

You will see:

staging
Successfully created context "staging"

What it means: Uses the ssh config name.

Example 14: Production

Type this:

docker context create prod --description 'shop PRODUCTION EC2' --docker host=ssh://prod

You will see:

prod
Successfully created context "prod"

What it means: Description in capitals as a warning.

Example 15: List again

Type this:

docker context ls --format '{{.Name}} {{.DockerEndpoint}}'

You will see:

default unix:///var/run/docker.sock
prod ssh://prod
staging ssh://staging

What it means: Three targets.

Step 6: Run one command on a context

Why: --context for a single command.

What: --context NAME runs just that command against the remote server. This is the safest habit.

Example 16: Remote ps

Type this:

docker --context staging ps --format '{{.Names}} {{.Status}}'

You will see:

dashboard-web-1 Up 2 days

What it means: Containers on staging.

Example 17: Remote info

Type this:

docker --context prod info --format '{{.Name}} {{.ServerVersion}} {{.NCPU}} CPUs'

You will see:

ip-172-31-9-44 27.3.1 2 CPUs

What it means: Production details.

Example 18: Remote run

Type this:

docker --context staging run --rm alpine:3.20 hostname

You will see:

b41c2d9e0f11

What it means: The container ran on staging, not your laptop.

Step 7: docker context use

Why: Switch the default.

What: use changes the active context for every later command until you switch back. Handy, but risky with prod.

Example 19: Switch

Type this:

docker context use staging && docker ps -q | wc -l

You will see:

staging
Current context is now "staging"
3

What it means: Now every command goes to staging.

Example 20: Env override

Type this:

DOCKER_CONTEXT=default docker ps -q | wc -l

You will see:

0

What it means: For one command, the env var wins.

Example 21: Back home

Type this:

docker context use default

You will see:

default
Current context is now "default"

What it means: Always switch back.

4. Deploy with a context

compose.yaml on laptopno copying to the serverdocker --context staging compose up -dimages pulled on the serverCheck stagingcurl and logsdocker --context prod compose up -dsame file, productionVerify prodthen switch back to default
Picture 2: deploying from the laptop

Step 8: Compose over a context

Why: The file stays on your laptop.

What: Compose reads the file locally and sends API calls to the remote Docker. Images are pulled by the server. Bind mounts refer to paths on the server.

Example 22: The app

Type this:

printf '%s\n' 'services:' '  web:' '    image: nginx:1.27-alpine' '    ports: ["80:80"]' '    restart: unless-stopped' > compose.yaml && echo ok

You will see:

ok

What it means: A simple file.

Example 23: Deploy to staging

Type this:

docker --context staging compose -p dashboard up -d

You will see:

[+] Running 2/2
 ✔ Network dashboard_default  Created
 ✔ Container dashboard-web-1  Started

What it means: Running on the server.

Example 24: Check

Type this:

curl -s -o /dev/null -w '%{http_code}\n' http://13.233.10.20/ && docker --context staging compose -p dashboard ps --format '{{.Name}} {{.Status}}'

You will see:

200
dashboard-web-1 Up 10 seconds

What it means: Served from staging.

Step 9: Build on the remote server

Why: The build context is uploaded.

What: docker --context staging build sends your folder to the remote daemon and builds there. Keep .dockerignore tight so the upload is small.

Example 25: Ignore file

Type this:

printf '%s\n' '.git' 'node_modules' '*.log' '.env' > .dockerignore && echo ok

You will see:

ok

What it means: Only needed files go up.

Example 26: Remote build

Type this:

docker --context staging build -q -t dashboard:1.1 .

You will see:

sha256:4e2a...

What it means: Built on staging's CPU.

Example 27: Better for prod

Type this:

printf 'For production, build in CI, push to ECR, and let the server pull by tag and digest.\n'

You will see:

For production, build in CI, push to ECR, and let the server pull by tag and digest.

What it means: Remote builds are fine for staging.

Step 10: Use a context from CI

Why: Deploy from Jenkins over SSH.

What: The CI agent gets the SSH key from its credential store, creates a context, deploys, and removes it.

Example 28: Pipeline step

Type this:

printf '%s\n' 'docker context create prod --docker host=ssh://deploy@13.233.50.60' 'docker --context prod compose -p dashboard pull' 'docker --context prod compose -p dashboard up -d' 'docker context rm -f prod' > deploy.sh && chmod 0755 deploy.sh && cat deploy.sh

You will see:

docker context create prod --docker host=ssh://deploy@13.233.50.60
docker --context prod compose -p dashboard pull
docker --context prod compose -p dashboard up -d
docker context rm -f prod

What it means: Pull first, then up.

Example 29: Known hosts

Type this:

ssh-keyscan -H 13.233.50.60 >> ~/.ssh/known_hosts 2>/dev/null && echo pinned

You will see:

pinned

What it means: Otherwise SSH waits for a yes prompt and the job hangs.

Example 30: Jenkins wrapper

Type this:

printf '%s\n' "sshagent(['prod-deploy-key']) {" '  sh "./deploy.sh"' '}' > Jenkinsfile.part && cat Jenkinsfile.part

You will see:

sshagent(['prod-deploy-key']) {
  sh "./deploy.sh"
}

What it means: The key stays in Jenkins credentials.

Step 11: Safety rules for prod

Why: Make mistakes hard.

What: Show the context in your prompt, prefer --context over use, and never leave prod as the active context.

Example 31: Prompt helper

Type this:

printf '%s\n' 'dctx(){ c=$(docker context show 2>/dev/null); [ "$c" = prod ] && printf "[PROD] " || printf "[%s] " "$c"; }' 'PS1="$(dctx)$PS1"' >> ~/.bashrc && echo added

You will see:

added

What it means: A visible warning.

Example 32: Read-only check first

Type this:

docker --context prod compose -p dashboard ps && docker --context prod compose -p dashboard config -q && echo ok-to-deploy

You will see:

NAME              STATUS
dashboard-web-1   Up 3 days
ok-to-deploy

What it means: Look before you change.

Example 33: Separate user

Type this:

printf 'Use a deploy user on prod with its own key, and limit who has that key.\n'

You will see:

Use a deploy user on prod with its own key, and limit who has that key.

What it means: Remember: docker access is root access.

Step 12: Context for a swarm and cleanup

Why: Manage a cluster remotely.

What: Point a context at a swarm manager to run docker stack and docker service commands from your laptop.

Example 34: Manager context

Type this:

docker context create swarm --docker host=ssh://ubuntu@10.0.1.10 && docker --context swarm node ls --format '{{.Hostname}} {{.Status}}'

You will see:

swarm
Successfully created context "swarm"
mgr1 Ready
wrk1 Ready
wrk2 Ready

What it means: The whole cluster from the laptop.

Example 35: Stack deploy remotely

Type this:

docker --context swarm stack deploy -c stack.yaml --with-registry-auth shop | tail -1

You will see:

Updating service shop_web (id: k8m2...)

What it means: Same file as the last lesson.

Example 36: Export and remove

Type this:

docker context export staging staging.dockercontext && docker context rm swarm && docker context ls --format '{{.Name}}'

You will see:

Written file "staging.dockercontext"
swarm
default
prod
staging

What it means: Export to share the endpoint; keys are not inside it.

5. Common mistakes and how Shruti avoids them

Mistake one: opening the Docker TCP port 2375 to use the CLI remotely. Shruti only uses SSH contexts.

Mistake two: leaving prod as the active context. She uses --context prod for each command and switches back to default.

Mistake three: bind mounts with laptop paths. Over a context, paths are on the server, so she uses named volumes or server paths.

Mistake four: building production images remotely. She builds in CI and only pulls on the server.

Mistake five: CI jobs hanging on an SSH host-key prompt. She adds the host to known_hosts in the pipeline.

Check yourself

Q1. What transport does an ssh:// context use?

SSH, using your normal key and ssh config.

Q2. Difference between --context and docker context use?

--context affects one command; use changes the default for all later commands.

Q3. With a remote context, whose disk does a bind mount use?

The remote server's.

Q4. Why not open port 2375?

It gives unauthenticated root-level Docker access over the network.

Friends, set up SSH key login, create one context per server, use --context for each command, deploy Compose files from your laptop, and keep production behind clear safety rules. Did you understand? Let us try it once more. Create a staging context for a VM, deploy nginx with compose up from your laptop, and check it with curl.

Ravindra Bagale's Tip

Type --context prod every single time instead of docker context use prod. The extra words are your seatbelt.

Lab: try at home

  1. Set up SSH key login and an ssh config entry for a VM.
  2. Confirm Docker on the VM is not listening on TCP.
  3. Create a staging context and run docker --context staging ps.
  4. Deploy nginx with docker --context staging compose up -d.
  5. Add SSH multiplexing and time the difference.
  6. Write the CI deploy script with known_hosts pinning.
  7. Add the context name to your shell prompt.

Recap

  • A context points the CLI at another Docker.
  • ssh:// contexts need only SSH, no open Docker port.
  • --context for one command, use for the default.
  • Compose files stay local; images and mounts are remote.
  • CI creates a context, deploys, then removes it.
  • Guard prod with prompts, a deploy user and read-only checks.

Lab

  1. Run an app with --user, --cap-drop ALL and --read-only and fix what breaks.
  2. Scan an image with Trivy and fix one CVE.
  3. Set log rotation in daemon.json.
  4. Start the monitoring stack and view a dashboard.
  5. Create a 1-node swarm and do a rolling update and a rollback.
  6. Deploy a stack with a secret and a config, then rotate the secret.
  7. Deploy a Compose app to a VM through an SSH context.

Practice task

Harden one of your images: non-root, read-only, dropped capabilities, no secrets in history, zero CRITICAL in Trivy. Show the commands and output.

Ravindra Bagale's Tip

The security rule: least privilege, least software, and never a secret in an image. Check every image with a scan before it reaches production.

In short

  • Run as non-root with dropped capabilities.
  • Make the root filesystem read-only.
  • Secrets come as files, never ENV.
  • Scan every image in CI.
  • Rotate logs and ship them centrally.
  • Swarm gives simple clustering built into Docker.