Skip to main content

How to Set Up Docker for Local Development

Docker Compose for web application development -- service containers, environment parity, volume mounts, networking, and the workflow changes that actually matter.

Category Guide
Read Time 10 min read
Updated June 2026
Steps 6 steps

Who This Guide Is For

This guide is for developers who want a local development environment that matches production, works identically across machines, and can be set up from scratch in minutes. You are building web applications with a backend framework, a relational database, a cache layer, and possibly a queue worker or search engine. You are tired of debugging issues caused by differences between your machine and the server. You understand the command line and have basic familiarity with how web applications are deployed, but you have not yet set up Docker for development or your previous attempts resulted in slow, frustrating workflows that you eventually abandoned.

Before You Start

Install Docker Desktop (macOS, Windows) or Docker Engine (Linux). Docker Desktop includes Docker Compose, which is the primary tool this guide uses. Verify the installation by running docker --version and docker compose version in your terminal.

You should have a working application that currently runs on your local machine using native tooling: a locally installed PHP, Python, or Node runtime, a locally installed database, and so on. The goal is to move these services into containers without changing your application code. If you are starting a new project from scratch, the setup is simpler because there is no existing environment to replicate.

Understand that Docker for local development has a different purpose from Docker for production deployment. In production, containers are optimised for size, security, and immutability. In development, containers are optimised for speed, convenience, and fast feedback loops. The Dockerfile and Compose configuration for development will look different from the production equivalents, and that is intentional.

Step 1: Understand What Docker Replaces

Without Docker, your local development environment is a collection of services installed directly on your operating system. PHP runs as a native binary. MySQL runs as a system service. Redis runs in the background. Each of these was installed separately, configured manually, and is maintained individually. When you join a new team, you spend hours installing and configuring these services to match the versions and settings the project expects.

Docker replaces this with a declarative configuration file, docker-compose.yml, that describes every service the application needs, the versions to use, how they connect to each other, and how they interact with your local file system. Running docker compose up starts all of these services in isolated containers. Running docker compose down stops them and removes the containers. The configuration is version-controlled alongside the application code, so every developer on the team runs the same environment.

The application code itself does not run inside a container that is rebuilt on every change. Your source code is mounted into the container as a volume, which means edits you make in your editor appear inside the container immediately. The container provides the runtime (PHP, Node, Python) and the configuration. Your code remains on your host machine, edited with your normal tools.

Step 2: Define Your Services in Docker Compose

A Docker Compose file describes the services your application depends on. For a typical web application, this includes the application runtime, a database, and usually a cache. Start with the minimum and add services as you need them.

The application service runs your web server and application code. For a PHP application, this is typically an Nginx or Apache container paired with a PHP-FPM container, or a single container that bundles both. For a Node application, this is a container running your Node process. The key configuration is the volume mount that maps your local source code directory into the container.

The volume mount is the most important line in your Compose file. It looks like ./:/var/www/html (mapping the current directory to the web root inside the container) and ensures that file changes on your host are reflected inside the container without rebuilding. For interpreted languages like PHP and Python, this means changes take effect immediately. For compiled or transpiled languages, you need a file watcher running inside the container.

The database service runs your database engine. Use the official image for your database (mysql, postgres, mariadb) and pin the version to match production. Configure the database name, user, and password through environment variables. Use a named volume for the database data directory so your data persists when the container is stopped and restarted.

Without a named volume, stopping the database container destroys all data. This is occasionally useful for testing (you get a clean database on every restart) but generally frustrating during development. Named volumes are the default for a reason.

The cache service (Redis, Memcached) is straightforward. The official Redis image requires almost no configuration for development use. It listens on port 6379 by default and your application connects to it using the service name as the hostname.

Additional services depend on your stack. A queue worker might be another instance of your application container running a different command (processing the queue instead of serving HTTP). A search engine like Meilisearch or Elasticsearch runs as its own container. A mail catcher like Mailpit captures outbound emails and provides a web interface to view them. Each service is a block in the Compose file.

