{"id":4723,"date":"2026-02-25T21:43:33","date_gmt":"2026-02-25T16:13:33","guid":{"rendered":"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/"},"modified":"2026-02-25T21:43:33","modified_gmt":"2026-02-25T16:13:33","slug":"master-docker-compose-a-guide-to-multi-container-apps","status":"publish","type":"post","link":"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/","title":{"rendered":"Master Docker Compose: A Guide to Multi-Container Apps"},"content":{"rendered":"<p>[2023-10-14 03:14:22] ERROR: Could not bind to port 8080. Address already in use.<br \/>\n[2023-10-14 03:14:22] DEBUG: Attempting to kill existing container &#8216;api_v2_final_FINAL&#8217;&#8230;<br \/>\n[2023-10-14 03:14:23] Error response from daemon: No such container: api_v2_final_FINAL<br \/>\n[2023-10-14 03:14:23] CRITICAL: Bash script &#8216;deploy_magic.sh&#8217; exited with code 127.<br \/>\n[2023-10-14 03:14:23] CRITICAL: Database connection string &#8216;localhost:5432&#8217; failed.<br \/>\n[2023-10-14 03:14:23] FATAL: Production is down. 404 errors spiking.<br \/>\n[2023-10-14 03:14:24] SMS ALERT: [SRE_TEAM] &#8211; Wake up. The world is ending. Kevin pushed a script.<\/p>\n<pre class=\"codehilite\"><code>It\u2019s 4:00 AM. I\u2019ve consumed enough caffeine to kill a small horse, and I\u2019m staring at a terminal screen that looks like a digital crime scene. The culprit? A 400-line bash script written by a junior developer who thought he could &quot;simplify&quot; our deployment process by manually wrapping `docker run` commands. \n\nHe didn't use a manifest. He didn't use an orchestrator. He used &quot;hope&quot; and a series of nested `if` statements that checked for the existence of PID files that hadn't been relevant since 2012. \n\nThe result? A cascading failure where the API tried to start before the database, the database couldn't find its volume because the path was hardcoded to a directory on Kevin's laptop, and the frontend was trying to talk to a Redis instance that existed only in the ethereal plane of a misconfigured bridge network.\n\nIf you are still using manual bash scripts to manage your containers, you are a liability. If you aren't using **docker compose**, you are essentially playing Jenga with a live grenade. This isn't about &quot;streamlining&quot; your workflow. This is about survival. This is about having a single source of truth that doesn't rely on the fragile memory of a human who hasn't slept.\n\n## The Manual Script That Tried To Kill Me\n\nLet\u2019s look at what I found when I logged into the production jump box. Kevin\u2019s script was a masterpiece of incompetence. It tried to manage container lifecycles using `grep` and `awk` to find container IDs. \n\n```bash\n# DO NOT DO THIS. EVER.\ndocker stop $(docker ps -a -q --filter name=web)\ndocker rm $(docker ps -a -q --filter name=web)\ndocker run -d --name web_app_v3 -p 80:8080 --link db_prod:db myapp:latest\n<\/code><\/pre>\n<p>The <code>--link<\/code> flag? That\u2019s been deprecated for years. It\u2019s a ghost. A relic. And yet, there it was, failing because the <code>db_prod<\/code> container had crashed five minutes earlier due to an unhandled OOM (Out Of Memory) event that the script didn&#8217;t bother to monitor.<\/p>\n<p>When you use <strong>docker compose<\/strong>, you aren&#8217;t just running containers; you are defining a state. You are telling the Docker Engine: &#8220;This is what the world should look like. Make it so.&#8221; With <strong>docker compose version v2.20.2<\/strong>, we have the power to define dependencies, healthchecks, and resource constraints in a way that a bash script never could.<\/p>\n<p>The manual approach fails because it is imperative. &#8220;Do this, then do that.&#8221; If &#8220;this&#8221; fails, &#8220;that&#8221; happens anyway, or the whole thing hangs. <strong>docker compose<\/strong> is declarative. It doesn&#8217;t care about the &#8220;how&#8221; as much as the &#8220;what.&#8221;<\/p>\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_80 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<p class=\"ez-toc-title\" style=\"cursor:inherit\">Table of Contents<\/p>\n<label for=\"ez-toc-cssicon-toggle-item-69d825b7a6498\" class=\"ez-toc-cssicon-toggle-label\"><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/label><input type=\"checkbox\"  id=\"ez-toc-cssicon-toggle-item-69d825b7a6498\"  aria-label=\"Toggle\" \/><nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><ul class='ez-toc-list-level-2' ><li class='ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/#YAML_The_Indentation-Sensitive_Hell_We_Deserve\" >YAML: The Indentation-Sensitive Hell We Deserve<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/#Networking_Why_Everything_Is_Broken_By_Default\" >Networking: Why Everything Is Broken By Default<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/#The_Dependency_Lie_Why_depends_on_Isnt_Enough\" >The Dependency Lie: Why depends_on Isn&#8217;t Enough<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/#Secrets_and_Environment_Variables_Stop_Putting_Passwords_in_Git\" >Secrets and Environment Variables: Stop Putting Passwords in Git<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/#Persistence_is_Futile_Unless_You_Use_Volumes\" >Persistence is Futile (Unless You Use Volumes)<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/#The_Build_vs_Image_Argument\" >The Build vs. Image Argument<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/#Verifying_the_State_of_the_World\" >Verifying the State of the World<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/#The_Final_Stand_Why_Im_Not_a_Goat_Farmer_Yet\" >The Final Stand: Why I\u2019m Not a Goat Farmer (Yet)<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-1'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/#Final_check_Everything_is_green\" >Final check. Everything is green.<\/a><ul class='ez-toc-list-level-2' ><li class='ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-10\" href=\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/#Related_Articles\" >Related Articles<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-1'><a class=\"ez-toc-link ez-toc-heading-11\" href=\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/#Output_shows_all_services_operational\" >Output shows all services operational.<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-1'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/#Sanity_restored%E2%80%A6_for_now\" >Sanity restored&#8230; for now.<\/a><\/li><\/ul><\/nav><\/div>\n<h2><span class=\"ez-toc-section\" id=\"YAML_The_Indentation-Sensitive_Hell_We_Deserve\"><\/span>YAML: The Indentation-Sensitive Hell We Deserve<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>People complain about YAML. They say the indentation is finicky. They say it\u2019s hard to read. To those people, I say: try reading a 500-line bash script with unquoted variables and no error handling at 3 AM. I will take a <code>docker-compose.yml<\/code> file any day of the week.<\/p>\n<p>Below is the reconstruction of our stack. This is the blueprint of sanity. It uses <strong>docker compose<\/strong> to ensure that every service knows its place, its limits, and its neighbors.<\/p>\n<pre class=\"codehilite\"><code class=\"language-yaml\">version: '3.9'\n\nservices:\n  db:\n    image: postgres:15-alpine\n    container_name: production_db\n    restart: unless-stopped\n    environment:\n      POSTGRES_USER: ${DB_USER}\n      POSTGRES_PASSWORD: ${DB_PASSWORD}\n      POSTGRES_DB: app_production\n    volumes:\n      - db_data:\/var\/lib\/postgresql\/data\n    networks:\n      - backend_internal\n    healthcheck:\n      test: [&quot;CMD-SHELL&quot;, &quot;pg_isready -U ${DB_USER} -d app_production&quot;]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    deploy:\n      resources:\n        limits:\n          cpus: '0.50'\n          memory: 512M\n\n  redis:\n    image: redis:7-alpine\n    container_name: production_cache\n    restart: always\n    networks:\n      - backend_internal\n    command: [&quot;redis-server&quot;, &quot;--appendonly&quot;, &quot;yes&quot;]\n\n  api:\n    build:\n      context: .\/api\n      dockerfile: Dockerfile\n    image: our-registry.com\/api-service:v2.4.1\n    container_name: production_api\n    depends_on:\n      db:\n        condition: service_healthy\n      redis:\n        condition: service_started\n    environment:\n      - DATABASE_URL=postgres:\/\/${DB_USER}:${DB_PASSWORD}@db:5432\/app_production\n      - REDIS_URL=redis:\/\/redis:6379\/0\n    networks:\n      - backend_internal\n      - frontend_external\n    restart: on-failure:3\n\n  frontend:\n    image: nginx:stable-alpine\n    container_name: production_frontend\n    ports:\n      - &quot;80:80&quot;\n      - &quot;443:443&quot;\n    volumes:\n      - .\/nginx.conf:\/etc\/nginx\/nginx.conf:ro\n      - .\/dist:\/usr\/share\/nginx\/html:ro\n    depends_on:\n      - api\n    networks:\n      - frontend_external\n\nnetworks:\n  frontend_external:\n    driver: bridge\n  backend_internal:\n    internal: true\n\nvolumes:\n  db_data:\n    driver: local\n<\/code><\/pre>\n<p>Look at that. It\u2019s beautiful. It\u2019s a contract. It specifies that the <code>api<\/code> service won&#8217;t even try to start until the <code>db<\/code> service passes its <code>pg_isready<\/code> healthcheck. It isolates the database in a <code>backend_internal<\/code> network so some script kiddie can&#8217;t hit it directly from the public internet. It sets memory limits so a single memory leak doesn&#8217;t take down the entire host.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Networking_Why_Everything_Is_Broken_By_Default\"><\/span>Networking: Why Everything Is Broken By Default<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>In Kevin&#8217;s manual nightmare, he was trying to connect services using host IP addresses. Do you know what happens to a container&#8217;s IP address when it restarts? It changes. It\u2019s ephemeral. Relying on hardcoded IPs in a containerized environment is like building a house on quicksand.<\/p>\n<p>With <strong>docker compose<\/strong>, we get automatic service discovery. The <code>api<\/code> service doesn&#8217;t need to know that the database is at <code>172.18.0.3<\/code>. It just needs to know that the service is named <code>db<\/code>. The internal DNS resolver provided by the Docker Engine handles the rest.<\/p>\n<p>Notice the network configuration in the YAML above. We have two distinct networks: <code>frontend_external<\/code> and <code>backend_internal<\/code>. <\/p>\n<ol>\n<li><strong>frontend_external<\/strong>: This is where the Nginx container lives, exposing ports 80 and 443 to the world.<\/li>\n<li><strong>backend_internal<\/strong>: This is a dark room. No outside traffic allowed. The database and Redis live here. The API acts as the bridge, sitting on both networks.<\/li>\n<\/ol>\n<p>This is basic security posture, yet without <strong>docker compose<\/strong>, managing these bridge networks manually requires a series of <code>docker network create<\/code>, <code>docker network connect<\/code>, and <code>docker network disconnect<\/code> commands that no human can be trusted to execute correctly under pressure.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"The_Dependency_Lie_Why_depends_on_Isnt_Enough\"><\/span>The Dependency Lie: Why <code>depends_on<\/code> Isn&#8217;t Enough<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Junior devs often think that adding <code>depends_on: - db<\/code> to their <code>docker-compose.yml<\/code> is enough. It isn&#8217;t. All <code>depends_on<\/code> does in its simplest form is ensure that the <code>db<\/code> container has <em>started<\/em>. It doesn&#8217;t mean the database is <em>ready<\/em>. <\/p>\n<p>Postgres takes time to initialize. It has to check its WAL logs, verify its data files, and open its sockets. If your API tries to connect the millisecond the Postgres container starts, it will crash. <\/p>\n<p>This is why we use the long-form <code>depends_on<\/code> syntax with <code>condition: service_healthy<\/code>. By pairing this with a robust <code>healthcheck<\/code>, we ensure the orchestration layer actually understands the state of the application. <\/p>\n<pre class=\"codehilite\"><code class=\"language-bash\"># What happens when I run the command correctly\n$ docker compose up -d\n[+] Running 5\/5\n \u283f Network production_frontend_external  Created\n \u283f Network production_backend_internal   Created\n \u283f Container production_db               Healthy\n \u283f Container production_cache            Started\n \u283f Container production_api              Started\n \u283f Container production_frontend         Started\n<\/code><\/pre>\n<p>If the database fails its healthcheck, the API won&#8217;t start. The system fails safely. It doesn&#8217;t enter a &#8220;half-alive&#8221; state where the frontend is up but showing 500 errors because the backend is in a crash loop.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Secrets_and_Environment_Variables_Stop_Putting_Passwords_in_Git\"><\/span>Secrets and Environment Variables: Stop Putting Passwords in Git<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>I found a file in the repository called <code>config_FINAL_v2.sh<\/code>. Inside were the production database credentials in plain text. I felt my left eye start to twitch.<\/p>\n<p><strong>docker compose<\/strong> supports <code>.env<\/code> files. This allows us to separate our configuration from our definition. The <code>docker-compose.yml<\/code> stays in version control, while the <code>.env<\/code> file\u2014containing the actual secrets\u2014stays on the secure build server or is injected at runtime.<\/p>\n<pre class=\"codehilite\"><code class=\"language-bash\"># .env file - DO NOT COMMIT\nDB_USER=admin_prod_user\nDB_PASSWORD=a_very_long_and_complex_password_that_kevin_would_never_guess\n<\/code><\/pre>\n<p>In the YAML, we reference these using <code>${VARIABLE_NAME}<\/code>. It\u2019s clean. It\u2019s standard. It doesn&#8217;t involve <code>sed<\/code> or <code>grep<\/code> or any other text-processing wizardry that inevitably breaks when someone puts a special character in their password.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Persistence_is_Futile_Unless_You_Use_Volumes\"><\/span>Persistence is Futile (Unless You Use Volumes)<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>The most heartbreaking part of the 48-hour outage was the data loss. Kevin\u2019s script didn&#8217;t use volumes. He thought that as long as the container was running, the data was safe. When he ran his &#8220;cleanup&#8221; command\u2014<code>docker rm $(docker ps -a -q)<\/code>\u2014he wiped the database. Six months of user logs, gone.<\/p>\n<p>We had backups, but restoring them took 12 hours because the backup script was also written by Kevin and it was trying to upload to an S3 bucket that didn&#8217;t exist.<\/p>\n<p>In <strong>docker compose<\/strong>, volumes are first-class citizens. <\/p>\n<pre class=\"codehilite\"><code class=\"language-yaml\">volumes:\n  db_data:\n    driver: local\n<\/code><\/pre>\n<p>By mapping <code>db_data:\/var\/lib\/postgresql\/data<\/code>, we ensure that the data lives on the host&#8217;s storage, independent of the container&#8217;s lifecycle. You can stop the container, delete it, upgrade the image to Postgres 16, and recreate it\u2014the data stays. This is the difference between a toy and a production system.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"The_Build_vs_Image_Argument\"><\/span>The Build vs. Image Argument<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>One of the most powerful features of <strong>docker compose<\/strong> is the ability to handle both local development and production deployments using the same file (or overrides). <\/p>\n<p>In development, you might use the <code>build<\/code> directive:<\/p>\n<pre class=\"codehilite\"><code class=\"language-yaml\">api:\n  build:\n    context: .\n    dockerfile: Dockerfile.dev\n  volumes:\n    - .:\/app\n<\/code><\/pre>\n<p>In production, you use the <code>image<\/code> directive to pull a pre-built, scanned, and tagged image from your private registry:<\/p>\n<pre class=\"codehilite\"><code class=\"language-yaml\">api:\n  image: our-registry.com\/api-service:v2.4.1\n<\/code><\/pre>\n<p>This ensures parity. The environment I\u2019m running on my workstation is the same environment running in the cloud. We aren&#8217;t dealing with &#8220;it works on my machine&#8221; syndrome because the <code>docker-compose.yml<\/code> defines the entire machine.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Verifying_the_State_of_the_World\"><\/span>Verifying the State of the World<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>When I finally got the stack running using <strong>docker compose<\/strong>, I needed to verify that everything was actually working. I didn&#8217;t use <code>ps -ef | grep app<\/code>. I used the tools built for the job.<\/p>\n<pre class=\"codehilite\"><code class=\"language-bash\">$ docker compose ps\n\nNAME                  IMAGE                         COMMAND                  SERVICE             CREATED             STATUS                          PORTS\nproduction_api        our-registry.com\/api:v2.4.1   &quot;python manage.py ru\u2026&quot;   api                 10 minutes ago      Up 10 minutes                   \nproduction_cache      redis:7-alpine                &quot;docker-entrypoint.s\u2026&quot;   redis               10 minutes ago      Up 10 minutes                   6379\/tcp\nproduction_db         postgres:15-alpine            &quot;docker-entrypoint.s\u2026&quot;   db                  10 minutes ago      Up 10 minutes (healthy)         5432\/tcp\nproduction_frontend   nginx:stable-alpine           &quot;\/docker-entrypoint.\u2026&quot;   frontend            10 minutes ago      Up 10 minutes                   0.0.0.0:80-&gt;80\/tcp, 0.0.0.0:443-&gt;443\/tcp\n<\/code><\/pre>\n<p>The <code>(healthy)<\/code> tag next to the database is my security blanket. It means the <code>pg_isready<\/code> command is passing. It means the socket is open. It means I can go to sleep for at least twenty minutes before the next alert.<\/p>\n<p>And when things do go wrong\u2014because they always do\u2014I don&#8217;t have to hunt through <code>\/var\/log\/syslog<\/code> or some obscure file Kevin created in <code>\/tmp<\/code>. I just stream the logs.<\/p>\n<pre class=\"codehilite\"><code class=\"language-bash\">$ docker compose logs --tail=20 -f api\n\nproduction_api  | [2023-10-14 04:45:12] INFO: Starting server on port 8000\nproduction_api  | [2023-10-14 04:45:12] INFO: Connecting to database at db:5432\nproduction_api  | [2023-10-14 04:45:13] INFO: Database connection established.\nproduction_api  | [2023-10-14 04:45:13] INFO: Connecting to Redis at redis:6379\nproduction_api  | [2023-10-14 04:45:13] INFO: Redis connection established.\nproduction_api  | [2023-10-14 04:45:14] INFO: Application is ready to receive traffic.\nproduction_api  | [2023-10-14 04:46:01] GET \/api\/v1\/health 200 OK\nproduction_api  | [2023-10-14 04:46:05] GET \/api\/v1\/users 200 OK\n<\/code><\/pre>\n<h2><span class=\"ez-toc-section\" id=\"The_Final_Stand_Why_Im_Not_a_Goat_Farmer_Yet\"><\/span>The Final Stand: Why I\u2019m Not a Goat Farmer (Yet)<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>I\u2019ve spent the last 48 hours cleaning up a mess that should never have happened. I\u2019ve seen things in that bash script that will haunt my nightmares\u2014unclosed loops, variables named <code>var1<\/code>, <code>var2<\/code>, and <code>var3_new<\/code>, and a complete lack of respect for the principles of idempotent infrastructure.<\/p>\n<p>We are moving everything to <strong>docker compose<\/strong>. No exceptions. If a service isn&#8217;t in the compose file, it doesn&#8217;t exist. If a configuration parameter isn&#8217;t in the <code>.env<\/code> file, it isn&#8217;t real. <\/p>\n<p>Is <strong>docker compose<\/strong> perfect? No. It has its quirks. YAML can be a pain. Sometimes the internal DNS resolver gets confused if you do too many hot-reloads. But compared to the alternative\u2014the manual, script-driven chaos that nearly destroyed this company\u2014it is a godsend.<\/p>\n<p>It provides a common language for developers and SREs. It allows us to version our infrastructure. It allows us to spin up an entire replica of production in seconds for testing. <\/p>\n<p>I\u2019m tired. My eyes are burning, and I\u2019m pretty sure I can hear the server rack humming in my sleep. But the stack is up. The healthchecks are green. The junior developer has been banned from using <code>chmod +x<\/code> on any file ending in <code>.sh<\/code>. <\/p>\n<p>I\u2019ve looked at the prices of land in Vermont. I\u2019ve researched the dietary needs of goats. It\u2019s a tempting life. No logs, no containers, no 3 AM calls. Just me, some animals, and a complete lack of internet connectivity. But as long as I have a well-structured <code>docker-compose.yml<\/code> and a functioning container runtime, I\u2019ll stay in the trenches. <\/p>\n<p>Just don&#8217;t let Kevin touch the production environment again. If I see one more <code>docker run<\/code> command without a <code>--restart<\/code> policy, I\u2019m quitting on the spot. <\/p>\n<p>Now, if you&#8217;ll excuse me, I&#8217;m going to go find a place to sleep where the sound of cooling fans can&#8217;t reach me. Use <strong>docker compose<\/strong>. Don&#8217;t be a Kevin. Your SREs will thank you, or at the very least, they won&#8217;t plot your demise in the middle of the night.<\/p>\n<p>&#8220;`bash<\/p>\n<h1><span class=\"ez-toc-section\" id=\"Final_check_Everything_is_green\"><\/span>Final check. Everything is green.<span class=\"ez-toc-section-end\"><\/span><\/h1>\n<p>$ docker compose ps &#8211;filter &#8220;status=running&#8221;<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Related_Articles\"><\/span>Related Articles<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Explore more insights and best practices:<\/p>\n<ul>\n<li><a href=\"https:\/\/itsupportwale.com\/blog\/top-cybersecurity-jobs-in-2024-careers-salary-and-skills\/\">Top Cybersecurity Jobs In 2024 Careers Salary And Skills<\/a><\/li>\n<li><a href=\"https:\/\/itsupportwale.com\/blog\/413-request-entity-too-large\/\">413 Request Entity Too Large<\/a><\/li>\n<li><a href=\"https:\/\/itsupportwale.com\/blog\/docker-best-practices-build-production-ready-containers\/\">Docker Best Practices Build Production Ready Containers<\/a><\/li>\n<\/ul>\n<h1><span class=\"ez-toc-section\" id=\"Output_shows_all_services_operational\"><\/span>Output shows all services operational.<span class=\"ez-toc-section-end\"><\/span><\/h1>\n<h1><span class=\"ez-toc-section\" id=\"Sanity_restored%E2%80%A6_for_now\"><\/span>Sanity restored&#8230; for now.<span class=\"ez-toc-section-end\"><\/span><\/h1>\n","protected":false},"excerpt":{"rendered":"<p>[2023-10-14 03:14:22] ERROR: Could not bind to port 8080. Address already in use. [2023-10-14 03:14:22] DEBUG: Attempting to kill existing container &#8216;api_v2_final_FINAL&#8217;&#8230; [2023-10-14 03:14:23] Error response from daemon: No such container: api_v2_final_FINAL [2023-10-14 03:14:23] CRITICAL: Bash script &#8216;deploy_magic.sh&#8217; exited with code 127. [2023-10-14 03:14:23] CRITICAL: Database connection string &#8216;localhost:5432&#8217; failed. [2023-10-14 03:14:23] FATAL: Production is &#8230; <a title=\"Master Docker Compose: A Guide to Multi-Container Apps\" class=\"read-more\" href=\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/\" aria-label=\"Read more  on Master Docker Compose: A Guide to Multi-Container Apps\">Read more<\/a><\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-4723","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.0 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Master Docker Compose: A Guide to Multi-Container Apps - ITSupportWale<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Master Docker Compose: A Guide to Multi-Container Apps - ITSupportWale\" \/>\n<meta property=\"og:description\" content=\"[2023-10-14 03:14:22] ERROR: Could not bind to port 8080. Address already in use. [2023-10-14 03:14:22] DEBUG: Attempting to kill existing container &#8216;api_v2_final_FINAL&#8217;&#8230; [2023-10-14 03:14:23] Error response from daemon: No such container: api_v2_final_FINAL [2023-10-14 03:14:23] CRITICAL: Bash script &#8216;deploy_magic.sh&#8217; exited with code 127. [2023-10-14 03:14:23] CRITICAL: Database connection string &#8216;localhost:5432&#8217; failed. [2023-10-14 03:14:23] FATAL: Production is ... Read more\" \/>\n<meta property=\"og:url\" content=\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/\" \/>\n<meta property=\"og:site_name\" content=\"ITSupportWale\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/Itsupportwale-298547177495978\" \/>\n<meta property=\"article:published_time\" content=\"2026-02-25T16:13:33+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/itsupportwale.com\/blog\/wp-content\/uploads\/2021\/05\/android-chrome-512x512-1.png\" \/>\n\t<meta property=\"og:image:width\" content=\"512\" \/>\n\t<meta property=\"og:image:height\" content=\"512\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Techie\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Techie\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"12 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/\"},\"author\":{\"name\":\"Techie\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/#\/schema\/person\/8c5a2b3d36396e0a8fd91ec8242fd46d\"},\"headline\":\"Master Docker Compose: A Guide to Multi-Container Apps\",\"datePublished\":\"2026-02-25T16:13:33+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/\"},\"wordCount\":1548,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/#organization\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/\",\"url\":\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/\",\"name\":\"Master Docker Compose: A Guide to Multi-Container Apps - ITSupportWale\",\"isPartOf\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/#website\"},\"datePublished\":\"2026-02-25T16:13:33+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/itsupportwale.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Master Docker Compose: A Guide to Multi-Container Apps\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/#website\",\"url\":\"https:\/\/itsupportwale.com\/blog\/\",\"name\":\"ITSupportWale\",\"description\":\"Tips, Tricks, Fixed-Errors, Tutorials &amp; Guides\",\"publisher\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/itsupportwale.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/#organization\",\"name\":\"itsupportwale\",\"url\":\"https:\/\/itsupportwale.com\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/itsupportwale.com\/blog\/wp-content\/uploads\/2023\/09\/cropped-Logo-trans-without-slogan.png\",\"contentUrl\":\"https:\/\/itsupportwale.com\/blog\/wp-content\/uploads\/2023\/09\/cropped-Logo-trans-without-slogan.png\",\"width\":1119,\"height\":144,\"caption\":\"itsupportwale\"},\"image\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/www.facebook.com\/Itsupportwale-298547177495978\"]},{\"@type\":\"Person\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/#\/schema\/person\/8c5a2b3d36396e0a8fd91ec8242fd46d\",\"name\":\"Techie\",\"sameAs\":[\"https:\/\/itsupportwale.com\",\"iswblogadmin\"],\"url\":\"https:\/\/itsupportwale.com\/blog\/author\/iswblogadmin\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Master Docker Compose: A Guide to Multi-Container Apps - ITSupportWale","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/","og_locale":"en_US","og_type":"article","og_title":"Master Docker Compose: A Guide to Multi-Container Apps - ITSupportWale","og_description":"[2023-10-14 03:14:22] ERROR: Could not bind to port 8080. Address already in use. [2023-10-14 03:14:22] DEBUG: Attempting to kill existing container &#8216;api_v2_final_FINAL&#8217;&#8230; [2023-10-14 03:14:23] Error response from daemon: No such container: api_v2_final_FINAL [2023-10-14 03:14:23] CRITICAL: Bash script &#8216;deploy_magic.sh&#8217; exited with code 127. [2023-10-14 03:14:23] CRITICAL: Database connection string &#8216;localhost:5432&#8217; failed. [2023-10-14 03:14:23] FATAL: Production is ... Read more","og_url":"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/","og_site_name":"ITSupportWale","article_publisher":"https:\/\/www.facebook.com\/Itsupportwale-298547177495978","article_published_time":"2026-02-25T16:13:33+00:00","og_image":[{"width":512,"height":512,"url":"https:\/\/itsupportwale.com\/blog\/wp-content\/uploads\/2021\/05\/android-chrome-512x512-1.png","type":"image\/png"}],"author":"Techie","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Techie","Est. reading time":"12 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/#article","isPartOf":{"@id":"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/"},"author":{"name":"Techie","@id":"https:\/\/itsupportwale.com\/blog\/#\/schema\/person\/8c5a2b3d36396e0a8fd91ec8242fd46d"},"headline":"Master Docker Compose: A Guide to Multi-Container Apps","datePublished":"2026-02-25T16:13:33+00:00","mainEntityOfPage":{"@id":"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/"},"wordCount":1548,"commentCount":0,"publisher":{"@id":"https:\/\/itsupportwale.com\/blog\/#organization"},"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/","url":"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/","name":"Master Docker Compose: A Guide to Multi-Container Apps - ITSupportWale","isPartOf":{"@id":"https:\/\/itsupportwale.com\/blog\/#website"},"datePublished":"2026-02-25T16:13:33+00:00","breadcrumb":{"@id":"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/itsupportwale.com\/blog\/master-docker-compose-a-guide-to-multi-container-apps\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/itsupportwale.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Master Docker Compose: A Guide to Multi-Container Apps"}]},{"@type":"WebSite","@id":"https:\/\/itsupportwale.com\/blog\/#website","url":"https:\/\/itsupportwale.com\/blog\/","name":"ITSupportWale","description":"Tips, Tricks, Fixed-Errors, Tutorials &amp; Guides","publisher":{"@id":"https:\/\/itsupportwale.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/itsupportwale.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/itsupportwale.com\/blog\/#organization","name":"itsupportwale","url":"https:\/\/itsupportwale.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/itsupportwale.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/itsupportwale.com\/blog\/wp-content\/uploads\/2023\/09\/cropped-Logo-trans-without-slogan.png","contentUrl":"https:\/\/itsupportwale.com\/blog\/wp-content\/uploads\/2023\/09\/cropped-Logo-trans-without-slogan.png","width":1119,"height":144,"caption":"itsupportwale"},"image":{"@id":"https:\/\/itsupportwale.com\/blog\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/Itsupportwale-298547177495978"]},{"@type":"Person","@id":"https:\/\/itsupportwale.com\/blog\/#\/schema\/person\/8c5a2b3d36396e0a8fd91ec8242fd46d","name":"Techie","sameAs":["https:\/\/itsupportwale.com","iswblogadmin"],"url":"https:\/\/itsupportwale.com\/blog\/author\/iswblogadmin\/"}]}},"_links":{"self":[{"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/posts\/4723","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/comments?post=4723"}],"version-history":[{"count":0,"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/posts\/4723\/revisions"}],"wp:attachment":[{"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/media?parent=4723"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/categories?post=4723"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/tags?post=4723"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}