For six modules you have taken for granted a machine with Docker installed. In module 6 that machine multiplied into a cluster and the question stopped being comfortable: where do the hosts come from, who installs Docker on them, who updates them, and who answers the phone when one of them stops booting? This lesson starts with the tool that tried it first — Docker Machine, archived today — and ends with how it is actually done in 2026.

Contents

  1. The real provisioning problem
  2. What Docker Machine was and its driver model
  3. The characteristic commands
  4. Why it is archived and what to do if you run into it
  5. What did survive: Docker contexts
  6. An aurora-prod context over SSH
  7. Deploying against a remote host without switching terminals
  8. SSH versus exposing the daemon over TCP
  9. The map of today's alternatives
  10. Direct installation: the idempotent script
  11. cloud-init: let the machine be born with Docker
  12. Terraform / OpenTofu: declarative infrastructure
  13. Ansible: configuring and maintaining the fleet
  14. Packer and managed services
  15. The host as an attack surface
  16. Your own hosts or managed? Decision criteria

  1. The real provisioning problem

Between "I have an account with a cloud provider" and "docker compose up -d brings up Aurora Libros in production" there is a list that someone has to work through: create the VM (size, region, disk, network, security group), set up SSH access and an unprivileged user, install Engine and the Compose plugin, tune the daemon (data-root, logging, live-restore), harden the host (firewall, automatic patching), deploy the application, repeat it identically on the second host, and keep it running for three years. The last two points are what separate a tool from a tutorial: Docker Machine tackled the first three and left the rest out.

  1. What Docker Machine was and its driver model

Docker Machine (docker-machine) was an official binary, a contemporary of Docker Toolbox (2015-2018), with one specific purpose: create a VM with Docker Engine already installed — locally or in the cloud — and point your local client at that remote daemon.

That second half was the interesting one. Remember 01-03: client and daemon are separate processes that talk over an API. If the client can reach a remote daemon, docker ps lists the containers of a server in Frankfurt from your laptop. Docker Machine automated the paperwork: creating the VM, installing Engine, generating mutual TLS certificates and exporting the variables that redirected the client.

graph LR
    DM["docker-machine<br/>(your laptop)"] -->|"1. creates the VM (provider API)"| VM
    DM -->|"2. installs Engine and generates certificates"| VM
    DM -->|"3. exports DOCKER_HOST and DOCKER_CERT_PATH"| CLI
    CLI["docker client<br/>(your laptop)"] -->|"docker ps over TLS"| VM
    VM["VM 'aurora-prod'<br/>dockerd + TLS :2376"]

The extensible piece was the driver: one adapter per platform. The command was the same; only --driver and its options changed.

Driver Where it created the machine Status in 2026
virtualbox, hyperv Local VM Obsolete (this was Docker Toolbox)
amazonec2, digitalocean Instance / droplet Archived along with the project
azure, google, openstack VM in the public or private cloud Archived
generic An already existing host, over SSH Its idea survives (see §10)
none Only registered an external machine Replaced by docker context

The generic driver created nothing: it connected over SSH to an existing machine and installed Docker on it. It was a crude remote installer, exactly what a configuration manager covers better today.

  1. The characteristic commands

You will see them in old documentation and in legacy scripts. Recognize them even if you never run them:

docker-machine create --driver digitalocean \
  --digitalocean-access-token "$DO_TOKEN" \
  --digitalocean-size s-2vcpu-4gb --digitalocean-region fra1 \
  aurora-prod

docker-machine ls                 # machines and their state
docker-machine ip aurora-prod     # public IP
docker-machine ssh aurora-prod    # SSH session without hunting for the key
eval "$(docker-machine env aurora-prod)"   # ← the key command
docker ps                         # it lists the REMOTE containers!
docker-machine rm aurora-prod     # destroys the VM at the provider

The eval was the magic and also the problem. It exported DOCKER_TLS_VERIFY=1, DOCKER_HOST=tcp://203.0.113.42:2376 and DOCKER_CERT_PATH pointing at the generated certificates.

