Ravindra BagaleCourses & study guides मराठी

Chapter 58: Docker Projects Part 2: AWS RDS, DynamoDB, S3, Reel Upload, Microservices, Queues, Logging, Monitoring and Cloud Deploys

58.12 Project 28: Deploy to EC2 from your laptop with docker context over SSH

Why: the problem

Suppose we are a two-person startup in Bengaluru. Our app runs on one EC2 server. Every release, someone does this: SSH into the server, git pull, build, restart, and hope. It is slow, and the server needs our source code and build tools. When it breaks at night, nobody remembers which commands were typed. Docker has a cleaner way. The docker command on the laptop can talk to the Docker engine on another machine over SSH. This is a docker context. We switch the context to prod, and docker compose up runs on the EC2, using compose files from our laptop. No copying folders, no open Docker port, only SSH. We do it in two styles. First, build on the server through the context. Then the better way: build and push to a registry from the laptop or CI, and the server only pulls tagged images. We add a deploy script with a health check and an instant rollback.

What: every term in plain English

  • Docker context — A saved target for the docker command: local engine, or a remote engine over SSH.
  • DOCKER_HOST — An env var that also points docker at a remote engine, for example ssh://ubuntu@server.
  • ssh-agent — Keeps your SSH key unlocked in memory so docker can use it without asking every time.
  • Remote build — docker build with a remote context sends the build context over SSH and builds on the server.
  • Image tag — A version label like 1.4.2 or the git commit id. We deploy tags, never latest.
  • Rollback — Running the previous tag again, in seconds.
  • docker group — Members can use Docker without sudo. It is the same as root on that host, so add only deploy users.
Prepare the EC2. Set up SSH. Create the context. Deploy by building remotely. Deploy by pulling tags. Roll back and clean up.
The laptop drives the EC2 Docker engine over SSH; images come from ECR by tag

How: step by step

  1. Prepare the EC2. Install Docker, add the ubuntu user to the docker group, open 22 to your IP only.
  2. Set up SSH. Host entry in ~/.ssh/config and ssh-agent with the key.
  3. Create the context. docker context create prod --docker host=ssh://prod.
  4. Deploy by building remotely. docker --context prod compose up -d --build.
  5. Deploy by pulling tags. Push images from the laptop, run a deploy script with health check.
  6. Roll back and clean up. Deploy the old tag; remove the context and terminate the EC2.

Architecture

Laptop or CI docker --context prod compose ... SSH (port 22, your IP only) no Docker TCP port EC2 Docker engine pull tag from ECR with IAM role proxy :80 web:TAG db + volume health check Rollback deploy previous tag in seconds
Deploy with docker context over SSH

Folder layout

startup/
  server/setup-ec2.sh
  laptop/ssh-config.txt  laptop/context.sh
  laptop/release.sh  laptop/deploy.sh  laptop/cleanup.sh
  app/compose.yaml  app/compose.prod.yaml  app/.env.example
  app/web/  (Dockerfile and code from Project 22)

Example 1: Prepare the server and the SSH connection, then create the context

Goal. Prepare the server and the SSH connection, then create the context.

server/setup-ec2.sh

# run once on the EC2 (Ubuntu 24.04) over SSH
set -e
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker ubuntu
sudo systemctl enable --now docker
echo "log out and in again so the docker group applies"

laptop/ssh-config.txt

# add to ~/.ssh/config on the laptop
Host prod
    HostName 203.0.113.10
    User ubuntu
    IdentityFile ~/.ssh/startup-prod.pem
    ServerAliveInterval 30
    ControlMaster auto
    ControlPath ~/.ssh/cm-%r@%h:%p
    ControlPersist 10m

laptop/context.sh

eval "$(ssh-agent -s)"
ssh-add ~/.ssh/startup-prod.pem
ssh prod docker version --format '{{.Server.Version}}'
docker context create prod --description "EC2 Mumbai" --docker "host=ssh://prod"
docker context ls
docker --context prod ps

