[{"data":1,"prerenderedAt":594},["ShallowReactive",2],{"mdc--rrlpo1-key":3,"mdc-rrf0vr-key":21,"mdc-51hgqu-key":171,"mdc-lxznnt-key":183,"mdc--itu508-key":244,"mdc--dji8be-key":293,"mdc--lvn8vj-key":381,"mdc-85xywu-key":391,"mdc-gs0j23-key":482,"mdc--354k0n-key":494},{"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","The Compose file from the Beginner course runs two services. A real project has a dozen, of which four should not start on a laptop, and needs different settings in CI than in development without maintaining two files that drift apart.",{"type":9,"tag":10,"props":17,"children":18},{},[19],{"type":14,"value":20},"Compose has specific answers for all of that.",{"data":22,"body":23},{},{"type":6,"children":24},[25,32,60,70,83,93,102,107,117,162],{"type":9,"tag":26,"props":27,"children":29},"h2",{"id":28},"overrides-several-files-merged",[30],{"type":14,"value":31},"Overrides: several files, merged",{"type":9,"tag":10,"props":33,"children":34},{},[35,42,44,50,52,58],{"type":9,"tag":36,"props":37,"children":39},"code",{"className":38},[],[40],{"type":14,"value":41},"docker compose",{"type":14,"value":43}," reads ",{"type":9,"tag":36,"props":45,"children":47},{"className":46},[],[48],{"type":14,"value":49},"compose.yaml",{"type":14,"value":51}," and, if it exists, ",{"type":9,"tag":36,"props":53,"children":55},{"className":54},[],[56],{"type":14,"value":57},"compose.override.yaml",{"type":14,"value":59}," on top of it — automatically, with no flags.",{"type":9,"tag":10,"props":61,"children":62},{},[63,68],{"type":9,"tag":36,"props":64,"children":66},{"className":65},[],[67],{"type":14,"value":49},{"type":14,"value":69}," — what is always true:",{"type":9,"tag":71,"props":72,"children":78},"pre",{"className":73,"code":75,"language":76,"meta":77},[74],"language-yaml","services:\n  api:\n    build: .\n    environment:\n      DATABASE_URL: postgres://app:secret@db:5432/app\n    depends_on:\n      db:\n        condition: service_healthy\n","yaml","",[79],{"type":9,"tag":36,"props":80,"children":81},{"__ignoreMap":77},[82],{"type":14,"value":75},{"type":9,"tag":10,"props":84,"children":85},{},[86,91],{"type":9,"tag":36,"props":87,"children":89},{"className":88},[],[90],{"type":14,"value":57},{"type":14,"value":92}," — development only, and typically gitignored or committed as the local default:",{"type":9,"tag":71,"props":94,"children":97},{"className":95,"code":96,"language":76,"meta":77},[74],"services:\n  api:\n    ports:\n      - \"127.0.0.1:3000:3000\"\n    environment:\n      LOG_LEVEL: debug\n    volumes:\n      - ./src:/app/src\n",[98],{"type":9,"tag":36,"props":99,"children":100},{"__ignoreMap":77},[101],{"type":14,"value":96},{"type":9,"tag":10,"props":103,"children":104},{},[105],{"type":14,"value":106},"Name files explicitly and the automatic override is skipped:",{"type":9,"tag":71,"props":108,"children":112},{"className":109,"code":111,"language":14},[110],"language-text","docker compose -f compose.yaml -f compose.prod.yaml up -d\n",[113],{"type":9,"tag":36,"props":114,"children":115},{"__ignoreMap":77},[116],{"type":14,"value":111},{"type":9,"tag":10,"props":118,"children":119},{},[120,122,128,130,136,138,144,146,152,154,160],{"type":14,"value":121},"The merge rules are worth knowing because one of them surprises people: ",{"type":9,"tag":123,"props":124,"children":125},"strong",{},[126],{"type":14,"value":127},"scalars are replaced, sequences are appended",{"type":14,"value":129},". Two files that both define ",{"type":9,"tag":36,"props":131,"children":133},{"className":132},[],[134],{"type":14,"value":135},"ports:",{"type":14,"value":137}," give you ",{"type":9,"tag":139,"props":140,"children":141},"em",{},[142],{"type":14,"value":143},"both",{"type":14,"value":145}," lists concatenated, not the second one winning. Use ",{"type":9,"tag":36,"props":147,"children":149},{"className":148},[],[150],{"type":14,"value":151},"!reset",{"type":14,"value":153}," to clear an inherited value, or ",{"type":9,"tag":36,"props":155,"children":157},{"className":156},[],[158],{"type":14,"value":159},"!override",{"type":14,"value":161}," to replace a list wholesale:",{"type":9,"tag":71,"props":163,"children":166},{"className":164,"code":165,"language":76,"meta":77},[74],"services:\n  api:\n    ports: !override\n      - \"8080:3000\"\n",[167],{"type":9,"tag":36,"props":168,"children":169},{"__ignoreMap":77},[170],{"type":14,"value":165},{"data":172,"body":173},{},{"type":6,"children":174},[175],{"type":9,"tag":176,"props":177,"children":182},"quiz",{":answer":178,":options":179,"explanation":180,"question":181},"0","[\"Both lists concatenated — sequences append rather than replace\",\"Only the second file's list\",\"An error about a duplicate key\"]","Scalars are overwritten, lists are appended. This is how you add a port in an override without restating the base ones — and how you accidentally publish two ports. `!override` replaces the list instead.","Two Compose files both define `ports:` for the same service. What does the merged result contain?",[],{"data":184,"body":185},{},{"type":6,"children":186},[187,193,202,222,231],{"type":9,"tag":26,"props":188,"children":190},{"id":189},"profiles-services-that-stay-off",[191],{"type":14,"value":192},"Profiles: services that stay off",{"type":9,"tag":71,"props":194,"children":197},{"className":195,"code":196,"language":76,"meta":77},[74],"services:\n  api:\n    build: .\n\n  db:\n    image: postgres:17-alpine\n\n  admin:\n    image: adminer\n    profiles: [tools]\n\n  loadtest:\n    build: ./bench\n    profiles: [bench]\n",[198],{"type":9,"tag":36,"props":199,"children":200},{"__ignoreMap":77},[201],{"type":14,"value":196},{"type":9,"tag":10,"props":203,"children":204},{},[205,207,213,215,220],{"type":14,"value":206},"A service with a ",{"type":9,"tag":36,"props":208,"children":210},{"className":209},[],[211],{"type":14,"value":212},"profiles",{"type":14,"value":214}," key does ",{"type":9,"tag":123,"props":216,"children":217},{},[218],{"type":14,"value":219},"not",{"type":14,"value":221}," start unless its profile is requested:",{"type":9,"tag":71,"props":223,"children":226},{"className":224,"code":225,"language":14},[110],"docker compose up -d                        # api and db only\ndocker compose --profile tools up -d        # ...plus adminer\nCOMPOSE_PROFILES=tools,bench docker compose up -d\n",[227],{"type":9,"tag":36,"props":228,"children":229},{"__ignoreMap":77},[230],{"type":14,"value":225},{"type":9,"tag":10,"props":232,"children":233},{},[234,236,242],{"type":14,"value":235},"This is how one file serves a team without everyone running the observability stack, the seed job, and the admin UI on a laptop. Naming a profiled service directly (",{"type":9,"tag":36,"props":237,"children":239},{"className":238},[],[240],{"type":14,"value":241},"docker compose up admin",{"type":14,"value":243},") also activates it.",{"data":245,"body":246},{},{"type":6,"children":247},[248,260,279,288],{"type":9,"tag":26,"props":249,"children":251},{"id":250},"include-composing-the-compose-file",[252,258],{"type":9,"tag":36,"props":253,"children":255},{"className":254},[],[256],{"type":14,"value":257},"include",{"type":14,"value":259},": composing the compose file",{"type":9,"tag":10,"props":261,"children":262},{},[263,265,270,272,277],{"type":14,"value":264},"Large projects split services across files, and ",{"type":9,"tag":36,"props":266,"children":268},{"className":267},[],[269],{"type":14,"value":257},{"type":14,"value":271}," pulls another complete Compose file into this one — paths and all resolved relative to ",{"type":9,"tag":139,"props":273,"children":274},{},[275],{"type":14,"value":276},"its",{"type":14,"value":278}," directory, which is what makes it different from a merge:",{"type":9,"tag":71,"props":280,"children":283},{"className":281,"code":282,"language":76,"meta":77},[74],"include:\n  - path: ./infra/observability.yaml\n  - path: ./services/payments/compose.yaml\n\nservices:\n  api:\n    build: .\n",[284],{"type":9,"tag":36,"props":285,"children":286},{"__ignoreMap":77},[287],{"type":14,"value":282},{"type":9,"tag":10,"props":289,"children":290},{},[291],{"type":14,"value":292},"Each included file stays independently runnable, which means a team can own their own service's Compose file and the top-level file stays readable.",{"data":294,"body":295},{},{"type":6,"children":296},[297,303,308,317,331,345,359,372],{"type":9,"tag":26,"props":298,"children":300},{"id":299},"lifecycle-init-containers-and-hooks",[301],{"type":14,"value":302},"Lifecycle: init containers and hooks",{"type":9,"tag":10,"props":304,"children":305},{},[306],{"type":14,"value":307},"Historically, \"run migrations before the app starts\" meant an entrypoint shell script that did two jobs badly. Compose has three keys for it now.",{"type":9,"tag":71,"props":309,"children":312},{"className":310,"code":311,"language":76,"meta":77},[74],"services:\n  api:\n    build: .\n    depends_on:\n      db:\n        condition: service_healthy\n    pre_start:\n      - command: ./manage.py migrate\n    post_start:\n      - command: ./scripts/warm-cache.sh\n        user: root\n    pre_stop:\n      - command: ./scripts/drain.sh\n",[313],{"type":9,"tag":36,"props":314,"children":315},{"__ignoreMap":77},[316],{"type":14,"value":311},{"type":9,"tag":10,"props":318,"children":319},{},[320,329],{"type":9,"tag":123,"props":321,"children":322},{},[323],{"type":9,"tag":36,"props":324,"children":326},{"className":325},[],[327],{"type":14,"value":328},"pre_start",{"type":14,"value":330}," (Compose v5.3) is an init container: it runs to completion before the service's own process starts, in the same environment. Migrations, schema checks, fixture loading.",{"type":9,"tag":10,"props":332,"children":333},{},[334,343],{"type":9,"tag":123,"props":335,"children":336},{},[337],{"type":9,"tag":36,"props":338,"children":340},{"className":339},[],[341],{"type":14,"value":342},"post_start",{"type":14,"value":344}," runs after the container starts, alongside the main process.",{"type":9,"tag":10,"props":346,"children":347},{},[348,357],{"type":9,"tag":123,"props":349,"children":350},{},[351],{"type":9,"tag":36,"props":352,"children":354},{"className":353},[],[355],{"type":14,"value":356},"pre_stop",{"type":14,"value":358}," runs before the container is stopped — connection draining, deregistering from a load balancer.",{"type":9,"tag":10,"props":360,"children":361},{},[362,364,370],{"type":14,"value":363},"The alternative you will still see is a one-shot service plus ",{"type":9,"tag":36,"props":365,"children":367},{"className":366},[],[368],{"type":14,"value":369},"service_completed_successfully",{"type":14,"value":371},", which is the right shape when the job is genuinely separate:",{"type":9,"tag":71,"props":373,"children":376},{"className":374,"code":375,"language":76,"meta":77},[74],"  migrate:\n    build: .\n    command: ./manage.py migrate\n    depends_on:\n      db: { condition: service_healthy }\n\n  api:\n    depends_on:\n      migrate: { condition: service_completed_successfully }\n",[377],{"type":9,"tag":36,"props":378,"children":379},{"__ignoreMap":77},[380],{"type":14,"value":375},{"data":382,"body":383},{},{"type":6,"children":384},[385],{"type":9,"tag":176,"props":386,"children":390},{":answer":178,":options":387,"explanation":388,"question":389},"[\"To completion, before the main process starts — it is an init container\",\"Concurrently with the main process\",\"After the container is healthy\"]","That is the point of it. It replaces the entrypoint script that ran migrations and then exec'd the real command, and it keeps the failure visible instead of buried in a wrapper.","When does a `pre_start` command run relative to the service's main process?",[],{"data":392,"body":393},{},{"type":6,"children":394},[395,407,416],{"type":9,"tag":26,"props":396,"children":398},{"id":397},"pull_policy-and-refresh-windows",[399,405],{"type":9,"tag":36,"props":400,"children":402},{"className":401},[],[403],{"type":14,"value":404},"pull_policy",{"type":14,"value":406}," and refresh windows",{"type":9,"tag":71,"props":408,"children":411},{"className":409,"code":410,"language":76,"meta":77},[74],"services:\n  api:\n    image: ghcr.io/acme/api:main\n    pull_policy: daily\n",[412],{"type":9,"tag":36,"props":413,"children":414},{"__ignoreMap":77},[415],{"type":14,"value":410},{"type":9,"tag":10,"props":417,"children":418},{},[419,421,427,429,435,436,442,444,450,452,458,459,465,466,472,474,480],{"type":14,"value":420},"Beyond ",{"type":9,"tag":36,"props":422,"children":424},{"className":423},[],[425],{"type":14,"value":426},"always",{"type":14,"value":428},", ",{"type":9,"tag":36,"props":430,"children":432},{"className":431},[],[433],{"type":14,"value":434},"never",{"type":14,"value":428},{"type":9,"tag":36,"props":437,"children":439},{"className":438},[],[440],{"type":14,"value":441},"missing",{"type":14,"value":443}," and ",{"type":9,"tag":36,"props":445,"children":447},{"className":446},[],[448],{"type":14,"value":449},"build",{"type":14,"value":451},", Compose accepts time-based policies — ",{"type":9,"tag":36,"props":453,"children":455},{"className":454},[],[456],{"type":14,"value":457},"daily",{"type":14,"value":428},{"type":9,"tag":36,"props":460,"children":462},{"className":461},[],[463],{"type":14,"value":464},"weekly",{"type":14,"value":428},{"type":9,"tag":36,"props":467,"children":469},{"className":468},[],[470],{"type":14,"value":471},"every_12h",{"type":14,"value":473},". A mutable tag gets refreshed on a schedule instead of on every ",{"type":9,"tag":36,"props":475,"children":477},{"className":476},[],[478],{"type":14,"value":479},"up",{"type":14,"value":481}," (slow, and offline-hostile) or never (stale for weeks).",{"data":483,"body":484},{},{"type":6,"children":485},[486],{"type":9,"tag":487,"props":488,"children":493},"fill-blank",{":answer":489,"hint":490,"placeholder":491,"prompt":492},"[\"docker compose --profile tools up -d\",\"docker compose --profile=tools up -d\"]","The profile flag goes before the subcommand, not after it.","docker compose ...","Start the stack in the background with the `tools` profile activated.",[],{"data":495,"body":496},{},{"type":6,"children":497},[498,589],{"type":9,"tag":499,"props":500,"children":502},"deep-dive",{"title":501},"The commands that answer 'what is it actually going to do'",[503,508,537,546,560],{"type":9,"tag":10,"props":504,"children":505},{},[506],{"type":14,"value":507},"Compose files acquire overrides, includes, profiles, and interpolation until nobody can predict the result by reading them. Three commands settle it.",{"type":9,"tag":10,"props":509,"children":510},{},[511,520,522,527,529,535],{"type":9,"tag":123,"props":512,"children":513},{},[514],{"type":9,"tag":36,"props":515,"children":517},{"className":516},[],[518],{"type":14,"value":519},"docker compose config",{"type":14,"value":521}," renders the final, fully-merged, fully-interpolated file. Every override applied, every ",{"type":9,"tag":36,"props":523,"children":525},{"className":524},[],[526],{"type":14,"value":257},{"type":14,"value":528}," inlined, every ",{"type":9,"tag":36,"props":530,"children":532},{"className":531},[],[533],{"type":14,"value":534},"${VAR}",{"type":14,"value":536}," resolved. If a value is not what you expect, this is where you find out, and it is worth running in CI as a validation step — it exits non-zero on a malformed file.",{"type":9,"tag":71,"props":538,"children":541},{"className":539,"code":540,"language":14},[110],"docker compose config\ndocker compose --profile tools config --services\ndocker compose config --variables       # what interpolation is looking for\n",[542],{"type":9,"tag":36,"props":543,"children":544},{"__ignoreMap":77},[545],{"type":14,"value":540},{"type":9,"tag":10,"props":547,"children":548},{},[549,558],{"type":9,"tag":123,"props":550,"children":551},{},[552],{"type":9,"tag":36,"props":553,"children":555},{"className":554},[],[556],{"type":14,"value":557},"docker compose ps --format json",{"type":14,"value":559}," gives machine-readable state, including health, which is what a wait-for-ready script in CI should be reading rather than grepping human output.",{"type":9,"tag":10,"props":561,"children":562},{},[563,572,574,579,581,587],{"type":9,"tag":123,"props":564,"children":565},{},[566],{"type":9,"tag":36,"props":567,"children":569},{"className":568},[],[570],{"type":14,"value":571},"docker compose --dry-run up -d",{"type":14,"value":573}," shows what ",{"type":9,"tag":139,"props":575,"children":576},{},[577],{"type":14,"value":578},"would",{"type":14,"value":580}," happen — which containers get created, recreated, started, or left alone — without touching anything. This became considerably more useful in v5, where the reconciliation algorithm was rewritten twice: if you have upgraded from v2 and containers are recreating more or less than you expect, ",{"type":9,"tag":36,"props":582,"children":584},{"className":583},[],[585],{"type":14,"value":586},"--dry-run",{"type":14,"value":588}," tells you what the new algorithm has decided and why.",{"type":9,"tag":10,"props":590,"children":591},{},[592],{"type":14,"value":593},"Next up: Compose Watch — the development loop where you stop rebuilding by hand.",1787908868121]