{"id":4747,"date":"2026-03-27T21:30:23","date_gmt":"2026-03-27T16:00:23","guid":{"rendered":"https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/"},"modified":"2026-03-27T21:30:23","modified_gmt":"2026-03-27T16:00:23","slug":"mastering-docker-compose-simplify-multi-container-apps-2","status":"publish","type":"post","link":"https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/","title":{"rendered":"Mastering Docker Compose: Simplify Multi-Container Apps"},"content":{"rendered":"<p>The smell of ozone and stale coffee is the only thing that stays the same. <\/p>\n<p>It was 03:14 AM on a Tuesday in 2014. I was standing in a data center in Secaucus, the floor tiles vibrating under my boots from the sheer CFM of the cooling fans. We had a cluster of HP ProLiant DL380s that had been running a monolithic financial settlement engine for six years. I knew those machines. I knew their IRQ timings. I knew which ones had slightly wonky RAID controllers that required a specific firmware revision just to keep the write-cache from flaking out.<\/p>\n<p>Then the &#8220;drift&#8221; happened. <\/p>\n<p>A junior dev\u2014bless his heart and his misplaced enthusiasm\u2014had manually updated <code>libssl<\/code> on Node 04 to patch a CVE he\u2019d read about on Hacker News. He didn&#8217;t use the configuration management scripts. He didn&#8217;t document it. He just <code>apt-get install<\/code>ed his way into a nightmare. Three hours later, the binary linked against the old library started throwing segmentation faults that looked like a digital stroke.<\/p>\n<pre class=\"codehilite\"><code class=\"language-text\">[72432.124005] settlement_eng[14202]: segfault at 0 ip 00007f8eac321a10 sp 00007ffed8c1a210 error 4 in libssl.so.1.0.0[7f8eac2e1000+60000]\n[72432.124012] Code: 48 89 45 f0 48 8b 45 f0 48 8b 00 48 85 c0 74 1b 48 8b 45 f0 48 8b 00 48 8b 40 28 48 85 c0 74 0a 48 8b 45 f0 48 8b 00 ff 50 28 &lt;48&gt; 8b 45 f0 48 8b 00 48 8b 40 30 48 85 c0 74 0a 48 8b 45 f0 48 8b\nldd \/usr\/local\/bin\/settlement_eng\n    linux-vdso.so.1 =&gt;  (0x00007ffed8dfb000)\n    libssl.so.1.0.0 =&gt; not found\n    libcrypto.so.1.0.0 =&gt; not found\n    libc.so.6 =&gt; \/lib\/x86_64-linux-gnu\/libc.so.6 (0x00007f8eabf10000)\n<\/code><\/pre>\n<p>The system was hemorrhaging $40,000 a minute. I spent four hours manually symlinking libraries and rebuilding the environment from a 500-line Bash script that I wrote in 2011, which\u2014of course\u2014failed because the Debian mirrors had moved. That was the old way. The &#8220;pure&#8221; way. The way that nearly gave me a coronary.<\/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-69e5fde9b8df7\" 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-69e5fde9b8df7\"  aria-label=\"Toggle\" \/><nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/#The_Shell_Script_Delusion\" >The Shell Script Delusion<\/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\/mastering-docker-compose-simplify-multi-container-apps-2\/#YAML_The_Necessary_Evil_of_Declarative_State\" >YAML: The Necessary Evil of Declarative State<\/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\/mastering-docker-compose-simplify-multi-container-apps-2\/#The_Networking_Lies_We_Tell_Ourselves\" >The Networking Lies We Tell Ourselves<\/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\/mastering-docker-compose-simplify-multi-container-apps-2\/#Volumes_Where_Data_Goes_to_Die\" >Volumes: Where Data Goes to Die<\/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\/mastering-docker-compose-simplify-multi-container-apps-2\/#Secrets_Environment_Variables_and_the_Art_of_Not_Getting_Fired\" >Secrets, Environment Variables, and the Art of Not Getting Fired<\/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\/mastering-docker-compose-simplify-multi-container-apps-2\/#The_Idempotency_Myth_and_the_Reality_of_docker_compose_up\" >The Idempotency Myth and the Reality of docker compose up<\/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\/mastering-docker-compose-simplify-multi-container-apps-2\/#The_Ghost_of_Bare-Metal_Past\" >The Ghost of Bare-Metal Past<\/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\/mastering-docker-compose-simplify-multi-container-apps-2\/#Related_Articles\" >Related Articles<\/a><\/li><\/ul><\/nav><\/div>\n<h2><span class=\"ez-toc-section\" id=\"The_Shell_Script_Delusion\"><\/span>The Shell Script Delusion<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>For twenty years, I believed that if you couldn&#8217;t build a server from a blank TTY with nothing but a shell script and a prayer, you weren&#8217;t a real engineer. I treated my servers like prize-winning orchids. I hand-tuned <code>\/etc\/sysctl.conf<\/code> like I was playing a Stradivarius. <\/p>\n<p>When Docker first showed up, I laughed. &#8220;A wrapper for LXC? I can write my own cgroups, thanks.&#8221; But then the microservices era hit like a freight train. Suddenly, I wasn&#8217;t managing one monolith; I was managing twelve different services, three different databases, and a message broker. <\/p>\n<p>I tried to keep the old faith. I wrote a master deployment script. It was a masterpiece of <code>if-else<\/code> blocks, <code>sed<\/code> commands, and <code>grep<\/code> hacks. It would SSH into five different boxes, pull the latest code, and run a series of <code>docker run<\/code> commands. <\/p>\n<pre class=\"codehilite\"><code class=\"language-bash\"># The old way - A fragment of my descent into madness\ndocker run -d --name pg-db -e POSTGRES_PASSWORD=password123 -v \/mnt\/data\/pg:\/var\/lib\/postgresql\/data postgres:15-alpine\ndocker run -d --name redis-cache redis:7.0-bullseye\ndocker run -d --name api-srv --link pg-db:db --link redis-cache:redis -p 8080:8080 my-api:latest\n# Wait, I forgot the network. And the restart policy. And the log rotation.\n# And if the API starts before the DB is ready, it crashes.\n# So I add a 'sleep 30'. God help me.\n<\/code><\/pre>\n<p>The &#8220;AHA&#8221; moment didn&#8217;t come during a keynote speech. It came when I had to explain to a new hire how to set up the local dev environment. It took him three days. He had the wrong version of Redis. His Postgres container couldn&#8217;t see the API container because he\u2019d messed up the bridge network naming. I looked at my 500-line Bash script, then I looked at his broken terminal, and I realized I was the problem. I was holding onto a &#8220;bare-metal&#8221; ego while the world was moving toward declarative state.<\/p>\n<p>I realized that <code>docker run<\/code> is a command, but <code>docker-compose.yml<\/code> is a contract.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"YAML_The_Necessary_Evil_of_Declarative_State\"><\/span>YAML: The Necessary Evil of Declarative State<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>I hate YAML. I hate the whitespace sensitivity. I hate that it feels like a configuration language designed by people who think tabs are a sin. But I hate downtime more. <\/p>\n<p>When I finally swallowed my pride and moved to Docker Compose (specifically the V2 specification), the first thing I noticed was the sanity of the <code>depends_on<\/code> and <code>healthcheck<\/code> parameters. No more <code>sleep 30<\/code> in my Bash scripts. No more hoping the database was ready to accept connections before the application layer tried to bind to it.<\/p>\n<p>Here is the anatomy of the beast that replaced my 500-line script. It\u2019s not &#8220;pretty,&#8221; but it\u2019s deterministic. It\u2019s idempotent. If I run it ten times, I get the same result ten times. That is a luxury I never had with bare metal.<\/p>\n<pre class=\"codehilite\"><code class=\"language-yaml\">version: '3.8' # Now part of the Compose Spec\n\nservices:\n  db:\n    image: postgres:15-alpine\n    container_name: production_db\n    restart: always\n    environment:\n      POSTGRES_USER: ${DB_USER:-admin}\n      POSTGRES_PASSWORD: ${DB_PASSWORD}\n      POSTGRES_DB: settlement_prod\n    volumes:\n      - pgdata:\/var\/lib\/postgresql\/data\n    healthcheck:\n      test: [&quot;CMD-SHELL&quot;, &quot;pg_isready -U admin -d settlement_prod&quot;]\n      interval: 10s\n      timeout: 5s\n      retries: 5\n    networks:\n      - backend\n\n  cache:\n    image: redis:7.0-bullseye\n    command: redis-server --save 60 1 --loglevel warning\n    volumes:\n      - redisdata:\/data\n    networks:\n      - backend\n\n  api:\n    build:\n      context: .\/services\/api\n      dockerfile: Dockerfile\n    depends_on:\n      db:\n        condition: service_healthy\n      cache:\n        condition: service_started\n    env_file: .env\n    ports:\n      - &quot;8080:8080&quot;\n    networks:\n      - backend\n      - frontend\n\nnetworks:\n  frontend:\n  backend:\n    internal: true\n\nvolumes:\n  pgdata:\n  redisdata:\n<\/code><\/pre>\n<p>Look at that <code>healthcheck<\/code>. In the old days, I\u2019d have a cron job or a Nagios check that would alert me <em>after<\/em> the service failed. Here, the <code>api<\/code> service won&#8217;t even attempt to spawn until the <code>db<\/code> service reports it\u2019s actually ready to handle queries. That\u2019s not just a feature; it\u2019s a preventative measure against the 3 AM wake-up call.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"The_Networking_Lies_We_Tell_Ourselves\"><\/span>The Networking Lies We Tell Ourselves<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>On bare metal, networking was simple. You had an IP. You had a port. You had a firewall. If you wanted two services to talk, you pointed them at each other&#8217;s static IPs. <\/p>\n<p>In the container world, networking is a hall of mirrors. I spent weeks fighting with the Docker bridge. I\u2019d see errors like this in the logs:<\/p>\n<pre class=\"codehilite\"><code class=\"language-text\">api-srv  | 2023\/10\/12 14:22:11 Error connecting to database: dial tcp 172.18.0.2:5432: connect: connection refused\napi-srv  | 2023\/10\/12 14:22:11 Retrying in 5 seconds...\napi-srv  | 2023\/10\/12 14:22:16 Error connecting to database: dial tcp: lookup db on 127.0.0.11:53: no such host\n<\/code><\/pre>\n<p>The <code>127.0.0.11:53<\/code> address is the Docker embedded DNS server. It\u2019s a fickle beast. If you don&#8217;t define your networks properly in Compose, your services are just shouting into the void. <\/p>\n<p>The &#8220;Veteran&#8221; in me wanted to use <code>network_mode: host<\/code> for everything. &#8220;Just give me the raw throughput!&#8221; I\u2019d scream at the screen. But <code>host<\/code> mode is a trap. It destroys the isolation that makes containers useful. It leads to port collisions that remind me of the dark days of trying to run two instances of Apache on the same box.<\/p>\n<p>Docker Compose forces you to define your topology. In the YAML above, the <code>db<\/code> and <code>cache<\/code> are on the <code>backend<\/code> network, which is marked as <code>internal: true<\/code>. This means they have no route to the outside world. They are invisible to the internet. Only the <code>api<\/code> service, which sits on both <code>frontend<\/code> and <code>backend<\/code>, can talk to them. This is the kind of security posture that used to take me hours to configure with <code>iptables<\/code> and VLANs. Now, it\u2019s four lines of YAML.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Volumes_Where_Data_Goes_to_Die\"><\/span>Volumes: Where Data Goes to Die<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>If networking is a hall of mirrors, volumes are a minefield. I\u2019ve seen more data lost to improper volume mounting than to actual hardware failure. <\/p>\n<p>The biggest mistake people make\u2014and I made it too\u2014is treating container storage like a regular filesystem. You think, &#8220;I&#8217;ll just mount <code>\/var\/lib\/mysql<\/code> to a local folder.&#8221; Then you realize that the UID\/GID of the user inside the container (usually <code>999<\/code> for Postgres) doesn&#8217;t match the user on your host machine. You end up with a permission denied error that kills the service on startup.<\/p>\n<pre class=\"codehilite\"><code class=\"language-text\">production_db | 2023-10-12 14:30:05.123 UTC [1] FATAL:  could not open directory &quot;pg_tblspc&quot;: Permission denied\nproduction_db | 2023-10-12 14:30:05.123 UTC [1] LOG:  database system is shut down\n<\/code><\/pre>\n<p>I learned the hard way: use named volumes. Let Docker manage the abstraction. If you need to back it up, you back up the volume, not the directory. And for the love of all that is holy, don&#8217;t use <code>bind mounts<\/code> for production databases unless you have a very specific reason to handle the I\/O overhead and permission mapping yourself.<\/p>\n<p>The bare-metal guy in me still winces at the thought of not knowing exactly which sector on the disk my data lives on. But the systems engineer in me recognizes that <code>pgdata:\/var\/lib\/postgresql\/data<\/code> is a much more stable way to handle state across container restarts than a hardcoded path in a Bash script that might not exist on the next server I rack.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Secrets_Environment_Variables_and_the_Art_of_Not_Getting_Fired\"><\/span>Secrets, Environment Variables, and the Art of Not Getting Fired<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>In the old days, secrets were kept in a file called <code>config.php<\/code> or <code>settings.py<\/code>, usually with permissions set to <code>600<\/code>, owned by <code>root<\/code>. We\u2019d use <code>rsync<\/code> to push these files around. It was primitive, but it worked\u2014until someone accidentally committed the config file to Git.<\/p>\n<p>Docker Compose handles environment variables with a <code>.env<\/code> file, but it\u2019s a double-edged sword. <\/p>\n<pre class=\"codehilite\"><code class=\"language-text\"># .env file - The place where security goes to hide\nDB_PASSWORD=super_secret_password_that_i_will_forget_to_change\nAPI_KEY=sk_live_51Mz...\nDEBUG=false\n<\/code><\/pre>\n<p>The problem is that people forget that <code>.env<\/code> is just a local convenience. In a real production environment, you shouldn&#8217;t be using <code>.env<\/code> files; you should be injecting these through your CI\/CD pipeline or a proper secret manager. But for local development\u2014the &#8220;mirroring production&#8221; part\u2014Compose is king. <\/p>\n<p>I can hand a developer a <code>docker-compose.yml<\/code> and a <code>.env.example<\/code>. They copy the example, fill in their local keys, and run <code>docker compose up -d<\/code>. <\/p>\n<pre class=\"codehilite\"><code class=\"language-bash\">$ docker compose up -d\n[+] Running 4\/4\n \u283f Network settlement_frontend  Created                                  0.1s\n \u283f Network settlement_backend   Created                                  0.1s\n \u283f Container production_db      Started                                  0.5s\n \u283f Container redis-cache        Started                                  0.4s\n \u283f Container api-srv            Started                                  0.8s\n\n$ docker compose ps\nNAME                IMAGE                COMMAND                  SERVICE             CREATED             STATUS                    PORTS\napi-srv             settlement-api       &quot;.\/main&quot;                 api                 10 seconds ago      Up 9 seconds              0.0.0.0:8080-&gt;8080\/tcp\nproduction_db       postgres:15-alpine   &quot;docker-entrypoint.s\u2026&quot;   db                  10 seconds ago      Up 9 seconds (healthy)    5432\/tcp\nredis-cache         redis:7.0-bullseye   &quot;docker-entrypoint.s\u2026&quot;   cache               10 seconds ago      Up 9 seconds              6379\/tcp\n<\/code><\/pre>\n<p>That <code>(healthy)<\/code> tag next to the database? That\u2019s the sound of me sleeping through the night. It means the container isn&#8217;t just &#8220;running&#8221; (which is a useless metric); it means it\u2019s actually responding to queries.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"The_Idempotency_Myth_and_the_Reality_of_docker_compose_up\"><\/span>The Idempotency Myth and the Reality of <code>docker compose up<\/code><span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We talk about &#8220;idempotency&#8221; like it\u2019s a religious commandment. The idea is that you can run the same command over and over and the state remains the same. Bare-metal scripts are almost never idempotent. If you run <code>mkdir \/data<\/code> twice, the second time it throws an error unless you add <code>-p<\/code>. If you run <code>apt-get install<\/code> twice, it might upgrade a package you didn&#8217;t want to touch.<\/p>\n<p>Docker Compose is the closest I\u2019ve ever come to true idempotency in a deployment workflow. If I change a single environment variable in the YAML and run <code>docker compose up -d<\/code>, it doesn&#8217;t tear down the whole stack. It calculates the diff. It sees that the <code>db<\/code> and <code>cache<\/code> haven&#8217;t changed, so it leaves them alone. It sees the <code>api<\/code> needs a new environment variable, so it recreates only that container.<\/p>\n<pre class=\"codehilite\"><code class=\"language-text\">$ docker compose up -d\n[+] Running 3\/3\n \u283f Container production_db      Running                                  0.0s\n \u283f Container redis-cache        Running                                  0.0s\n \u283f Container api-srv            Recreated                                0.3s\n<\/code><\/pre>\n<p>This &#8220;Recreated&#8221; status is the magic. It\u2019s the difference between a 5-minute outage and a 500-millisecond blip. <\/p>\n<p>But let\u2019s talk about the failures, because that\u2019s where the grit is. What happens when the <code>build<\/code> fails? What happens when the <code>.dockerignore<\/code> is missing and you accidentally send your 2GB <code>node_modules<\/code> folder to the Docker daemon as part of the build context?<\/p>\n<pre class=\"codehilite\"><code class=\"language-text\">$ docker compose build\n[+] Building 124.2s (7\/11)\n =&gt; [api internal] load build definition from Dockerfile                 0.1s\n =&gt; [api internal] load .dockerignore                                    0.1s\n =&gt; [api internal] load metadata for docker.io\/library\/golang:1.21-alpine  0.5s\n =&gt; [api internal] sending build context to Docker daemon              1.8GB\n<\/code><\/pre>\n<p>I\u2019ve seen seniors sit there for ten minutes wondering why their build is slow, not realizing they\u2019re uploading their entire local history to the daemon. The <code>.dockerignore<\/code> is as important as the <code>Dockerfile<\/code> itself. It\u2019s the &#8220;don&#8217;t touch this&#8221; sign on the server rack.<\/p>\n<pre class=\"codehilite\"><code class=\"language-text\"># .dockerignore\n.git\nnode_modules\n*.log\ntmp\/\n<\/code><\/pre>\n<h2><span class=\"ez-toc-section\" id=\"The_Ghost_of_Bare-Metal_Past\"><\/span>The Ghost of Bare-Metal Past<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>I still miss the physical reality of servers. I miss knowing that <code>eth0<\/code> is the top port on the NIC and <code>eth1<\/code> is the bottom one. I miss the tactile click of a drive tray locking into place. <\/p>\n<p>But I don&#8217;t miss the configuration drift. I don&#8217;t miss the &#8220;it works on my machine&#8221; excuses from developers who have a different version of <code>libpq<\/code> installed on their MacBook than we have on the Debian stable servers. <\/p>\n<p>Docker Compose is the bridge. It allows me to codify my twenty years of infrastructure knowledge into a format that a 22-year-old intern can understand. It\u2019s a way to ensure that the &#8220;manual configuration&#8221; disaster of 2014 never happens again. <\/p>\n<p>When I look at a <code>docker-compose.yml<\/code> file, I don&#8217;t see &#8220;shiny&#8221; new tech. I see a hardened, version-controlled blueprint of a system. I see the end of the 500-line Bash script. I see a world where I can actually take a vacation without worrying that a library update will trigger a cascade of segmentation faults.<\/p>\n<p>If you\u2019re still writing manual scripts to manage your containers, you\u2019re not being &#8220;hardcore.&#8221; You\u2019re being a liability. You\u2019re building a snowflake in a world that demands an ice factory. <\/p>\n<p>The transition wasn&#8217;t easy. I fought it every step of the way. I grumbled about the overhead of the Docker daemon. I complained about the complexity of overlay networks. But then I realized that the complexity was always there\u2014I was just hiding it in my head and in my brittle shell scripts. Docker Compose just forces that complexity out into the open where it can be versioned, tested, and managed.<\/p>\n<p>I\u2019m still a bare-metal veteran. I still care about kernel parameters and I\/O schedulers. But now, I set those parameters in the <code>sysctls<\/code> section of my Compose file. I tune my limits in the <code>ulimits<\/code> block. I\u2019m still racking servers; I\u2019m just doing it in code now. And the coffee still tastes like battery acid at 3 AM, but at least the servers are staying up.<\/p>\n<pre class=\"codehilite\"><code class=\"language-text\">$ docker compose logs -f api\napi-srv  | 2023-10-12 15:00:01 INFO: Starting settlement engine v2.4.1\napi-srv  | 2023-10-12 15:00:01 INFO: Connected to database: settlement_prod\napi-srv  | 2023-10-12 15:00:01 INFO: Connected to cache: redis-7.0\napi-srv  | 2023-10-12 15:00:01 INFO: Listening on :8080\n<\/code><\/pre>\n<p>That\u2019s the only log I want to see. No segfaults. No missing libraries. Just a clean, boring, stable start. That\u2019s the dream. And Docker Compose, for all its YAML-induced headaches, is the only thing that actually delivered it.<\/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\/laravel-clear-cache\/\">Laravel Clear Cache<\/a><\/li>\n<li><a href=\"https:\/\/itsupportwale.com\/blog\/master-the-python-list-a-complete-guide-with-examples\/\">Master The Python List A Complete Guide With Examples<\/a><\/li>\n<li><a href=\"https:\/\/itsupportwale.com\/blog\/how-to-install-open-source-zimbra-8-8-mail-server-zcs-8-8-12-on-ubuntu-16-04-lts\/\">How To Install Open Source Zimbra 8 8 Mail Server Zcs 8 8 12 On Ubuntu 16 04 Lts<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>The smell of ozone and stale coffee is the only thing that stays the same. It was 03:14 AM on a Tuesday in 2014. I was standing in a data center in Secaucus, the floor tiles vibrating under my boots from the sheer CFM of the cooling fans. We had a cluster of HP ProLiant &#8230; <a title=\"Mastering Docker Compose: Simplify Multi-Container Apps\" class=\"read-more\" href=\"https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/\" aria-label=\"Read more  on Mastering Docker Compose: Simplify 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-4747","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>Mastering Docker Compose: Simplify 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\/mastering-docker-compose-simplify-multi-container-apps-2\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Mastering Docker Compose: Simplify Multi-Container Apps - ITSupportWale\" \/>\n<meta property=\"og:description\" content=\"The smell of ozone and stale coffee is the only thing that stays the same. It was 03:14 AM on a Tuesday in 2014. I was standing in a data center in Secaucus, the floor tiles vibrating under my boots from the sheer CFM of the cooling fans. We had a cluster of HP ProLiant ... Read more\" \/>\n<meta property=\"og:url\" content=\"https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/\" \/>\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-03-27T16:00:23+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=\"13 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/\"},\"author\":{\"name\":\"Techie\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/#\/schema\/person\/8c5a2b3d36396e0a8fd91ec8242fd46d\"},\"headline\":\"Mastering Docker Compose: Simplify Multi-Container Apps\",\"datePublished\":\"2026-03-27T16:00:23+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/\"},\"wordCount\":1954,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/#organization\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/\",\"url\":\"https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/\",\"name\":\"Mastering Docker Compose: Simplify Multi-Container Apps - ITSupportWale\",\"isPartOf\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/#website\"},\"datePublished\":\"2026-03-27T16:00:23+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/itsupportwale.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Mastering Docker Compose: Simplify 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":"Mastering Docker Compose: Simplify 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\/mastering-docker-compose-simplify-multi-container-apps-2\/","og_locale":"en_US","og_type":"article","og_title":"Mastering Docker Compose: Simplify Multi-Container Apps - ITSupportWale","og_description":"The smell of ozone and stale coffee is the only thing that stays the same. It was 03:14 AM on a Tuesday in 2014. I was standing in a data center in Secaucus, the floor tiles vibrating under my boots from the sheer CFM of the cooling fans. We had a cluster of HP ProLiant ... Read more","og_url":"https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/","og_site_name":"ITSupportWale","article_publisher":"https:\/\/www.facebook.com\/Itsupportwale-298547177495978","article_published_time":"2026-03-27T16:00:23+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":"13 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/#article","isPartOf":{"@id":"https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/"},"author":{"name":"Techie","@id":"https:\/\/itsupportwale.com\/blog\/#\/schema\/person\/8c5a2b3d36396e0a8fd91ec8242fd46d"},"headline":"Mastering Docker Compose: Simplify Multi-Container Apps","datePublished":"2026-03-27T16:00:23+00:00","mainEntityOfPage":{"@id":"https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/"},"wordCount":1954,"commentCount":0,"publisher":{"@id":"https:\/\/itsupportwale.com\/blog\/#organization"},"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/","url":"https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/","name":"Mastering Docker Compose: Simplify Multi-Container Apps - ITSupportWale","isPartOf":{"@id":"https:\/\/itsupportwale.com\/blog\/#website"},"datePublished":"2026-03-27T16:00:23+00:00","breadcrumb":{"@id":"https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/itsupportwale.com\/blog\/mastering-docker-compose-simplify-multi-container-apps-2\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/itsupportwale.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Mastering Docker Compose: Simplify 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\/4747","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=4747"}],"version-history":[{"count":0,"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/posts\/4747\/revisions"}],"wp:attachment":[{"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/media?parent=4747"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/categories?post=4747"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/itsupportwale.com\/blog\/wp-json\/wp\/v2\/tags?post=4747"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}