What each line does

  • download.docker.com — The official Docker apt repository. Docker docs show the same steps.
  • usermod -aG docker ubuntu — ubuntu can run docker without sudo. This is root-level power, so only trusted users.
  • Host prod — A short name. docker uses your normal ssh, so this config applies.
  • ControlMaster / ControlPersist — Keeps one SSH connection open and reuses it. Remote docker commands become much faster.
  • ssh-add — The key is loaded once. docker cannot type a passphrase for you.
  • No port 2375 — We never open the Docker TCP port to the internet. SSH is the only door, and only from your IP.

Green output

$ bash laptop/context.sh
Agent pid 41822
Identity added: /home/asha/.ssh/startup-prod.pem
27.3.1
prod
NAME        DESCRIPTION                               DOCKER ENDPOINT
default *   Current DOCKER_HOST based configuration   unix:///var/run/docker.sock
prod        EC2 Mumbai                                ssh://prod
CONTAINER ID   IMAGE   COMMAND   CREATED   STATUS   PORTS   NAMES

Expected result. The laptop can now control the EC2 Docker engine. The server has no containers yet.

Example 2: Style 1: build and run on the server from the laptop with the same compose file

Goal. Style 1: build and run on the server from the laptop with the same compose file.

app/compose.yaml

services:
  web:
    build: ./web
    image: notes-app:${TAG:-dev}
    environment:
      DATABASE_URL: postgresql://notes:${DB_PASSWORD}@db:5432/notes
    depends_on:
      db: { condition: service_healthy }
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/healthz"]
      interval: 10s
      retries: 3

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: notes
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: notes
    volumes: [db-data:/var/lib/postgresql/data]
    healthcheck: { test: ["CMD", "pg_isready", "-U", "notes"], interval: 5s, retries: 10 }
    restart: unless-stopped

  proxy:
    image: nginx:1.27-alpine
    ports: ["80:80"]
    configs:
      - source: nginx
        target: /etc/nginx/conf.d/default.conf
    depends_on: [web]
    restart: unless-stopped

