How to Set Up a Local Development Environment with Docker in Under 30 Minutes
October 01, 2026

How to Set Up a Local Development Environment with Docker in Under 30 Minutes

A local environment that works on your laptop and nowhere else is a tax you pay every time a teammate joins, every time you switch machines, and every time a dependency updates behind your back. Docker fixes most of that, and it does not require a week of study. What follows is the path I use when starting a new project: install the tooling, write one Dockerfile, add one compose file, and confirm the whole thing comes up from a clean start. If you follow along with a small app — a Node API backed by Postgres is what I will use here — you can be finished before your coffee goes cold.

What Docker actually solves

A container is not a virtual machine. It is your application plus its runtime, packaged so that the exact same image runs on your laptop, a colleague's Windows machine, and the staging server. The operating system kernel is shared, so containers start in seconds and stay small.

The practical benefit is that you stop maintaining a wiki page titled "how to set up the project". Node version, database version, environment variables, the order things start in — all of it lives in files that sit next to your code and get reviewed like any other change. That is the whole pitch. The rest is syntax.

Step 1: Install Docker and prove it works

On macOS and Windows, install Docker Desktop from Docker's official site. On Windows, keep the WSL 2 backend enabled during setup; it is noticeably faster with file-heavy projects. On Linux, install Docker Engine from your distribution's repository or from Docker's own package repository, then add yourself to the docker group so you do not need sudo for every command:

sudo usermod -aG docker $USER

Log out and back in for that to take effect. Then verify the install with two commands. The first prints a version; the second pulls a tiny image and runs it:

docker --version

docker run hello-world

If you see the greeting, the daemon is running and you have network access to the registry. If the command hangs, the daemon is usually not started — open Docker Desktop, or run sudo systemctl start docker on Linux. Fix this now rather than debugging it later in the middle of a build.

Step 2: Write your first Dockerfile

Create a file called Dockerfile in the project root. It describes how to build your application image, line by line, and the order matters more than beginners expect because Docker caches each layer.

Start from a specific base image rather than latest: FROM node:20-alpine is small, predictable, and pins the runtime. Then set a working directory with WORKDIR /app, and copy only the dependency manifests before the rest of the source:

COPY package*.json ./

RUN npm ci --omit=dev

Only after that comes COPY . .. The reason for the split is caching: as long as your manifests do not change, Docker reuses the installed dependencies layer and your rebuilds take seconds instead of minutes. Copying everything up front would invalidate the cache on every source edit.

Finish with EXPOSE 3000 and CMD ["node", "server.js"]. Use the JSON array form so your process receives signals properly and stops cleanly when you press Ctrl+C.

Now add a .dockerignore file next to it containing at least node_modules, .git, .env and any build output. This keeps the build context small and, more importantly, stops your host's binaries from overwriting the ones installed inside the image — a classic source of "it works on my machine, except it doesn't" bugs.

Step 3: Add the database with a compose file

One container is a start; real projects need two or three running together. That is what Compose is for. Create docker-compose.yml in the same folder. Modern Docker ships Compose as a plugin, so the command is docker compose with a space, not the older hyphenated docker-compose.

Define two services. The first, call it api, uses build: . to build from your Dockerfile, maps "3000:3000" so the browser can reach it, and reads a DATABASE_URL environment variable pointing at the second service. The second, db, uses image: postgres:16-alpine, sets a password from an env file, and mounts a named volume at /var/lib/postgresql/data so your data survives restarts.

The one detail that trips up nearly everyone: inside the compose network, the database hostname is the service name. Your connection string is postgres://user:pass@db:5432/app, not localhost. Each container has its own loopback interface, so localhost inside the API container refers to the API container itself.

Startup order needs a nudge too. depends_on alone only waits for the container to be created, not for Postgres to accept connections. Add a healthcheck to the database using pg_isready, then use depends_on with condition: service_healthy on the API. Your app will stop crashing on first boot with connection-refused errors.

Step 4: Run it, then run it again from scratch

Bring the stack up with docker compose up --build. The --build flag forces a rebuild after Dockerfile changes; leave it off for faster restarts once things are stable. Open your API in the browser, hit an endpoint, confirm the database responds.

Then do the thing that actually proves reproducibility: tear everything down and start again.

  • docker compose down stops and removes the containers and network, keeping the data volume.
  • docker compose down -v also removes the volume — useful when you want a genuinely clean database.
  • docker compose up --build rebuilds from your committed files and nothing else.

If a clone of the repository in a fresh directory, with no local node_modules and no leftover containers, comes up successfully, you have done the job. That is the test a new teammate will run.

Four snags you will probably hit

Port already allocated

Something on your host is using 3000 or 5432. Change the left side of the mapping — "3001:3000" — and leave the container port alone. Do not open the database port to the host at all unless you need to connect with a GUI client.

The container exits immediately

Check docker compose logs api before changing anything. Nine times out of ten it is a missing environment variable or a wrong database hostname.

Code edits do not show up

By default the image contains a frozen copy of your source. For live reloading during development, mount your source with a bind mount, and add an anonymous volume over /app/node_modules so the container keeps its own dependency folder.

Builds are slow and pull the whole registry

Usually a stray .dockerignore omission. Confirm that node_modules and .git are excluded; sending a few hundred megabytes as build context will make every build feel broken.

Your 30-minute checklist

  1. Install Docker (or Docker Engine on Linux) and run docker run hello-world.
  2. Write a Dockerfile with a pinned base image and dependency layers before source.
  3. Add .dockerignore and a committed .env.example — never the real .env.
  4. Create docker-compose.yml with the app, the database, a healthcheck and a named volume.
  5. Run docker compose up --build, then down -v, then up again.
  6. Write the three commands a new contributor needs into the README, and delete the setup wiki page.

Commit those files and you have turned a half-day of onboarding into a single command. From here, the next useful step is a compose override file for development, so production settings stay strict while your local stack keeps hot reloading and readable logs.

Photo: Pixabay / Pexels

Next Post

What Is a REST API? A Plain-English Explainer for New Developers

Related Posts