Sign in with Google

How to Set Up Docker and Run Your First Container

Install it, understand the three nouns, run a real container, and stop it from eating your disk.

intermediate10 min read
dev-setupdockercontainersdevopsdeveloper-tools

The app runs on your laptop. You push it, and it fails on the server with an error about a library version — the server has Python 3.9 and OpenSSL 1.1 while you have 3.13 and 3.4. Docker exists to make that afternoon impossible: you ship the dependency versions along with the code, so the thing running on the server is the exact thing you tested. This guide gets Docker installed, runs a real container, and covers the commands you will type every day.
iWhat you need
A 64-bit machine with hardware virtualization available, at least 4 GB of RAM on macOS or 8 GB on Windows, roughly 5 GB of free disk space, and comfort running commands in a terminal. Some familiarity with a project you can containerize helps but is not required — the examples below stand alone.
Nearly all Docker confusion comes from mixing up three words. Learn them once and the command names stop looking arbitrary.

Images, containers, volumes

  1. 1An image is a frozen, read-only filesystem snapshot: an OS base layer plus your dependencies plus your code plus the command to start it. Images are built, tagged, pushed, and pulled. Think of it as a class definition.
  2. 2A container is a running instance of an image, with its own isolated process list, network interface, and writable scratch layer. You start, stop, and delete containers. Think of it as an object created from that class — one image can run as twenty containers.
  3. 3A volume is storage that lives outside any container's lifecycle. Delete a container and its writable layer is destroyed with it; a mounted volume survives. This is the single most expensive lesson people learn the hard way.
So docker build makes an image, docker run creates a container from one, and docker volume manages the storage that outlives both. Every command below is one of those three nouns with a verb attached.
Docker Desktop bundles the Docker engine, the CLI, a virtual machine to run Linux containers in, and a dashboard. On macOS and Windows the VM is mandatory — containers are a Linux kernel feature, so there is always a Linux VM underneath even when you never see it.

By platform

  1. 1macOS: download the Apple Silicon or Intel build from docs.docker.com and drag it to Applications. Docker supports the current and two previous major macOS releases, and needs at least 4 GB of RAM. Rosetta 2 is no longer strictly required on Apple Silicon, though a handful of optional AMD64 command-line tools still want it.
  2. 2Windows: you need Windows 10 22H2 build 19045 or Windows 11 23H2 build 22631 or newer, on Enterprise, Pro, or Education, with 8 GB of RAM and virtualization enabled in BIOS or UEFI. The installer defaults to the WSL 2 backend, which needs WSL version 2.1.5 or later — run wsl --update first if in doubt.
  3. 3Linux: install Docker Engine from Docker's own apt or dnf repository instead of Desktop. There is no GUI and no VM, because your kernel is already the right kernel. This is the leanest option and what production servers run.
  4. 4After installing on Linux, add yourself to the docker group with sudo usermod -aG docker $USER, then log out and back in. Otherwise every command needs sudo.
!Docker Desktop is not free for large companies
Commercial use of Docker Desktop in an organization with more than 250 employees or more than $10 million USD in annual revenue requires a paid subscription. It stays free for small businesses, personal use, education, and non-commercial open source. Docker Engine on Linux is Apache-2.0 licensed and carries no such term — which is one reason Linux servers use Engine directly.
Start Docker Desktop and wait for the whale icon to stop animating, then run the canonical smoke test.
macOS
$docker run hello-world
The output starts with Unable to find image 'hello-world:latest' locally, which is not an error — Docker checked your local image cache, missed, and is now pulling from Docker Hub. Then it prints a confirmation and exits. Four things just worked: the CLI reached the daemon, the daemon reached the registry, a container was created from the pulled image, and its process ran and terminated cleanly. When this fails, it is almost always step one — the daemon is not running.
hello-world exits immediately. A web server should keep running and be reachable from your browser, which needs two more flags.
macOS
$docker run -d -p 8080:80 --name web nginx
Open http://localhost:8080 and you get the nginx welcome page. Each flag earns its place: -d detaches so the container runs in the background instead of holding your terminal, -p 8080:80 maps port 8080 on your machine to port 80 inside the container, and --name web gives it a readable name so you stop copying container IDs around.
The port order is host:container
Host port on the left, container port on the right. Getting it backwards is the most common cause of "the container is running but nothing loads." If 8080 is taken, change only the left number — -p 9000:80 works because nginx still listens on 80 inside.
Five commands cover most of what you do once containers are running. Learn these and you can debug almost anything without opening the dashboard.