configs:
  nginx:
    content: |
      server {
        listen 80;
        location / { proxy_pass http://web:3000; proxy_set_header Host $$host; }
      }

volumes:
  db-data:

app/.env.example

DB_PASSWORD=change-me-db
TAG=dev

What each line does

  • configs with content — The nginx file is written inside compose.yaml. With a remote context, bind mounts like ./nginx.conf would look for the file on the server, not the laptop. Inline configs avoid that.
  • $$host — Double dollar so Compose does not treat $host as its own variable.
  • build: ./web — With --context prod, the web folder is sent over SSH and built on the EC2.
  • .env stays on the laptop — Compose reads .env locally and sends the final values. The server never needs the file.
  • Named volume db-data — Lives on the server. It is not affected when we switch contexts.
  • docker context use prod — Makes prod the default. Risky: you may forget. We prefer --context prod on each command.

Green output

$ cd app && cp .env.example .env
$ docker --context prod compose up -d --build
 => [web internal] load build context   0.2s
 => transferring context: 48.31kB
 Container app-db-1  Healthy
 Container app-web-1  Started
 Container app-proxy-1  Started
$ docker --context prod compose ps --format '{{.Name}} {{.Status}}'
app-db-1 Up 40 seconds (healthy)
app-proxy-1 Up 30 seconds
app-web-1 Up 31 seconds (healthy)
$ curl -s http://203.0.113.10/healthz
ok

Expected result. The app runs on EC2. We typed every command on the laptop.

Example 3: Style 2 (better): push tagged images, deploy with a health check, and roll back

Goal. Style 2 (better): push tagged images, deploy with a health check, and roll back.

laptop/release.sh

set -e
REG=123456789012.dkr.ecr.ap-south-1.amazonaws.com
TAG=$(git rev-parse --short HEAD)
aws ecr get-login-password --region ap-south-1 | docker login --username AWS --password-stdin "$REG"
docker build --platform linux/amd64 -t "$REG/notes-app:$TAG" app/web
docker push "$REG/notes-app:$TAG"
echo "$TAG" | tee .last-built-tag

app/compose.prod.yaml

services:
  web:
    image: 123456789012.dkr.ecr.ap-south-1.amazonaws.com/notes-app:${TAG}
    build: !reset null
    pull_policy: always

laptop/deploy.sh

set -e
TAG=${1:?usage: deploy.sh TAG}
C="docker --context prod compose -f app/compose.yaml -f app/compose.prod.yaml --env-file app/.env"
PREV=$(docker --context prod inspect app-web-1 --format '{{.Config.Image}}' 2>/dev/null | cut -d: -f2 || echo none)
echo "deploying $TAG (previous $PREV)"
TAG=$TAG $C up -d --no-deps web
for i in $(seq 1 12); do
  S=$(docker --context prod inspect app-web-1 --format '{{.State.Health.Status}}')
  echo "health: $S"
  [ "$S" = "healthy" ] && { echo "$TAG" | tee .deployed-tag; exit 0; }
  sleep 5
done
echo "NOT healthy, rolling back to $PREV"
TAG=$PREV $C up -d --no-deps web
exit 1

laptop/cleanup.sh

docker --context prod compose -f app/compose.yaml down -v
docker --context prod image prune -af
docker context rm prod
echo "now terminate the EC2 instance in the console or with aws ec2 terminate-instances"

What each line does

  • Tag = git commit — Every image matches a commit. We always know what is running.
  • --platform linux/amd64 — Laptops with Apple chips build ARM images by default. The EC2 is x86, so we say amd64. Or use a t4g ARM instance.
  • !reset null — In the prod override, the build section is removed, so the server only pulls.
  • Server needs ECR access — Attach an IAM role with AmazonEC2ContainerRegistryReadOnly to the EC2, and install the ECR credential helper there. No keys on the server.
  • --no-deps web — Only web is replaced. The database keeps running.
  • Health loop and rollback — If web is not healthy within 60 seconds, the script redeploys the previous tag automatically.
  • Same script in CI — GitHub Actions or Jenkins can run release.sh and deploy.sh with an SSH key stored as a secret.

Green output

$ bash laptop/release.sh
Login Succeeded
 => pushing 123456789012.dkr.ecr.ap-south-1.amazonaws.com/notes-app:9f3c2ab
9f3c2ab
$ bash laptop/deploy.sh 9f3c2ab
deploying 9f3c2ab (previous dev)
 Container app-web-1  Recreated
health: starting
health: healthy
9f3c2ab
$ bash laptop/deploy.sh broken01
deploying broken01 (previous 9f3c2ab)
health: starting
health: unhealthy
...
NOT healthy, rolling back to 9f3c2ab

Expected result. Good tags go live in seconds, and a bad tag is rolled back automatically.

Every command, step by step (plain docker and Compose)

  1. Launch EC2

    Ubuntu 24.04, t3.small, security group: 22 from your IP, 80 from anywhere
    

    What it does: The server for this project.

    Instance running, public IP 203.0.113.10
    
  2. Install Docker on it

    scp server/setup-ec2.sh prod: && ssh prod bash setup-ec2.sh
    

    What it does: Official Docker packages.

    log out and in again
    
  3. Load your key

    eval "$(ssh-agent -s)" && ssh-add ~/.ssh/startup-prod.pem
    

    What it does: docker can now use SSH quietly.

    Identity added
    
  4. Test SSH docker

    ssh prod docker ps
    

    What it does: Works without sudo.

    CONTAINER ID   IMAGE ...
    
  5. Create context

    docker context create prod --docker host=ssh://prod
    

    What it does: Saves the target.

    prod
    
  6. List contexts

    docker context ls
    

    What it does: Star shows the current one.

    default *
    
  7. Deploy style 1

    docker --context prod compose up -d --build
    

    What it does: Builds and runs on EC2.

    Container app-web-1 Started
    
  8. Check from outside

    curl -s http://203.0.113.10/healthz
    

    What it does: The app answers.

    ok
    
  9. Read remote logs

    docker --context prod compose logs --tail 5 web
    

    What it does: Logs over SSH.

    web-1 | listening on 3000
    
  10. Release style 2

    bash laptop/release.sh
    

    What it does: Builds and pushes a tagged image.

    9f3c2ab
    
  11. Deploy a tag

    bash laptop/deploy.sh 9f3c2ab
    

    What it does: Health check then success.

    health: healthy
    
  12. Clean up

    bash laptop/cleanup.sh
    

    What it does: Removes stack, images and context; then terminate the EC2.

    now terminate the EC2 instance
    

When it is red 1

error during connect: ... ssh: connect to host 203.0.113.10 port 22: Connection timed out

Cause. The security group does not allow SSH from your current IP.

Fix, step by step

  1. Edit inbound rule 22 to your current public IP.
  2. Check with ssh prod true first.

When it is red 2

permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock

Cause. The ubuntu user is not in the docker group yet, or did not log in again.

Fix, step by step

  1. Run sudo usermod -aG docker ubuntu, then reconnect.
  2. Kill the old ControlMaster: ssh -O exit prod.

When it is red 3

Load key "/home/asha/.ssh/startup-prod.pem": bad permissions

Cause. The key file is readable by others. SSH refuses it.

Fix, step by step

  1. Make the key private to your user, for example with the file permission setting 600 in your file manager or with your usual tool.
  2. On Windows, remove other users from the file's security tab.

When it is red 4

exec format error

Cause. The image was built for ARM on an Apple laptop and runs on an x86 EC2.

Fix, step by step

  1. Build with --platform linux/amd64, or use docker buildx for both.

When it is red 5

Error response from daemon: pull access denied for 123456789012.dkr.ecr...

Cause. The server cannot log in to ECR.

Fix, step by step

  1. Attach the ECR read-only role to the EC2.
  2. Install amazon-ecr-credential-helper on the server and set credsStore ecr-login.

When it is red 6

open /home/ubuntu/app/nginx.conf: no such file or directory

Cause. A bind mount path is resolved on the server, but the file is on the laptop.

Fix, step by step

  1. Use inline configs, or copy the file to the server first, or bake it into the image.

Practice tasks

Easy Task 1: Context

Why: Create.

What to do: Create the prod context and list it.

How:
  1. Use your laptop with Docker and one Ubuntu EC2 (t3.small) you can SSH into.
  2. Use the 3-tier app from Project 16 or the notes app from Project 22.
  3. Run the steps and compare with the solution. Always run the cleanup step.
Solution
docker context create prod --docker host=ssh://prod

Easy Task 2: Remote ps

Why: Use.

What to do: Run docker ps on prod without logging in.

How:
  1. Use your laptop with Docker and one Ubuntu EC2 (t3.small) you can SSH into.
  2. Use the 3-tier app from Project 16 or the notes app from Project 22.
  3. Run the steps and compare with the solution. Always run the cleanup step.
Solution
docker --context prod ps

Easy Task 3: Deploy

Why: Compose.

What to do: Deploy style 1 and open the public IP.

How:
  1. Use your laptop with Docker and one Ubuntu EC2 (t3.small) you can SSH into.
  2. Use the 3-tier app from Project 16 or the notes app from Project 22.
  3. Run the steps and compare with the solution. Always run the cleanup step.
Solution
docker --context prod compose up -d --build

Medium Task 4: DOCKER_HOST

Why: Env.

What to do: Do the same with DOCKER_HOST=ssh://prod instead of a context.

How:
  1. Use your laptop with Docker and one Ubuntu EC2 (t3.small) you can SSH into.
  2. Use the 3-tier app from Project 16 or the notes app from Project 22.
  3. Run the steps and compare with the solution. Always run the cleanup step.
Solution
DOCKER_HOST=ssh://prod docker ps

Medium Task 5: Bind mount trap

Why: Paths.

What to do: Change the nginx config to a bind mount and see the error.

How:
  1. Use your laptop with Docker and one Ubuntu EC2 (t3.small) you can SSH into.
  2. Use the 3-tier app from Project 16 or the notes app from Project 22.
  3. Run the steps and compare with the solution. Always run the cleanup step.
Solution
./nginx.conf:/etc/nginx/conf.d/default.conf

Medium Task 6: Release

Why: ECR.

What to do: Push a commit tag to ECR.

How:
  1. Use your laptop with Docker and one Ubuntu EC2 (t3.small) you can SSH into.
  2. Use the 3-tier app from Project 16 or the notes app from Project 22.
  3. Run the steps and compare with the solution. Always run the cleanup step.
Solution
bash laptop/release.sh

Medium Task 7: Health deploy

Why: Script.

What to do: Deploy the tag and watch the health loop.

How:
  1. Use your laptop with Docker and one Ubuntu EC2 (t3.small) you can SSH into.
  2. Use the 3-tier app from Project 16 or the notes app from Project 22.
  3. Run the steps and compare with the solution. Always run the cleanup step.
Solution
bash laptop/deploy.sh TAG

Hard Task 8: Rollback

Why: Script.

What to do: Deploy a broken tag and confirm auto rollback.

How:
  1. Use your laptop with Docker and one Ubuntu EC2 (t3.small) you can SSH into.
  2. Use the 3-tier app from Project 16 or the notes app from Project 22.
  3. Run the steps and compare with the solution. Always run the cleanup step.
Solution
bash laptop/deploy.sh broken01

Hard Task 9: CI

Why: Actions.

What to do: Run release and deploy from a CI job with approval.

How:
  1. Use your laptop with Docker and one Ubuntu EC2 (t3.small) you can SSH into.
  2. Use the 3-tier app from Project 16 or the notes app from Project 22.
  3. Run the steps and compare with the solution. Always run the cleanup step.
Solution
environment: production

Easy Task 10: Clean

Why: Habit.

What to do: Remove the stack and context and terminate the EC2.

How:
  1. Use your laptop with Docker and one Ubuntu EC2 (t3.small) you can SSH into.
  2. Use the 3-tier app from Project 16 or the notes app from Project 22.
  3. Run the steps and compare with the solution. Always run the cleanup step.
Solution
bash laptop/cleanup.sh

Homework: try at home

Move the deploy into GitHub Actions: on a push to main, build and push the tag to ECR, then run deploy.sh with DOCKER_HOST=ssh://prod using an SSH key from repository secrets and a known_hosts entry. Add a manual approval step with an environment named production.

Recap

  • A docker context lets the laptop or CI control a remote Docker engine over SSH.
  • Never open the Docker TCP port; SSH from your IP is enough.
  • Bind mounts resolve on the server; use inline configs or bake files into images.
  • Deploy tagged images from a registry; the server only pulls.
  • A health-check loop with automatic rollback makes deploys safe.
  • Clean up contexts, images and the EC2 after practice.

FAQ

Q. Is this good for production?

For one or two servers, yes. For many servers use ECS (Project 29), Swarm (Project 30) or Kubernetes.

Q. Can I use Windows?

Yes. Docker Desktop and the Windows OpenSSH client work with ssh:// contexts.

Q. Is docker context the same as kubectl context?

Same idea, different tool. One points docker, the other points kubectl.

Q. How do I see which context I am on?

docker context ls shows a star, and docker context show prints the name.

Q. Can two people deploy at once?

They can clash. Deploy from CI only, with one job at a time.

Q. Where do secrets live on the server?

Nowhere in files. Compose sends the values from the laptop or CI. For more safety, read them from SSM Parameter Store at start, as in Project 17.

Q. Is remote build slow?

The build context goes over SSH every time. A good .dockerignore and the registry style make it fast.

Q. What about zero downtime?

With one web container there is a few seconds gap. Run two copies behind nginx (Project 22) and replace one at a time.

Interview questions

  1. How do you deploy containers to a single VM without Kubernetes?

    Model answer: Push images to a registry and run compose on the VM, often through a docker context or CI over SSH.

  2. What is a docker context?

    Model answer: A saved endpoint configuration for the Docker CLI.

  3. How do you roll back a container deploy?

    Model answer: Run the previous image tag; keep tags immutable and never rely on latest.

  4. How do you give an EC2 access to ECR without keys?

    Model answer: An IAM instance role with ECR read permissions and the credential helper.

  5. Why is access to the Docker socket sensitive?

    Model answer: It allows starting privileged containers, which equals root on the host.

Quick quiz

Q1. What is a docker context?

A named target for the docker CLI, such as a remote engine over SSH.

Q2. Why SSH instead of opening port 2375?

The Docker API is root access. SSH adds keys and encryption and no new open port.

Q3. Why do bind mounts break with a remote context?

Paths are read on the server, not on the laptop.

Q4. Why deploy tags instead of building on the server?

Faster, repeatable, the server needs no source, and rollback is just the old tag.

Q5. How does the script roll back?

If the new container is not healthy in time, it starts the previous tag.

Samajla ka?

Ravi Sir's Cards for this lesson

मराठी cards