{"id":"l-dk-3-4","courseId":"docker-kubernetes","lang":"en","kind":"lesson","summary":"Move notes-api’s configuration into env files and Compose secrets, split dev-only settings into an override file and profiles, and get a fast edit loop with <code dir=\"ltr\">docker compose watch</code>.","html":"<p>The notes-api Compose file now works, and it has three problems teams hit in the second week. The database password sits in plain text in a committed file. Dev-only settings like debug ports are mixed in with what CI needs. And every code change means <code dir=\"ltr\">docker compose up --build</code> and a thirty-second wait. This lesson fixes all three.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Two kinds of environment</h2>\n<p>Compose has two separate mechanisms that both involve “env”, and mixing them up causes most configuration confusion.</p>\n<p><strong>Interpolation</strong> fills in <code dir=\"ltr\">${VAR}</code> placeholders <em>in the Compose file itself</em>. Values come from your shell, then from a <code dir=\"ltr\">.env</code> file in the project directory:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ini\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">.env</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">POSTGRES_PASSWORD</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=change-me-locally</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">API_PORT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=8080</span></span></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">compose.yaml</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">services</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  api</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    ports</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"${API_PORT:-8080}:3000\"</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  db</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    environment</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      POSTGRES_PASSWORD</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}</span></span></code></pre></div>\n<p><code dir=\"ltr\">${API_PORT:-8080}</code> falls back to 8080 if unset. <code dir=\"ltr\">${POSTGRES_PASSWORD:?…}</code> makes the value required: if it’s missing, Compose stops with your message instead of starting Postgres with an empty password.</p>\n<p><strong>Container environment</strong> is what the process inside sees. It comes from <code dir=\"ltr\">environment:</code> and <code dir=\"ltr\">env_file:</code> on the service:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">YAML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  api</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    env_file</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.env.api</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    environment</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      DATABASE_URL</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">postgres://notes:${POSTGRES_PASSWORD}@db:5432/notes</span></span></code></pre></div>\n<p>The project <code dir=\"ltr\">.env</code> is <em>not</em> passed into containers by itself. If a value should reach the app, reference it under <code dir=\"ltr\">environment:</code> or list the file under <code dir=\"ltr\">env_file:</code>. When the same variable comes from both, <code dir=\"ltr\">environment:</code> wins.</p>\n<p>For interpolation, a variable set in your shell beats the same one in <code dir=\"ltr\">.env</code>, so <code dir=\"ltr\">API_PORT=9090 docker compose up -d</code> is a quick one-off override without editing any file. To use a different file entirely, pass <code dir=\"ltr\">--env-file .env.staging</code>. Keep the number of these files small; three slightly different env files is how teams end up debugging configuration instead of code.</p>\n<p>Commit a <code dir=\"ltr\">.env.example</code> with dummy values and add <code dir=\"ltr\">.env</code> to <code dir=\"ltr\">.gitignore</code>. New teammates copy the example and fill it in.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>See what Compose sees</span></p>\n<p><code dir=\"ltr\">docker compose config</code> prints the final configuration after merging files and interpolating variables. When a value isn’t what you expect, run it before anything else. Ten seconds with it has saved me hours of guessing.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">Compose secrets</h2>\n<p>Environment variables are visible in <code dir=\"ltr\">docker inspect</code> and to every process in the container. For passwords, Compose can mount a file instead:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">YAML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">services</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  db</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    image</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">postgres:17</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    environment</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      POSTGRES_PASSWORD_FILE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/run/secrets/db_password</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    secrets</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">db_password</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">secrets</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  db_password</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    file</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">./secrets/db_password.txt</span></span></code></pre></div>\n<p>The secret appears inside the container at <code dir=\"ltr\">/run/secrets/db_password</code>. The Postgres image reads <code dir=\"ltr\">POSTGRES_PASSWORD_FILE</code>, and many official images support the same <code dir=\"ltr\">_FILE</code> convention. For your own app, read the file at startup. Kubernetes Secrets mounted as files work the same way, so the habit transfers.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Base file plus override</h2>\n<p>Compose automatically merges <code dir=\"ltr\">compose.override.yaml</code> on top of <code dir=\"ltr\">compose.yaml</code> if it exists. Use that split on purpose:</p>\n<ul>\n<li><code dir=\"ltr\">compose.yaml</code>: what every environment needs, CI included.</li>\n<li><code dir=\"ltr\">compose.override.yaml</code>: developer conveniences, like published database ports, debug flags and watch rules.</li>\n</ul>\n<p>CI can then run with the base file only: <code dir=\"ltr\">docker compose -f compose.yaml up -d --wait</code>. When you pass <code dir=\"ltr\">-f</code>, the override isn’t loaded automatically.</p>\n<p><strong>Profiles</strong> cover optional services. A pgAdmin container that only some people want gets <code dir=\"ltr\">profiles: [tools]</code>, and starts only with <code dir=\"ltr\">docker compose --profile tools up</code>.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Watch mode: a fast edit loop</h2>\n<p>Rebuilding the image for every edit is slow. Bind-mounting the whole project is fragile: your host’s <code dir=\"ltr\">node_modules</code> (built for macOS) shadows the container’s (built for Linux), and file access across the Docker Desktop VM is slow. <strong>Compose watch</strong> solves both by syncing only the files you choose into the running container.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">compose.override.yaml</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">services</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  api</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    build</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      context</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      target</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">build</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    command</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"npm\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"run\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"dev\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    develop</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      watch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">action</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">sync</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          path</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">./src</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          target</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/app/src</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">action</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">rebuild</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          path</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">package.json</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">action</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">rebuild</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          path</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">package-lock.json</span></span></code></pre></div>\n<p>For development, the override builds the <code dir=\"ltr\">build</code> stage (which has dev dependencies) and runs <code dir=\"ltr\">npm run dev</code>, which in notes-api is <code dir=\"ltr\">tsx watch src/server.ts</code>, a TypeScript runner that restarts on changes. Then:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"5\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">docker</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> compose</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> up</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --watch</span></span></code></pre></div>\n<p>Edit <code dir=\"ltr\">src/routes.ts</code> and the file is copied into <code dir=\"ltr\">/app/src</code> within a second; <code dir=\"ltr\">tsx</code> restarts the server. Change <code dir=\"ltr\">package-lock.json</code> and Compose rebuilds the image and replaces the container, because new dependencies need a real install. A third action, <code dir=\"ltr\">sync+restart</code>, syncs files and restarts the container, which suits config files the app reads only at startup. <code dir=\"ltr\">docker compose watch</code> does the same as <code dir=\"ltr\">up --watch</code> with quieter output.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Shipping the dev setup</span></p>\n<p>The override runs the <code dir=\"ltr\">build</code> stage with npm as PID 1 and a file watcher. That’s fine on a laptop and wrong anywhere else. Keep dev-only settings in <code dir=\"ltr\">compose.override.yaml</code> and make sure CI and any server deployment use <code dir=\"ltr\">-f compose.yaml</code> explicitly, so the production-shaped image is what gets tested.</p>\n</aside>\n<p>Section 3 is complete: a reproducible local stack with persistent data, ordered startup, safe configuration and a fast edit loop. Next you’ll take the same image to Kubernetes, starting with how Kubernetes thinks.</p>\n","toc":[{"id":"sec-1","text":"Two kinds of environment","level":2},{"id":"sec-2","text":"Compose secrets","level":2},{"id":"sec-3","text":"Base file plus override","level":2},{"id":"sec-4","text":"Watch mode: a fast edit loop","level":2}],"takeaways":["The project <code dir=\"ltr\">.env</code> file feeds <code dir=\"ltr\">${VAR}</code> interpolation in <code dir=\"ltr\">compose.yaml</code>; <code dir=\"ltr\">env_file:</code> and <code dir=\"ltr\">environment:</code> set variables inside containers. They are different mechanisms.","<code dir=\"ltr\">${VAR:?message}</code> makes Compose fail fast with a clear message when a required value is missing.","<code dir=\"ltr\">docker compose config</code> prints the fully merged and interpolated file, which is the quickest way to debug configuration.","<code dir=\"ltr\">compose.override.yaml</code> is merged automatically, so keep shared settings in <code dir=\"ltr\">compose.yaml</code> and dev-only ones in the override.","<code dir=\"ltr\">develop.watch</code> with <code dir=\"ltr\">sync</code> copies edited files into the running container, and <code dir=\"ltr\">rebuild</code> rebuilds the image when dependencies change."],"further":[{"title":"Set environment variables in Compose","url":"https://docs.docker.com/compose/how-tos/environment-variables/set-environment-variables/"},{"title":"Use Compose Watch","url":"https://docs.docker.com/compose/how-tos/file-watch/"},{"title":"Merge Compose files","url":"https://docs.docker.com/compose/how-tos/multiple-compose-files/merge/"},{"title":"Secrets in Compose","url":"https://docs.docker.com/compose/how-tos/use-secrets/"}],"quiz":[{"q":"<p>Your project’s <code dir=\"ltr\">.env</code> file contains <code dir=\"ltr\">LOG_LEVEL=debug</code>. The <code dir=\"ltr\">api</code> service has no <code dir=\"ltr\">environment:</code> or <code dir=\"ltr\">env_file:</code> entries. What is <code dir=\"ltr\">LOG_LEVEL</code> inside the container?</p>","options":[{"text":"<code dir=\"ltr\">debug</code>, because Compose loads <code dir=\"ltr\">.env</code> into every container.","why":"The project <code dir=\"ltr\">.env</code> is for interpolating the Compose file itself. It isn’t injected into containers unless you reference it."},{"text":"Unset, unless the service passes it with <code dir=\"ltr\">environment:</code> or <code dir=\"ltr\">env_file:</code>.","why":"Correct. Add <code dir=\"ltr\">LOG_LEVEL: ${LOG_LEVEL}</code> under <code dir=\"ltr\">environment:</code>, or <code dir=\"ltr\">env_file: .env</code>, to pass it through."},{"text":"<code dir=\"ltr\">debug</code>, but only after <code dir=\"ltr\">docker compose restart</code>.","why":"Restarting doesn’t change how <code dir=\"ltr\">.env</code> is used. It still only feeds interpolation."},{"text":"An error, because <code dir=\"ltr\">.env</code> must be named <code dir=\"ltr\">api.env</code>.","why":"<code dir=\"ltr\">.env</code> is the conventional name Compose reads for interpolation. No per-service naming is required."}],"answer":1},{"q":"<p><code dir=\"ltr\">compose.yaml</code> contains <code dir=\"ltr\">POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set it in .env}</code> and the variable isn’t defined anywhere. What happens on <code dir=\"ltr\">docker compose up</code>?</p>","options":[{"text":"Postgres starts with an empty password.","why":"That’s what plain <code dir=\"ltr\">${POSTGRES_PASSWORD}</code> would do, which is exactly why the <code dir=\"ltr\">:?</code> form exists."},{"text":"Compose prompts you to type the password.","why":"Compose never prompts for variables."},{"text":"The literal text <code dir=\"ltr\">${POSTGRES_PASSWORD:?set it in .env}</code> becomes the password.","why":"Compose interpolates the expression; it doesn’t pass it through literally."},{"text":"Compose refuses to start and prints <code dir=\"ltr\">set it in .env</code>.","why":"Correct. <code dir=\"ltr\">:?</code> makes the variable required and uses your text as the error message."}],"answer":3},{"q":"<p>You edit <code dir=\"ltr\">src/routes.ts</code>. With the watch rules below, what does Compose do?</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">YAML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">develop</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  watch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">action</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">sync</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      path</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">./src</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      target</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/app/src</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">action</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">rebuild</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      path</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">package-lock.json</span></span></code></pre></div>\n","options":[{"text":"Rebuilds the image and recreates the container.","why":"Only changes under <code dir=\"ltr\">package-lock.json</code> trigger a rebuild. A file under <code dir=\"ltr\">./src</code> matches the sync rule."},{"text":"Nothing until you run <code dir=\"ltr\">docker compose up</code> again.","why":"Watch reacts to file changes while it runs; you don’t rerun <code dir=\"ltr\">up</code>."},{"text":"Copies the changed file to <code dir=\"ltr\">/app/src</code> in the running container; the dev server inside picks it up.","why":"Correct. <code dir=\"ltr\">sync</code> updates files in place in seconds, with no rebuild."},{"text":"Restarts the database, because the API depends on it.","why":"Watch rules only act on the service they’re defined in."}],"answer":2},{"q":"<p>You want a <code dir=\"ltr\">pgadmin</code> service available for local debugging, but it shouldn’t start in CI or for teammates who don’t need it. What do you use?</p>","options":[{"text":"<code dir=\"ltr\">profiles: [tools]</code> on the service, started with <code dir=\"ltr\">docker compose --profile tools up</code>.","why":"Correct. Services with a profile only start when that profile is enabled."},{"text":"A comment telling people to start it manually.","why":"Comments don’t change behaviour; everyone gets pgadmin on every <code dir=\"ltr\">up</code>."},{"text":"<code dir=\"ltr\">deploy: replicas: 0</code>","why":"It’s a workaround that obscures intent and still needs editing to turn on. Profiles exist for this."},{"text":"A separate Git branch with pgadmin added.","why":"Branches for configuration drift out of date quickly. Profiles keep it in one file."}],"answer":0}],"exercise":{"kind":"fill","language":"yaml","template":"# compose.yaml\nservices:\n  db:\n    environment:\n      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD{{0}}set POSTGRES_PASSWORD in .env}\n\n# compose.override.yaml\nservices:\n  api:\n    develop:\n      watch:\n        - action: {{1}}\n          path: ./src\n          target: /app/src\n        - action: {{2}}\n          path: package-lock.json\n\n# start the stack and keep watching:\n# docker compose up {{3}}","blanks":[{"answers":[":?"]},{"answers":["sync"]},{"answers":["rebuild"]},{"answers":["--watch","-w"]}],"title":"Required values and a fast edit loop","prompt":"<p>Fill in the blanks so that Compose refuses to start when <code dir=\"ltr\">POSTGRES_PASSWORD</code> is missing, source edits are copied into the running API container, lockfile changes rebuild the image, and the stack starts in watch mode.</p>\n","hints":["The interpolation operator for “required, with an error message” is two characters: a colon and a question mark.","One watch action copies files into the container; another rebuilds the image.","<code dir=\"ltr\">docker compose up</code> takes a flag that turns on watch mode."],"explanation":"<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">YAML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">POSTGRES_PASSWORD</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># …</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">- </span><span style=\"color:#116329;--shiki-dark:#7EE787\">action</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">sync</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  path</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">./src</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  target</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/app/src</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">- </span><span style=\"color:#116329;--shiki-dark:#7EE787\">action</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">rebuild</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  path</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">package-lock.json</span></span></code></pre></div>\n<p>Then <code dir=\"ltr\">docker compose up --watch</code>. <code dir=\"ltr\">:?</code> turns a silently empty value into a clear startup error. <code dir=\"ltr\">sync</code> copies edited source files into the running container in about a second, while a dependency change needs <code dir=\"ltr\">rebuild</code> because new packages must be installed into the image.</p>\n","checkLabels":{}},"words":715,"readMinutes":3}