From that point on, every docker command in that terminal went to the remote server, and opening another tab put you back on the local daemon. More than one famous incident was born from a docker compose down in the wrong terminal.

  1. Why it is archived and what to do if you run into it

Docker archived the repository in 2021. There are no security patches, the drivers use APIs that have changed, and the installation downloads Engine versions that no longer exist.

Reason it was abandoned What replaced it
Every provider published its own CLI and API aws, gcloud, az, plus Terraform
Infrastructure as code became standard Terraform / OpenTofu, Pulumi
Configuring the host is a separate problem Ansible, cloud-init
The local VM was solved on the desktop Docker Desktop (07-03)
Switching daemons does not require creating machines docker context
Multi-node orchestration was taken over by Kubernetes Module 6

Rule of thumb: if a tutorial uses it, that tutorial is more than five years old and everything else it says needs verifying. If it shows up in a legacy script at your company, do not run it: work out which machines it manages, move that information into contexts or Terraform, and plan its retirement with whoever maintains the infrastructure.

  1. What did survive: Docker contexts

The idea that was worth keeping — one client, several daemons — is a first-class feature today: contexts. A context is a named destination. Unlike the eval, the switch is global and persistent, not per terminal.

Command What it does
docker context create <n> --docker host=... Defines a new destination
docker context ls Lists the contexts; * marks the active one
docker context use <n> Changes the active one (persistent)
docker context inspect <n> Shows the endpoint and the TLS configuration
docker --context <n> <cmd> Runs a single command against another destination
docker context rm <n> Removes the context (it does not touch the server)

The important distinction: docker-machine rm destroyed a machine; docker context rm merely forgets an address. Contexts do not create, do not install and do not maintain anything: they only point. That is why they are still useful and why they are no substitute for provisioning.

  1. An aurora-prod context over SSH

Aurora Libros has a host at libros.example.com with a deploy user in the docker group. First, make SSH work with a key and no password, and add an entry in ~/.ssh/config that makes everything else readable:

ssh-keygen -t ed25519 -C "deploy@aurora" -f ~/.ssh/aurora_deploy
ssh-copy-id -i ~/.ssh/aurora_deploy.pub [email protected]
Host aurora-prod
    HostName libros.example.com
    User deploy
    IdentityFile ~/.ssh/aurora_deploy
    IdentitiesOnly yes

The context is a one-liner, and checking it does not require activating it:

docker context create aurora-prod \
  --description "Aurora Libros production (fra1)" \
  --docker "host=ssh://aurora-prod"

docker --context aurora-prod version --format '{{.Server.Version}}'
# 27.5.1
docker --context aurora-prod ps --format '{{.Names}}\t{{.Status}}'
# aurora-web     Up 9 days
# aurora-api     Up 9 days (healthy)
# aurora-cache   Up 9 days
# aurora-db      Up 9 days (healthy)

Four containers, nine days of uptime, without opening a single manual SSH session.

  1. Deploying against a remote host without switching terminals

Compose honors contexts, so the full deployment is this:

docker --context aurora-prod compose \
  -f compose.yaml -f compose.prod.yaml \
  --env-file .env.prod up -d

docker --context aurora-prod compose ps
docker --context aurora-prod compose logs -f --tail=50 api

Three details change the outcome:

Detail Behavior
volumes paths They resolve on the remote host, not on your laptop
build: The build context is packaged and uploaded over SSH: slow
.env and env_file They are read on your machine, and the values travel over the network
ports: "8080:8080" It publishes on the server's IP, not on your localhost

From the second row comes a golden rule: in production you do not build, you deploy an already published image. That is what your compose.prod.yaml does, referencing ghcr.io/auroralibros/aurora-api:2.0.0 by digest instead of a build:. The pipeline from 06-02 builds, signs and publishes; the host only does a pull.

Defensive pattern: never use docker context use for production. Leave default active and type --context aurora-prod explicitly, or at most set up an alias dprod='docker --context aurora-prod'. It takes longer to type, and that is exactly the advantage.

  1. SSH versus exposing the daemon over TCP

