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
- The real provisioning problem
- What Docker Machine was and its driver model
- The characteristic commands
- Why it is archived and what to do if you run into it
- What did survive: Docker contexts
- An
aurora-prodcontext over SSH - Deploying against a remote host without switching terminals
- SSH versus exposing the daemon over TCP
- The map of today's alternatives
- Direct installation: the idempotent script
- cloud-init: let the machine be born with Docker
- Terraform / OpenTofu: declarative infrastructure
- Ansible: configuring and maintaining the fleet
- Packer and managed services
- The host as an attack surface
- Your own hosts or managed? Decision criteria
- 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.
- 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.
- 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 providerThe 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.
- 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.
- 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.
- An
aurora-prod context over SSH
aurora-prod context over SSHAurora 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 yesThe 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.
- 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 apiThree 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.
- 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
- 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.
- 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 versionUpside: 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.
- 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.
- 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 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.
- 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-02The 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.
- 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.
- 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.
- 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-machinein 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-prodand forgetting about it is the recipe for acompose downin 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.tfstatein Git or secrets in theuser-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
defaultalways active. The friction of typing--contextis 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
READMEwho 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 sshThree 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
- What Is Docker?
- Installing Docker
- Docker Architecture
- Basic Docker Commands
- Understanding Docker Images
- Creating Your First Docker Container
- The Course Project: The Aurora Libros Platform
Module 2: Working with Docker Images
- Docker Hub and Repositories
- Building Docker Images
- Dockerfile Basics
- Advanced Dockerfile Instructions
- Managing Docker Images
- Tagging and Publishing Images
Module 3: Docker Containers
- Running Containers
- Container Lifecycle
- Managing Containers
- Inspecting and Debugging Containers
- Docker Networking
- Data Persistence with Volumes
- Resource Limits and Restart Policies
Module 4: Docker Compose
- Introduction to Docker Compose
- Defining Services in Docker Compose
- Docker Compose Commands
- Multi-Container Applications
- Environment Variables in Docker Compose
- Profiles, Overrides and Multiple Environments
- Local Development with Docker Compose
Module 5: Advanced Docker Concepts
- Docker Networking Deep Dive
- Docker Storage Options
- Docker Security Best Practices
- Optimizing Docker Images
- Advanced Builds with BuildKit and Buildx
- Logging and Monitoring in Docker
- The Runtime Inside: Namespaces, Cgroups and Layers
Module 6: Docker in Production
- Preparing an Image for Production
- CI/CD with Docker
- Orchestrating Containers with Docker Swarm
- Introduction to Kubernetes
- Deploying Docker Containers in Kubernetes
- Scaling and Load Balancing
- Deployment Strategies and Rollback
