[{"data":1,"prerenderedAt":533},["ShallowReactive",2],{"mdc--1yql48-key":3,"mdc--odixd2-key":27,"mdc--5yjb6c-key":63,"mdc-lmsftr-key":114,"mdc--1kxzfu-key":161,"mdc-fgyx44-key":315,"mdc--469zfr-key":400,"mdc-y5tfjb-key":412,"mdc-tgunk2-key":506,"mdc--lajt2l-key":516},{"data":4,"body":5},{},{"type":6,"children":7},"root",[8,16],{"type":9,"tag":10,"props":11,"children":12},"element","p",{},[13],{"type":14,"value":15},"text","Everything so far has been one container at a time, typed by hand. A real application is four containers with a network, two volumes, an ordering constraint, and eleven flags nobody will remember tomorrow.",{"type":9,"tag":10,"props":17,"children":18},{},[19,25],{"type":9,"tag":20,"props":21,"children":22},"strong",{},[23],{"type":14,"value":24},"Compose",{"type":14,"value":26}," is that written down. One file, one command, reproducible.",{"data":28,"body":29},{},{"type":6,"children":30},[31,38,50],{"type":9,"tag":32,"props":33,"children":35},"h2",{"id":34},"the-file",[36],{"type":14,"value":37},"The file",{"type":9,"tag":10,"props":39,"children":40},{},[41,48],{"type":9,"tag":42,"props":43,"children":45},"code",{"className":44},[],[46],{"type":14,"value":47},"compose.yaml",{"type":14,"value":49}," in your project root:",{"type":9,"tag":51,"props":52,"children":58},"pre",{"className":53,"code":55,"language":56,"meta":57},[54],"language-yaml","services:\n  api:\n    build: .\n    ports:\n      - \"3000:3000\"\n    environment:\n      DATABASE_URL: postgres://app:secret@db:5432/app\n    depends_on:\n      db:\n        condition: service_healthy\n\n  db:\n    image: postgres:17-alpine\n    environment:\n      POSTGRES_USER: app\n      POSTGRES_PASSWORD: secret\n      POSTGRES_DB: app\n    volumes:\n      - pgdata:/var/lib/postgresql/data\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U app\"]\n      interval: 5s\n      timeout: 3s\n      retries: 5\n\nvolumes:\n  pgdata:\n","yaml","",[59],{"type":9,"tag":42,"props":60,"children":61},{"__ignoreMap":57},[62],{"type":14,"value":55},{"data":64,"body":65},{},{"type":6,"children":66},[67,72],{"type":9,"tag":68,"props":69,"children":71},"terminal-teaser",{":lines":70},"[{\"cmd\":\"docker compose up -d\",\"out\":\"[+] Running 4/4\\n ✔ Network myapp_default  Created\\n ✔ Volume myapp_pgdata    Created\\n ✔ Container myapp-db-1   Healthy\\n ✔ Container myapp-api-1  Started\"},{\"cmd\":\"docker compose ps\",\"out\":\"NAME           IMAGE               STATUS\\nmyapp-api-1    myapp-api           Up 6 seconds\\nmyapp-db-1     postgres:17-alpine  Up 12 seconds (healthy)\"}]",[],{"type":9,"tag":10,"props":73,"children":74},{},[75,77,82,84,90,92,98,100,105,107,112],{"type":14,"value":76},"Read what it did without being asked. It ",{"type":9,"tag":20,"props":78,"children":79},{},[80],{"type":14,"value":81},"created a network",{"type":14,"value":83}," — a user-defined one, so ",{"type":9,"tag":42,"props":85,"children":87},{"className":86},[],[88],{"type":14,"value":89},"api",{"type":14,"value":91}," can reach ",{"type":9,"tag":42,"props":93,"children":95},{"className":94},[],[96],{"type":14,"value":97},"db",{"type":14,"value":99}," by name, which is the DNS problem from the last lesson solved by default. It ",{"type":9,"tag":20,"props":101,"children":102},{},[103],{"type":14,"value":104},"created the volume",{"type":14,"value":106},". It ",{"type":9,"tag":20,"props":108,"children":109},{},[110],{"type":14,"value":111},"waited for Postgres to report healthy",{"type":14,"value":113}," before starting the API. Four things you would otherwise have typed, in order, correctly, every time.",{"data":115,"body":116},{},{"type":6,"children":117},[118,132,137,146,156],{"type":9,"tag":32,"props":119,"children":121},{"id":120},"there-is-no-version-key",[122,124,130],{"type":14,"value":123},"There is no ",{"type":9,"tag":42,"props":125,"children":127},{"className":126},[],[128],{"type":14,"value":129},"version:",{"type":14,"value":131}," key",{"type":9,"tag":10,"props":133,"children":134},{},[135],{"type":14,"value":136},"If you have seen Compose files before, you have seen this at the top:",{"type":9,"tag":51,"props":138,"children":141},{"className":139,"code":140,"language":56,"meta":57},[54],"version: \"3.8\"     # obsolete — delete it\n",[142],{"type":9,"tag":42,"props":143,"children":144},{"__ignoreMap":57},[145],{"type":14,"value":140},{"type":9,"tag":10,"props":147,"children":148},{},[149,154],{"type":9,"tag":20,"props":150,"children":151},{},[152],{"type":14,"value":153},"Do not write it.",{"type":14,"value":155}," That key belonged to Compose v1's file-format versioning, which was retired years ago. The current Compose Specification has no version field; the file format is versionless and features are detected from what you actually use. Modern Compose ignores the key, and older versions of Compose v2 warn about it.",{"type":9,"tag":10,"props":157,"children":158},{},[159],{"type":14,"value":160},"Its presence in a tutorial is the most reliable single indicator that the tutorial predates roughly everything in this course.",{"data":162,"body":163},{},{"type":6,"children":164},[165],{"type":9,"tag":166,"props":167,"children":169},"deep-dive",{"title":168},"Why the CLI jumped from v2 to v5",[170,175,225,275,302],{"type":9,"tag":10,"props":171,"children":172},{},[173],{"type":14,"value":174},"Docker Compose v5.0.0 shipped on 2 December 2025, and the version number is a deliberate piece of communication.",{"type":9,"tag":10,"props":176,"children":177},{},[178,180,186,188,194,196,202,204,209,211,215,217,223],{"type":14,"value":179},"There was no v3 and no v4. The project skipped both — explicitly, in the release notes — because the ",{"type":9,"tag":181,"props":182,"children":183},"em",{},[184],{"type":14,"value":185},"file format",{"type":14,"value":187}," had legacy versions numbered ",{"type":9,"tag":42,"props":189,"children":191},{"className":190},[],[192],{"type":14,"value":193},"2.x",{"type":14,"value":195}," and ",{"type":9,"tag":42,"props":197,"children":199},{"className":198},[],[200],{"type":14,"value":201},"3.x",{"type":14,"value":203},", and a ",{"type":9,"tag":20,"props":205,"children":206},{},[207],{"type":14,"value":208},"CLI",{"type":14,"value":210}," called v3 would have been permanently confused with a ",{"type":9,"tag":20,"props":212,"children":213},{},[214],{"type":14,"value":185},{"type":14,"value":216}," called ",{"type":9,"tag":42,"props":218,"children":220},{"className":219},[],[221],{"type":14,"value":222},"version: \"3\"",{"type":14,"value":224},". Skipping to 5 severs the association.",{"type":9,"tag":10,"props":226,"children":227},{},[228,230,235,237,243,245,250,252,258,260,265,267,273],{"type":14,"value":229},"So when you read a version number now: ",{"type":9,"tag":20,"props":231,"children":232},{},[233],{"type":14,"value":234},"v1",{"type":14,"value":236}," was the Python ",{"type":9,"tag":42,"props":238,"children":240},{"className":239},[],[241],{"type":14,"value":242},"docker-compose",{"type":14,"value":244}," (end of life July 2023), ",{"type":9,"tag":20,"props":246,"children":247},{},[248],{"type":14,"value":249},"v2",{"type":14,"value":251}," was the Go rewrite as ",{"type":9,"tag":42,"props":253,"children":255},{"className":254},[],[256],{"type":14,"value":257},"docker compose",{"type":14,"value":259},", and ",{"type":9,"tag":20,"props":261,"children":262},{},[263],{"type":14,"value":264},"v5",{"type":14,"value":266}," is the current line. ",{"type":9,"tag":42,"props":268,"children":270},{"className":269},[],[271],{"type":14,"value":272},"version: \"3.8\"",{"type":14,"value":274}," was never a program at all — it was a schema declaration, and it is dead.",{"type":9,"tag":10,"props":276,"children":277},{},[278,280,285,287,293,295,300],{"type":14,"value":279},"The other headline change in v5.0.0: ",{"type":9,"tag":20,"props":281,"children":282},{},[283],{"type":14,"value":284},"Compose no longer has an internal builder.",{"type":14,"value":286}," Builds are delegated to Docker Bake, the same path ",{"type":9,"tag":42,"props":288,"children":290},{"className":289},[],[291],{"type":14,"value":292},"docker build",{"type":14,"value":294}," takes. In practice this means Compose builds and CLI builds behave identically, and Bake's features are available to a Compose project — the subject of a lesson in ",{"type":9,"tag":181,"props":296,"children":297},{},[298],{"type":14,"value":299},"Docker in Practice",{"type":14,"value":301},".",{"type":9,"tag":10,"props":303,"children":304},{},[305,307,313],{"type":14,"value":306},"Later v5 releases have concentrated on reconciliation — how Compose decides what to change when you re-run ",{"type":9,"tag":42,"props":308,"children":310},{"className":309},[],[311],{"type":14,"value":312},"up",{"type":14,"value":314}," against something already running. v5.2 rewrote the algorithm, v5.4 extended it to volumes and networks, and v5.5 stopped unnecessary container recreation caused by digest churn. If you have upgraded from v2 and containers restart more or less often than you expect, that is what changed.",{"data":316,"body":317},{},{"type":6,"children":318},[319,325,335,379],{"type":9,"tag":32,"props":320,"children":322},{"id":321},"the-commands-worth-knowing",[323],{"type":14,"value":324},"The commands worth knowing",{"type":9,"tag":51,"props":326,"children":330},{"className":327,"code":329,"language":14},[328],"language-text","docker compose up -d              # create/start everything, detached\ndocker compose down               # stop and remove containers + network\ndocker compose down -v            # ...and delete the volumes. Careful.\ndocker compose ps                 # what is running\ndocker compose logs -f api        # follow one service\ndocker compose exec api sh        # shell into a running service\ndocker compose run --rm api npm test   # one-off container, then delete it\ndocker compose build              # rebuild images\ndocker compose pull               # refresh images from the registry\n",[331],{"type":9,"tag":42,"props":332,"children":333},{"__ignoreMap":57},[334],{"type":14,"value":329},{"type":9,"tag":10,"props":336,"children":337},{},[338,340,346,347,353,355,365,367,377],{"type":14,"value":339},"The two that get mixed up are ",{"type":9,"tag":42,"props":341,"children":343},{"className":342},[],[344],{"type":14,"value":345},"exec",{"type":14,"value":195},{"type":9,"tag":42,"props":348,"children":350},{"className":349},[],[351],{"type":14,"value":352},"run",{"type":14,"value":354},". ",{"type":9,"tag":20,"props":356,"children":357},{},[358,363],{"type":9,"tag":42,"props":359,"children":361},{"className":360},[],[362],{"type":14,"value":345},{"type":14,"value":364}," needs the service already running",{"type":14,"value":366}," and joins it. ",{"type":9,"tag":20,"props":368,"children":369},{},[370,375],{"type":9,"tag":42,"props":371,"children":373},{"className":372},[],[374],{"type":14,"value":352},{"type":14,"value":376}," starts a new container",{"type":14,"value":378}," from the same definition — useful for tests, migrations, and shells against a service that is currently crashing.",{"type":9,"tag":10,"props":380,"children":381},{},[382,384,390,392,398],{"type":14,"value":383},"And ",{"type":9,"tag":42,"props":385,"children":387},{"className":386},[],[388],{"type":14,"value":389},"down -v",{"type":14,"value":391}," deletes your named volumes. Plain ",{"type":9,"tag":42,"props":393,"children":395},{"className":394},[],[396],{"type":14,"value":397},"down",{"type":14,"value":399}," does not. That one flag is the difference between \"stop the stack\" and \"stop the stack and destroy the database\".",{"data":401,"body":402},{},{"type":6,"children":403},[404],{"type":9,"tag":405,"props":406,"children":411},"quiz",{":answer":407,":options":408,"explanation":409,"question":410},"0","[\"`-v` also deletes the named volumes, and with them your data\",\"`-v` is verbose output\",\"`-v` removes the images as well as the containers\"]","Plain `down` removes containers and the network but leaves volumes, so `up` again finds your database intact. `-v` removes them too. It is the right command for a clean slate and the wrong one at any other time.","What is the difference between `docker compose down` and `docker compose down -v`?",[],{"data":413,"body":414},{},{"type":6,"children":415},[416,428,437,463,468,477],{"type":9,"tag":32,"props":417,"children":419},{"id":418},"depends_on-and-what-it-actually-promises",[420,426],{"type":9,"tag":42,"props":421,"children":423},{"className":422},[],[424],{"type":14,"value":425},"depends_on",{"type":14,"value":427}," and what it actually promises",{"type":9,"tag":51,"props":429,"children":432},{"className":430,"code":431,"language":56,"meta":57},[54],"depends_on:\n  - db\n",[433],{"type":9,"tag":42,"props":434,"children":435},{"__ignoreMap":57},[436],{"type":14,"value":431},{"type":9,"tag":10,"props":438,"children":439},{},[440,442,447,449,454,456,461],{"type":14,"value":441},"This controls ",{"type":9,"tag":20,"props":443,"children":444},{},[445],{"type":14,"value":446},"start order only",{"type":14,"value":448},". Compose starts ",{"type":9,"tag":42,"props":450,"children":452},{"className":451},[],[453],{"type":14,"value":97},{"type":14,"value":455}," first, then immediately starts ",{"type":9,"tag":42,"props":457,"children":459},{"className":458},[],[460],{"type":14,"value":89},{"type":14,"value":462}," — it does not wait for Postgres to be ready to accept connections, because it has no way of knowing what \"ready\" means. Your API tries to connect a hundred milliseconds later and crashes.",{"type":9,"tag":10,"props":464,"children":465},{},[466],{"type":14,"value":467},"The long form fixes it, and requires the dependency to declare a healthcheck:",{"type":9,"tag":51,"props":469,"children":472},{"className":470,"code":471,"language":56,"meta":57},[54],"depends_on:\n  db:\n    condition: service_healthy\n",[473],{"type":9,"tag":42,"props":474,"children":475},{"__ignoreMap":57},[476],{"type":14,"value":471},{"type":9,"tag":10,"props":478,"children":479},{},[480,482,488,490,496,498,504],{"type":14,"value":481},"Three conditions are available: ",{"type":9,"tag":42,"props":483,"children":485},{"className":484},[],[486],{"type":14,"value":487},"service_started",{"type":14,"value":489}," (the old behaviour), ",{"type":9,"tag":42,"props":491,"children":493},{"className":492},[],[494],{"type":14,"value":495},"service_healthy",{"type":14,"value":497}," (waits for the healthcheck to pass), and ",{"type":9,"tag":42,"props":499,"children":501},{"className":500},[],[502],{"type":14,"value":503},"service_completed_successfully",{"type":14,"value":505}," (waits for a one-shot container to exit 0 — migrations, seed jobs).",{"data":507,"body":508},{},{"type":6,"children":509},[510],{"type":9,"tag":405,"props":511,"children":515},{":answer":407,":options":512,"explanation":513,"question":514},"[\"Plain `depends_on` only orders startup; it doesn't wait for the database to be ready to accept connections\",\"The database service name is wrong\",\"`depends_on` doesn't work with the `build` key\"]","Started and ready are different things. Give `db` a `healthcheck` and use `condition: service_healthy`, which is the whole point of the long form.","Your API crashes on startup with \"connection refused\" even though `depends_on: [db]` is set. Why?",[],{"data":517,"body":518},{},{"type":6,"children":519},[520,528],{"type":9,"tag":521,"props":522,"children":527},"fill-blank",{":answer":523,"hint":524,"placeholder":525,"prompt":526},"[\"docker compose up -d\",\"docker compose up --detach\"]","Three words plus the same detach flag `docker run` uses.","docker compose ...","Start every service in the Compose file in the background.",[],{"type":9,"tag":10,"props":529,"children":530},{},[531],{"type":14,"value":532},"Next up: the last lesson — finding out what went wrong, and stopping Docker from filling your disk.",1787908867650]