Docker Machine used tcp://IP:2376 with mutual TLS. You can still do that today, but you almost never should.

Aspect ssh:// tcp:// with TLS tcp:// without TLS
Exposed port 22 (already open) 2376 2375
Authentication SSH keys you already manage Your own certificates to maintain None
Encryption Yes Yes No
Credential rotation An existing procedure Your own CA, renewals, CRL —
Auditing The host's SSH logs Only the daemon's —
Risk if leaked That user's access root on the host root for anyone
Recommendation The default Only with a justification Never

The risk row is the one to internalize. You already saw it in 05-03: whoever talks to the daemon can run docker run -v /:/host --privileged and is root on the machine. A 2375 open to the Internet is a machine given away; there are bots continuously scanning that port, and the usual pattern is mining cryptocurrency on your server for weeks.

If some tool insists on the API over TCP, the right answer is a local SSH tunnel, not opening the port:

ss -lntp | grep -E ':237[56]'    # this should not return anything exposed
ssh -N -L 2375:/var/run/docker.sock deploy@aurora-prod &
DOCKER_HOST=tcp://127.0.0.1:2375 docker ps

  1. The map of today's alternatives

Tool What it solves Idempotent When to pick it
Your own SSH script Installing and configuring If you write it well 1-2 hosts
cloud-init Configuring on first boot It runs once Ephemeral machines
Terraform / OpenTofu Creating the infrastructure Yes Infra reviewable in Git
Ansible Configuring and maintaining the fleet Yes 3+ long-lived hosts
Packer Building the machine image Yes Autoscaling, fleets
docker context Pointing at existing hosts N/A Operating, not provisioning
Managed services Avoiding the host N/A Not wanting to maintain hosts

They do not compete: they stack. A healthy 2026 setup is Terraform creates + cloud-init boots + Ansible maintains + contexts operate.

  1. Direct installation: the idempotent script

The honest version of the generic driver. The key point is that you can run it ten times with the same result.

#!/usr/bin/env bash
# provision-host.sh — installs Docker Engine on Debian/Ubuntu. Idempotent.
set -euo pipefail

command -v docker >/dev/null 2>&1 \
  || curl -fsSL https://get.docker.com | sh   # Engine + compose/buildx plugins

id -u deploy >/dev/null 2>&1 || sudo useradd -m -s /bin/bash deploy
sudo usermod -aG docker deploy

sudo install -d -m 0755 /etc/docker
sudo tee /etc/docker/daemon.json >/dev/null <<'JSON'
{ "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" },
  "live-restore": true }
JSON
sudo systemctl enable --now docker && sudo systemctl reload docker
docker --version && docker compose version

Upside: zero dependencies and anyone can understand it. Limits: it does not control drift between hosts, it does not know which version is on each one, and it grows until it becomes unreadable. From the third host onwards, use Ansible. And one note: get.docker.com is convenient, but it is a curl | sh against the Internet; in production you use the official APT repository with its GPG key pinned, which is what the script does under the hood.

  1. cloud-init: let the machine be born with Docker

cloud-init is the de facto standard in cloud images: it reads a user-data payload on first boot and configures the machine before anyone logs in. It is the most direct replacement for docker-machine create.

#cloud-config
# cloud-config.yaml — Aurora Libros application node
hostname: aurora-app-01
timezone: Europe/Madrid
package_update: true
package_upgrade: true
packages: [ca-certificates, curl, gnupg, ufw, unattended-upgrades, fail2ban]

users:
  - name: deploy
    groups: [sudo]
    shell: /bin/bash
    sudo: "ALL=(ALL) NOPASSWD:ALL"
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... deploy@aurora

write_files:
  - path: /etc/docker/daemon.json
    permissions: "0644"
    content: |
      { "log-driver": "json-file",
        "log-opts": { "max-size": "10m", "max-file": "3" },
        "live-restore": true }
  - path: /etc/apt/apt.conf.d/20auto-upgrades
    content: |
      APT::Periodic::Update-Package-Lists "1";
      APT::Periodic::Unattended-Upgrade "1";

