·8 min read

How to Install Docker on Ubuntu, macOS and Windows (and Check It Actually Works)

Installing Docker takes ten minutes. Most of the pain comes afterwards: permission errors, a daemon that isn't running, a database container your app can't reach yet. Here's the install plus the fixes.

Ram profile

Ram

Docker logo with container illustration and terminal commands
Share

Installing Docker is the easy part. The time usually goes on what comes after: permission denied on the socket, a daemon that isn't running, or an app container that starts before its database is ready.

This guide covers the install on each OS, then a first Dockerfile and Compose file that follow current practice, and fixes for the problems you're most likely to hit.

Installation commands change now and then. If a step below fails, compare it with Docker's official docs at docs.docker.com/engine/install.

Which one do you need?

  • Linux (Ubuntu, Debian and so on): install Docker Engine directly. It's free and open source, and there's no VM involved.
  • macOS or Windows: install Docker Desktop. Containers need a Linux kernel, so Desktop runs a small Linux VM for you and adds a GUI on top.

Docker Desktop is free for personal use, education, open-source work and small businesses (under 250 employees and under $10M annual revenue). Larger companies need a paid plan. That catches people out when they install it on a work laptop.

Ubuntu: Docker Engine

1. Remove conflicting packages. Ubuntu's own docker.io package and Docker's official one shouldn't be installed side by side:

for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do
  sudo apt-get remove -y $pkg
done

It's fine if apt says none of these are installed.

2. Add Docker's repository:

sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
  https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

3. Install Engine, CLI, Buildx and Compose:

sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

4. Check it:

sudo docker run hello-world

If you see "Hello from Docker!", the engine works.

5. (Optional) Run Docker without sudo:

sudo usermod -aG docker $USER

Then log out and back in. newgrp docker only applies the change to the current shell.

One thing most guides skip: membership of the docker group is effectively root access. Anyone in it can start a container that mounts / and edit any file on the host. That's fine on your own laptop. On a shared server, think twice, or look at Docker's rootless mode.

The service starts automatically on Ubuntu. If it doesn't, run sudo systemctl enable --now docker.

macOS: Docker Desktop

  1. Download Docker Desktop from docker.com. Pick Apple Silicon for M-series Macs, or Intel for older ones.
  2. Open the .dmg, drag Docker into Applications, and launch it.
  3. Wait for the whale icon in the menu bar to stop animating. The docker command only works while Desktop is running.

Or, with Homebrew:

brew install --cask docker
open -a Docker

Then check it:

docker run hello-world
docker compose version

On Apple Silicon, most popular images ship ARM builds. If an image only has an amd64 build, it still runs under emulation, just more slowly. You'll see a platform warning when that happens.

Windows: Docker Desktop with WSL 2

Docker Desktop runs on Windows 10 and 11, including Home edition, using WSL 2 (Windows Subsystem for Linux).

  1. In an administrator PowerShell:

    wsl --install
    

    Restart when it asks. This installs WSL 2 and Ubuntu by default.

  2. Download and run the Docker Desktop installer. Leave Use WSL 2 instead of Hyper-V ticked.

  3. In Docker Desktop, go to Settings → Resources → WSL Integration and enable your Ubuntu distro. Now docker works inside WSL as well as in PowerShell.

  4. Check it:

    docker run hello-world
    

If Desktop says virtualisation is disabled, you need to turn it on in your BIOS/UEFI. It's called Intel VT-x or AMD-V/SVM, depending on your CPU.

A practical tip: keep your project files inside the WSL filesystem (~/projects in Ubuntu), not under /mnt/c/.... File access across that boundary is much slower, and bind-mounted dev servers feel sluggish there.

Commands you'll actually use

docker run -d --name web -p 8080:80 nginx   # start a container in the background
docker ps                                    # running containers (-a for all)
docker logs -f web                           # follow its logs
docker exec -it web sh                       # shell inside it (many images have sh but not bash)
docker stop web && docker rm web             # stop and remove it

docker images                                # images on disk
docker build -t myapp .                      # build from ./Dockerfile
docker system df                             # what's using disk space

A first Dockerfile (Node.js)

FROM node:24-alpine

WORKDIR /app

# Copy only the dependency manifests first, so this layer stays cached
# until package.json or the lockfile actually changes.
COPY package*.json ./
RUN npm ci --omit=dev

COPY . .

# The official Node images include an unprivileged "node" user.
USER node

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

Add a .dockerignore next to it, or COPY . . will copy your local node_modules, your .git folder and your .env file into the image:

node_modules
.git
.env
*.log

Build and run it:

docker build -t my-node-app .
docker run -d -p 3000:3000 --name my-app my-node-app
curl http://localhost:3000

Pin the base image to a version (node:24-alpine, not node:latest) so a rebuild next month gives you the same thing. Check that the Node version you pick is still in LTS support. Node 20 reached end-of-life in April 2026.

A first Compose file (app + Postgres)

services:
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://app:devpassword@db:5432/appdb
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: devpassword
      POSTGRES_DB: appdb
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
      interval: 5s
      retries: 10

volumes:
  pgdata:

Two things in here that trip people up:

  • The hostname is the service name. Inside the Compose network, the app reaches the database at db:5432, not localhost. Inside a container, localhost means the container itself.
  • depends_on alone only waits for the container to start, not for Postgres to be ready to accept connections. The healthcheck plus condition: service_healthy makes the app wait until pg_isready passes. Without it, the app often fails its first connection on a fresh up and only works after a restart.
docker compose up -d --build
docker compose ps
docker compose logs -f web
docker compose down          # stop; add -v to also delete the database volume

When things go wrong

permission denied while trying to connect to the Docker daemon socket Your user isn't in the docker group yet, or you haven't logged out and back in since adding it. In the meantime, use sudo.

Cannot connect to the Docker daemon. Is the docker daemon running? On Linux: sudo systemctl status docker, then sudo systemctl start docker. On macOS or Windows: Docker Desktop isn't running, or it hasn't finished starting.

port is already allocated / address already in use Something else is using that host port. Find it with sudo lsof -i :3000 on Linux/macOS or netstat -ano | findstr :3000 on Windows. Or map a different host port: -p 3001:3000.

Disk filling up Images and build cache pile up quietly. docker system df shows where the space went. docker system prune removes stopped containers, unused networks and dangling images. Add -a to also remove every image not used by a container. Volumes are kept unless you add --volumes, so your database data is safe by default.

My code changes don't show up You rebuilt the image but restarted the old container. With Compose, use docker compose up -d --build. For development, a bind mount (volumes: - .:/app) lets the container see your edits live.

Docker vs a virtual machine, briefly

A VM runs a whole operating system, kernel included, on virtual hardware. A container is an isolated process sharing the host's kernel. That's why containers start in about a second and use much less memory, and why a Linux container needs a Linux kernel (hence the VM inside Docker Desktop). The trade-off is weaker isolation. For running code you don't trust, a VM is the safer boundary.

Once this all works, the Ubuntu install guide is a good companion if you'd rather run Docker Engine on a dedicated Linux machine or VM.

Share

Related Articles

Stay in the loop

Get the latest articles delivered straight to your inbox. No spam, ever.