Step 3: Configure Networking and Ports

Docker Compose creates a network for your services automatically. Each service is reachable from other services using its service name as a hostname. If your database service is named db, your application connects to the database at host db on the standard port. This is simpler and more reliable than using localhost or IP addresses.

Port mapping exposes container ports to your host machine so you can access services from your browser or database client. The syntax 8080:80 maps port 80 inside the container to port 8080 on your host. You access the application at http://localhost:8080.

Choose host ports that do not conflict with services already running on your machine. If you have a native MySQL instance running on port 3306, map the containerised MySQL to a different port like 33060. Your application inside the container still connects to db:3306 (the internal port) while your desktop database client connects to localhost:33060.

Avoid exposing unnecessary ports. Services that only need to communicate with other containers (like Redis used only by the application) do not need port mappings. Expose only the ports you need to access from outside the Docker network: the web server for your browser, the database for your GUI client, and the mail catcher for viewing emails.

Step 4: Handle Environment Variables and Configuration

Your application’s configuration needs to change between environments. The database host is db in Docker but 127.0.0.1 when running natively. Environment variables are the correct mechanism for this.

Use an .env file for Docker-specific configuration. Most frameworks already read environment variables from an .env file. Create a Docker-specific version (or adjust your existing one) that points to the Docker service names. The database host becomes db, the Redis host becomes redis, the mail host becomes mailpit.

Do not hardcode Docker-specific values into your application configuration. The configuration should read from environment variables so it works in Docker, in CI, and in production without changes. If your database configuration file contains 'host' => 'db', it will break outside Docker. If it contains 'host' => env('DB_HOST', '127.0.0.1'), it works everywhere.

Compose supports environment variable interpolation. You can reference variables from a .env file in your docker-compose.yml, which avoids duplicating values. The database password defined in .env can be referenced in the Compose file when configuring the database container, keeping the single source of truth.

Keep secrets out of the Compose file. Database passwords, API keys, and other sensitive values should come from the .env file, which is excluded from version control. The Compose file itself should be committed; it is configuration, not secrets.

Step 5: Manage Dependencies and Build Steps

Most web applications have dependencies that need to be installed: PHP packages via Composer, Node modules via npm, Python packages via pip. These dependencies need to be available inside the container.

Mount your vendor directory from the host for interpreted languages where you manage dependencies locally. If you run composer install or npm install on your host machine, the resulting vendor/ or node_modules/ directory is available inside the container through the volume mount. This is the simplest approach and avoids the performance overhead of running package managers inside containers.

Run dependency installation inside the container if you want complete isolation or if your dependencies include compiled extensions that are platform-specific. This means running docker compose exec app composer install instead of composer install. The dependencies are installed for the container’s platform and architecture, which eliminates the “works on my machine” problem for native extensions.

Dockerfiles for custom images are necessary when the official images do not include everything you need. A PHP project might need extensions (GD, PDO MySQL, Redis) that are not in the base PHP image. Create a Dockerfile that extends the official image and installs the required extensions. Reference this Dockerfile in your Compose file using the build directive instead of the image directive.

Keep development Dockerfiles fast to build. Use multi-stage builds only for production images. For development, a single stage that installs everything you need is faster to build and easier to debug. Cache dependency installation layers by copying the lock file before the source code so Docker can skip the installation step when dependencies have not changed.

Step 6: Optimise the Development Workflow

A Docker setup that works but feels slow will be abandoned within a week. Performance and workflow ergonomics determine whether your team actually adopts it.

File system performance on macOS and Windows is the single biggest complaint about Docker for development. The volume mount that makes your source code available inside the container introduces overhead on non-Linux hosts because Docker translates between the host file system and the container’s Linux file system. For large projects with thousands of files, this can make requests noticeably slower.