runcmd:   # official repository with a pinned GPG key, never curl | sh
  - install -m 0755 -d /etc/apt/keyrings
  - curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
  - chmod a+r /etc/apt/keyrings/docker.asc
  - echo "deb [signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable" > /etc/apt/sources.list.d/docker.list
  - apt-get update
  - DEBIAN_FRONTEND=noninteractive apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
  - usermod -aG docker deploy        # the group exists once Engine is installed
  - systemctl enable --now docker
  - ufw default deny incoming && ufw allow 22/tcp && ufw allow 443/tcp && ufw --force enable
final_message: "Aurora node ready after $UPTIME seconds"

You pass it as --user-data when creating the instance, and you validate it without spending money with cloud-init schema --config-file cloud-config.yaml; once on the host, cloud-init status --wait waits for it to finish and /var/log/cloud-init-output.log tells you what happened. Its limitation is structural: it runs only once. It is no use for changing something on fifty machines that are already running.

  1. Terraform / OpenTofu: declarative infrastructure

Terraform describes the infrastructure you want and works out the plan to get there. OpenTofu is its open source fork under the Linux Foundation, born after the 2023 license change; for everything you will see here they are interchangeable (tofu instead of terraform).

# main.tf — one Aurora Libros application node
terraform {
  required_providers {
    digitalocean = { source = "digitalocean/digitalocean", version = "~> 2.40" }
  }
}
provider "digitalocean" { token = var.do_token }

resource "digitalocean_droplet" "app" {
  name      = "aurora-app-01"
  image     = "debian-12-x64"
  region    = "fra1"
  size      = "s-2vcpu-4gb"
  ssh_keys  = [digitalocean_ssh_key.deploy.fingerprint]
  user_data = file("${path.module}/cloud-config.yaml")   # ← §11 fits in here
  tags      = ["aurora", "production"]
}

resource "digitalocean_firewall" "app" {
  name        = "aurora-app-fw"
  droplet_ids = [digitalocean_droplet.app.id]
  inbound_rule { protocol = "tcp"  port_range = "22"
                 source_addresses = ["203.0.113.0/24"] }    # the office only
  inbound_rule { protocol = "tcp"  port_range = "443"
                 source_addresses = ["0.0.0.0/0", "::/0"] }
}
terraform init && terraform plan -out=plan.bin && terraform apply plan.bin

terraform plan is the reason this replaces scripts: it tells you what is going to happen before it happens. And since it lives in Git, provisioning gets reviewed in a pull request like any other code. The state file (terraform.tfstate) contains secrets in the clear: it belongs in a remote backend with locking, never in the repository.

  1. Ansible: configuring and maintaining the fleet

Terraform creates; Ansible maintains. It connects over SSH, needs no agent, and its modules are idempotent: you describe the final state and it only acts when it has to.

# inventory.ini
[aurora_app]
aurora-app-01 ansible_host=203.0.113.42
aurora-app-02 ansible_host=203.0.113.43
[aurora_app:vars]
ansible_user=deploy
ansible_ssh_private_key_file=~/.ssh/aurora_deploy
# playbook.yml — installs Docker and deploys the Aurora Libros stack
- name: Prepare and deploy Aurora Libros
  hosts: aurora_app
  become: true
  tasks:
    - name: Docker repository
      ansible.builtin.deb822_repository:
        name: docker
        uris: https://download.docker.com/linux/debian
        suites: bookworm
        components: stable
        signed_by: https://download.docker.com/linux/debian/gpg

    - name: Docker Engine and plugins
      ansible.builtin.apt:
        name: [docker-ce, docker-ce-cli, containerd.io,
               docker-buildx-plugin, docker-compose-plugin, python3-docker]
        state: present
        update_cache: true
      notify: restart docker

    - name: Daemon configuration
      ansible.builtin.copy:
        dest: /etc/docker/daemon.json
        mode: "0644"
        content: '{ "log-driver": "json-file", "log-opts": { "max-size": "10m" } }'
      notify: restart docker

    - name: Stack descriptors
      ansible.builtin.copy: { src: "{{ item }}", dest: /opt/aurora/, mode: "0640" }
      loop: [compose.yaml, compose.prod.yaml]

    - name: Log in to ghcr.io
      community.docker.docker_login:
        registry_url: ghcr.io
        username: auroralibros
        password: "{{ ghcr_token }}"      # encrypted with ansible-vault

    - name: Bring the stack up
      community.docker.docker_compose_v2:
        project_src: /opt/aurora
        files: [compose.yaml, compose.prod.yaml]
        pull: always
        state: present

  handlers:
    - name: restart docker
      ansible.builtin.systemd: { name: docker, state: restarted, enabled: true }
