I was pairing with a teammate who had a classic starter project checklist on a sticky note. Install this, pin that, flush these ports, pray to the demo gods. Their local MySQL kept fighting with a forgotten background service. After twenty minutes of whack a mole, we wiped the slate, wrote a tiny Compose file, and brought the entire stack up in one shot. Same versions for both of us, clean networking, nothing leaking on the host. The sticky note went into the bin. That moment sold me on local development with Compose.
Docker Compose gives you a simple way to describe your app as a set of services and run them together on your laptop. The newer file format introduced native networks and named volumes, so service names become stable DNS entries out of the box and storage does not vanish by accident. For folks on Mac and Windows today that means the stack runs inside a small VM managed by Docker tools, and Compose does the orchestration inside. One file becomes the living sketch of your project. You define the app image, the database, a cache, maybe a mail catcher, all wired through a private network. No more scattered bash scripts or a README that drifts with every commit. When someone asks how to run the project, the answer is just run Compose and watch the logs. It feels boring in the best way.
State is where local setups usually go sideways, so treat it with intent. Use named volumes for databases and anything you want to keep between runs. That keeps your data separate from containers, which means you can rebuild services without blowing away a week of test rows. Keep a repeatable seed path in mind. A small script that runs migrations and loads fixtures right after the stack comes up makes onboarding smooth and keeps reviews honest. Compose gives you dependencies to express who starts before who, though service readiness is not the same as readiness checks. A database might accept TCP and still be busy warming up. Solve that at the app level with a simple wait routine or a retry loop. Your future self will thank you during a demo. For port binding, only expose what you need, and prefer defaults that do not clash with common local ports. Or run the database only on the internal network and talk to it by service name inside the app container. Less noise on the host means fewer support messages from teammates who got burned by a phantom process.
Speed decides whether the team loves containers or quietly bypasses them. Compose is not magic. It reflects your choices. Keep images lean, pin base images, and avoid work that breaks the build cache for every small change. If your language supports live reload, mount your code into the app container and let the watcher do its job so you can edit and refresh without a rebuild. On Mac, file sharing goes through a virtual machine, which can slow heavy write loads. Two pragmatic moves help a lot. First, move noisy directories like build and vendor inside the container so they do not traverse the shared path. Second, prefer watchers that poll less often or ignore folders that churn. For web apps, use a local bind for the template layer and keep dependencies baked into the image. That pattern makes hot reload snappy while avoiding the slowest file activity. Measure with a stop watch and tune before declaring containers slow.
Compose shines when teams stop treating it as an afterthought. Put the file in the repo root and commit it. Expand it thoughtfully with an override pattern so everyone can keep local only tweaks without dirtying the main file. Share an .env sample with safe defaults and document the few variables that change per developer. Use tags for your images so everyone runs the same base instead of chasing the latest from the registry. In continuous integration, spin up the exact same set of services and run tests against the stack, then bring it down and remove volumes that are only for the pipeline. This reveals flaky assumptions that a plain unit run would miss. For troubleshooting, lean on service names, not container IDs. Follow logs from all services in a single view, and when you are done with an experiment, clean old containers and orphaned volumes so your next run starts fresh. Keep a small set of known commands in a Makefile or a script so teammates do not need to memorize the tool belt. That tiny wrapper pays off every week.
Quick wrap up. Local development with Compose replaces tribal setup lore with a clear, versioned document. Describe services and let Compose handle wiring, networking, and repeatable starts. Protect state with named volumes and a seed path. Watch service readiness, not just start order. Tune for speed with smaller images and smart mounts so your edit to refresh loop stays tight. Share the main file, keep overrides local, and standardize a handful of commands that people can learn once and forget. The goal is not to show off container tricks. The goal is to give every developer a boring, dependable way to run the same stack as everyone else without a pile of post it notes. That consistency is what makes shipping feel calm. And calm teams ship more.