Mitigations include: excluding directories that do not need to be synchronised (like node_modules/ or vendor/ if installed inside the container), using Docker’s built-in file sync mechanisms (virtiofs on macOS, which is now the default in recent Docker Desktop versions), and keeping only the application code in the mount while serving static assets from the container’s own file system.

Use docker compose exec for interactive commands. Instead of opening a shell inside the container and running commands there, use exec to run one-off commands from your host terminal. docker compose exec app php artisan migrate runs the migration inside the application container and returns the output to your host terminal. This feels natural and integrates with your existing workflow.

Hot reloading and file watching should work through the volume mount. Vite, Webpack Dev Server, and similar tools detect file changes and rebuild automatically. If file watching does not work through the mount (rare on modern Docker Desktop), configure the watcher to use polling instead of filesystem events. Polling is slower but reliable across all volume mount types.

Logging and debugging are simpler when you configure your application to write logs to stdout/stderr rather than to files inside the container. Docker captures container output and makes it available through docker compose logs. You can tail all services with docker compose logs -f or a single service with docker compose logs -f app.

Database GUI tools connect to the containerised database through the exposed port. Your existing database client works exactly as before, with the only difference being the port number. Migrations, seeders, and database inspection tools run through docker compose exec as described above.

Common Mistakes

  • Using Docker in development but not matching production versions. If production runs PHP 8.2 on MySQL 8.0 and your Docker setup runs PHP 8.3 on MySQL 8.4, you have environment parity in theory but not in practice. Pin versions explicitly.
  • Not using named volumes for database data. Without a named volume, your development database is destroyed every time you run docker compose down. You will learn this the hard way exactly once.
  • Rebuilding containers after every code change. The volume mount exists specifically so you do not need to rebuild. If you find yourself running docker compose build frequently, your volume mount is misconfigured or you are confusing development workflow with production deployment.
  • Running everything inside the container. Your editor, your Git client, and your browser run on your host. Only the runtime services (web server, database, cache, queue) run in containers. Docker replaces your server environment, not your workstation.
  • Complex Compose files for simple projects. A project that needs PHP and MySQL does not need Nginx, Redis, Meilisearch, Mailpit, and a queue worker on day one. Start minimal and add services when your application actually uses them.

What Good Looks Like

A well-configured Docker development environment starts with a single command and is usable within thirty seconds. New team members clone the repository, copy the example .env file, run docker compose up, and have a working application. The environment matches production in all meaningful ways: same language version, same database engine, same service topology. File changes are reflected immediately. The Compose file is version-controlled and maintained alongside the application code. Developers do not think about Docker during their day. It is invisible infrastructure that just works.

Next Steps

For the CI pipeline that runs your tests inside similar containers, see How to Set Up CI/CD for a Laravel Project. For setting up a staging environment that mirrors your Docker-defined production architecture, How to Set Up a Staging Environment covers the approach. For monitoring the servers that run your containers in production, How to Configure Server Monitoring covers the observability layer. Browse the full Technical Guides index for related coverage across the stack.

Written by

Alex

CEO

I’m a software developer and CEO of Digital Royalty, helping growing teams scale their SaaS platforms without losing quality, visibility, or control. I focus on building structured, maintainable systems with clear processes, reporting, and accountability. With over a decade of experience across agency and in-house roles, I specialise in delivering long-term, scalable solutions that support complex, evolving products.

Portrait of Alexander De Sousa, founder of Digital Royalty
Founder-led
“I’ve put everything I know into how this company works — the standards, the method, the care on every project. It runs through the whole team, and I hold us all to it.”

Alexander De Sousa · Founder LinkedIn

Featured on BBC Radio Solent

Get started

Tell us what you need

A few quick questions, then a straight answer from a real person — usually within a few hours.

Tell us what you're working on

Whether it's a new site, a platform, or a process that shouldn't be manual any more — we'll tell you honestly if we can help.