ansible-playbook -i inventory.ini playbook.yml --check   # dry run (= plan)
ansible-playbook -i inventory.ini playbook.yml
ansible-playbook -i inventory.ini playbook.yml --limit aurora-app-02

The ghcr_token goes in encrypted with ansible-vault encrypt_string, never in the clear. And on the second run you will see changed=0: that is idempotency genuinely working.

  1. Packer and managed services

Instead of installing Docker on every boot, Packer builds a machine image (AMI, snapshot) once with everything already in it, and instances boot in seconds. It is the reasoning behind container images, one level further down.

Approach Time until useful Drift between hosts When it pays off
cloud-init on every boot 2-4 min Possible (repositories change) Few hosts
Packer image 15-40 s None: bit-for-bit identical Autoscaling, fleets

The cost is one more pipeline: every patch forces you to rebuild and redistribute the image. With autoscaling, it pays for itself. But the best way to maintain a host is still not to have one:

Option What you manage What disappears Trade-off
Instances with containers The app's configuration Installing Docker There is still a VM
ECS + Fargate The task definition The whole host Tied to the provider
Cloud Run / Container Apps The image and its variables Host and orchestrator Less network control
Managed Kubernetes The manifests (module 6) The control plane The nodes are still yours

For an image like ghcr.io/auroralibros/aurora-api:2.0.0 — stateless, twelve-factor, with probes and graceful shutdown, exactly what you prepared in 06-01 — a Cloud Run-style service is a serious candidate: you hand it an image, a port and some variables. The fact that you can even consider it is a direct consequence of module 6, not a coincidence.

  1. The host as an attack surface

In 05-03 you hardened the container. All of that is useless if the host has gone fourteen months without patches.

Vector Minimum measure Verification
Unpatched kernel and packages unattended-upgrades enabled sudo unattended-upgrade --dry-run -d
More open ports than necessary Deny by default sudo ufw status verbose
Exposed Docker API Never 2375/2376 in public ss -lntp | grep 237
SSH with passwords or brute force PasswordAuthentication no, fail2ban sudo sshd -T | grep -i password
Keys that never expire Documented annual rotation Audit authorized_keys
The docker group = root Only the deployment user getent group docker
Disk filled with logs and images Rotation + scheduled prune docker system df

And a warning that is not technical: agree in writing who maintains those machines. The question "who applies the kernel patches on the production host?" must have a person's name as its answer before the first customer arrives. If there is an infrastructure team or a systems administrator, host ownership, the maintenance window and the access procedure are agreed with them; if there is not, you are the one responsible, and it is worth writing down so nobody discovers it in the middle of an incident.

  1. Your own hosts or managed? Decision criteria

Criterion Points to your own hosts Points to managed
Team size and on-call There is a systems person and 24×7 shifts Developers only, no on-call
Cost at scale High, constant and predictable traffic Irregular or low traffic
Regulatory compliance Data that must sit on your hardware The provider's guarantees are enough
Need for control Specific kernel, GPU, network or disks The application and little else
Vendor lock-in A serious concern Acceptable
Location Places with no managed service Standard regions
Time to production You have weeks You need days