Inspect, enter, stop, remove

  1. 1docker ps lists running containers. docker ps -a also lists stopped ones — this is where the container you thought vanished actually went.
  2. 2docker logs web prints everything the container wrote to stdout and stderr. Add -f to follow it live, which is how you watch a crash happen.
  3. 3docker exec -it web bash opens an interactive shell inside the running container. Use it to check whether a config file really landed where you think. Some minimal images ship no bash, in which case use sh.
  4. 4docker stop web sends the process a graceful shutdown signal. docker start web brings the same container back with its writable layer intact.
  5. 5docker rm web deletes the stopped container permanently. Add -f to stop and remove in one step.
Images accumulate silently. docker pull postgres:17 fetches one without running it, docker images lists what you have with sizes, and docker rmi deletes one by name or ID. Six months in, most people discover 40 GB of dangling layers and build cache.
macOS
$docker system df
That shows exactly what is reclaimable, broken down by images, containers, volumes, and build cache. To actually reclaim it, run docker system prune, which removes stopped containers, unused networks, dangling images, and build cache. It leaves named volumes alone unless you add --volumes, and that omission is deliberate — read the next section before you add that flag.
A Dockerfile is a recipe: start from a base image, copy your code in, install dependencies, declare the start command. Here is a working one for a small Node service.
dockerfile
FROM node:24-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --omit=dev

COPY . .

EXPOSE 3000
CMD ["node", "index.js"]

Dockerfile — copy package files first so dependency installs stay cached

Build it with docker build -t myapp . — the -t tags the image and the trailing dot is the build context, the directory Docker is allowed to copy from. Then run it exactly like nginx: docker run -d -p 3000:3000 --name myapp myapp. Copying package*.json before the rest of the source is deliberate: while dependencies are unchanged, Docker reuses the cached npm ci layer and rebuilds take seconds.
Add a .dockerignore
Without one, the build context includes node_modules and .git, which makes builds slow and can bake stale dependencies into the image. Create a .dockerignore listing node_modules, .git, and any local env files, the same way you would a .gitignore.
Real apps are more than one container — an API plus a database, at minimum. Compose describes the set in one file and starts them on a shared network where they reach each other by service name.
yaml
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://postgres:secret@db:5432/app
    depends_on:
      - db

  db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

compose.yaml — the app reaches Postgres at the hostname 'db'

macOS
$docker compose up -d
Note the space: docker compose is the current form, a plugin built into the Docker CLI. The old hyphenated docker-compose was a separate Python binary and is not what you should install today. Tear the set down with docker compose down, which removes the containers and network but keeps named volumes.
Here is the concrete failure. Run docker run -d --name db postgres:17 with no volume, insert a hundred rows, then docker rm -f db and start it again. Every row is gone — the writable layer belonged to the container you deleted. The pgdata volume above prevents that: it lives independently, survives docker compose down, and reattaches on the next up.
prune --volumes deletes your data
docker system prune --volumes and docker volume prune remove volumes not currently attached to a container. If your database is stopped at that moment, its volume counts as unused and goes with it. Run docker volume ls first and know what each one holds.

Symptom, cause, fix

  1. 1Cannot connect to the Docker daemon. Docker Desktop is not running, or on Linux you are not in the docker group. Start Desktop and wait for the icon to settle, or run the usermod step and re-login.
  2. 2Bind for 0.0.0.0:8080 failed, port is already allocated. Something else holds that host port, often an earlier container. Run docker ps to find it, or pick a different host port.
  3. 3The container exits immediately after starting. Its main process finished or crashed. Run docker logs with the container name to see why — a typo in CMD is the usual cause.
  4. 4The build ignores your code change. A cached layer is being reused when it should not be. Rebuild with --no-cache and check that your COPY ordering is not caching more than you intended.
  5. 5Your disk is full and nothing obvious is large. On macOS and Windows, Docker keeps its data inside a VM disk image. Run docker system df, then prune, and check the disk limit in Settings under Resources.
  6. 6The database is empty after a restart. No volume was mounted. Add one before you put anything in the database you care about.
The Dockerfile above assumes a Node runtime you also want locally for fast iteration — How to Install Node.js covers that, and its lockfile section explains the npm ci line. A Dockerfile belongs in version control from the first commit, so pair this with How to Use Git, and lean on How to Use the Command Line if the prompt still slows you down. To place containers in the wider picture of shipping software, the Software Development roadmap sequences build tooling and deployment against the fundamentals underneath them.