For Aurora Libros in 2026 the reasonable answer is a mix: a managed database — nobody wants to be the person responsible for PostgreSQL point-in-time recovery at three in the morning — and the application on your own hosts or on managed Kubernetes depending on the size. Which is exactly the decision the next lesson analyzes.

Common Mistakes and Tips

  • Following a tutorial that uses docker-machine in 2026. Archived since 2021; if the tutorial uses it, everything else in it has expired too.
  • Exposing 2375 "just for a moment, to test something". There are bots permanently scanning that port. An open daemon is root handed over for free, and the mining starts within minutes.
  • Confusing the active context with your working machine. Running docker context use aurora-prod and forgetting about it is the recipe for a compose down in production. Always use an explicit --context.
  • Building images on the production host. The build context travels over SSH, the cache is not reused and the server burns CPU on something that belongs to the pipeline. Publish and pull.
  • Non-idempotent scripts. If running it twice breaks something, that is not provisioning: it is a postponed accident.
  • Keeping terraform.tfstate in Git or secrets in the user-data. Both are readable: the first by anyone with access to the repository, the second from the metadata service. Remote backend with locking, and a secrets manager.
  • Tip: one context per environment with a clear description, and default always active. The friction of typing --context is deliberate.
  • Tip: start with cloud-init even if you only have one host; the day you need a second one, it is already solved. And document in the README who maintains each host, with which patching window and who gets notified: that page is worth more than any script.

Exercises

Exercise 1 — A context and a remote audit. Create an aurora-lab context pointing at a host over SSH (a local VM or another laptop will do). Without ever activating it as the default context, check the remote daemon's version, list its containers, verify that ports 2375 and 2376 are not listening, and show how much disk Docker is using. Write it as audit-host.sh, which must return exit code 1 if it detects the daemon exposed and 2 if it cannot reach the host.

Exercise 2 — cloud-init for an Aurora Libros node. Write a cloud-config.yaml that prepares a node with: a deploy user with your public key and in the docker group, Docker Engine from the official repository, log rotation at 10 MB and 3 files, a firewall that only allows 22 and 443, SSH without passwords, unattended upgrades, and /opt/aurora ready for the compose.yaml. Validate the syntax without booting a single machine.

Exercise 3 — A reasoned decision. Aurora Libros is opening up the Mexican market and needs a replica of the platform. The facts: four developers with nobody on systems, no night-time on-call, around 40 requests per second with ×8 peaks during campaigns, 30 days of database backups, and management insists on being able to switch providers "if prices go up". Write a recommendation with the chosen option, two alternatives you rejected with the reason, and the three questions you would ask the infrastructure lead before executing anything.

Solutions

Solution 1.

docker context create aurora-lab --description "Module 7 lab" \
  --docker "host=ssh://[email protected]"
#!/usr/bin/env bash
# audit-host.sh — basic audit of a remote Docker host
set -uo pipefail
CTX="${1:-aurora-lab}"; FAILED=0

docker --context "$CTX" version --format 'Server: {{.Server.Version}} ({{.Server.Arch}})' \
  || { echo "ERROR: the daemon is unreachable"; exit 2; }
docker --context "$CTX" ps --format '{{.Names}}\t{{.Image}}\t{{.Status}}'
docker --context "$CTX" system df

HOST=$(docker context inspect "$CTX" --format '{{.Endpoints.docker.Host}}' | sed -E 's#ssh://##')
if ssh "$HOST" 'ss -lntp 2>/dev/null | grep -E ":(2375|2376)\b"'; then
  echo "CRITICAL: the Docker API is listening on TCP"; FAILED=1
else
  echo "OK: the daemon is not listening on 2375/2376"
fi
exit "$FAILED"

Key points: an explicit --context on every command (never use); docker context inspect --format derives the host without duplicating configuration; and the exit codes distinguish "I cannot reach it" (2) from "I reach it and it is misconfigured" (1), which is what lets you put the script in a cron job or a pipeline.

Solution 2.

Starting from the file in §11, you need to add three blocks and one line:

#cloud-config
hostname: aurora-mx-01
# ... users, packages, write_files and runcmd from §11 ...

write_files:
  - path: /etc/ssh/sshd_config.d/99-aurora.conf     # ← SSH without passwords
    content: |
      PasswordAuthentication no
      PermitRootLogin no

runcmd:
  # ... Engine installation identical to §11 ...
  - usermod -aG docker deploy                       # after installing Engine
  - install -d -o deploy -g deploy -m 0750 /opt/aurora   # ← destination for the compose
  - ufw default deny incoming && ufw allow 22/tcp && ufw allow 443/tcp && ufw --force enable
  - systemctl restart ssh
cloud-init schema --config-file cloud-config.yaml
# Valid schema cloud-config.yaml

Three details make the difference: the usermod -aG docker deploy goes in runcmd and after installing Engine, because before that the group does not exist; ufw denies inbound by default and only opens 22 and 443, never 2375/2376; and there is not a single secret in the file, because the user-data is readable from the metadata service by anyone who gets onto the machine.

Solution 3. Chosen option: the application on managed Kubernetes (or Cloud Run) + managed PostgreSQL in the Mexican region.

Fact from the brief Implication
Four developers, nobody on systems Nobody can patch hosts or respond at night
No night-time on-call A self-managed host down at 3:00 has no owner
40 req/s with ×8 peaks Demands real autoscaling; the HPA from 06-06 is already written
30 days of backups Managed point-in-time recovery avoids a role that does not exist
"Being able to switch providers" Pushes towards Kubernetes + an OCI image, not proprietary services

Rejected: (a) your own hosts with Compose, which would cope with 40 req/s but would force manual scaling at the worst time of the year and would leave the operating system without an owner; (b) Fargate or App Service, which solve the operational side but collide with the portability requirement, whereas Kubernetes keeps the module 6 manifests almost as they are.

Questions to ask first: who is the declared owner of the Mexican platform and what is their maintenance window?; is there a legal data residency requirement that prevents replicating to Europe?; what is the maximum monthly budget, and would they rather pay more than hire a systems person? The third one usually decides in practice, and it is not a technical question.

Conclusion

You have closed the gap that was left underneath the platform: the machines. You know what Docker Machine was, what it solved with its drivers and its eval $(docker-machine env), and why it has been archived since 2021; if you see it in a tutorial or in a legacy script, you know how to read it and you know how to retire it.

What survived is in your hands: contexts. You created aurora-prod over SSH, checked the remote daemon without opening a session, deployed with docker --context aurora-prod compose up -d and understood the three traps — volumes that resolve on the server, build contexts that travel over the network, and ports that get published where you are not looking — along with the rule that follows from them: in production you do not build, you pull a signed image. And you are clear about why ssh:// beats tcp://, and why an open 2375 is not an oversight but a machine given away.

On the modern side you have the division of roles down: cloud-init so the host is born configured, Terraform or OpenTofu to create it declaratively and reviewably in a pull request, Ansible to keep the fleet free of drift and deploy the stack with idempotent modules, Packer when boot time has to be measured in seconds, and managed services for when the best way to look after a host is not to have one. With the criteria table you can defend your choice with arguments instead of taste. And you take away something that is not a command: the host is an attack surface — it gets patched, firewalled and audited — and somebody with a first and last name has to be responsible for it before the first customer arrives.

In the next lesson we put the two ways of deploying that you already know how to use head to head: Docker Compose and Kubernetes. Not as a duel, but as a decision with criteria, with exact equivalences between both formats and an honest answer to the question almost nobody asks out loud: what does it really cost to maintain a cluster.

Docker: From Beginner to Advanced

Module 1: Introduction to Docker

Module 2: Working with Docker Images

Module 3: Docker Containers

Module 4: Docker Compose

Module 5: Advanced Docker Concepts

Module 6: Docker in Production

Module 7: Docker Ecosystem and Tools

© Copyright 2026. All rights reserved