Docker lets you package an application and everything it needs to run — code, runtime, libraries, settings — into a single portable unit called a container. That container runs the same way on any machine that has Docker installed.
This guide explains the core concepts, walks you through installing Docker, and gets you to a running container with your own Dockerfile.
Three concepts you need before anything else
Image — a read-only template that describes what your container will contain: the OS layer, the runtime (Node, Python, etc.), your code, and any configuration.
Container — a running instance of an image. Like a process that was spawned from the image blueprint.
Dockerfile — a text file with instructions that tell Docker how to build an image. Step by step: start from this base image, copy these files, install these packages, run this command.
The relationship:
Dockerfile → (build) → Image → (run) → Container
Step 1: Install Docker Desktop
Docker Desktop is the easiest way to get Docker running on your machine. It includes Docker engine, CLI, and a GUI dashboard.
macOS
- Go to docker.com/products/docker-desktop
- Download the macOS version for your chip — Apple Silicon (M1/M2/M3/M4) or Intel
- Open the
.dmg, drag Docker to Applications - Open Docker from Applications — it runs in the menu bar
Windows
- Go to docker.com/products/docker-desktop
- Download the Windows installer
- Run the installer — it will enable WSL 2 (Windows Subsystem for Linux) if not already on
- Restart if prompted
- Open Docker Desktop from the Start menu
Verify installation
Open your terminal (Terminal on Mac, PowerShell or Command Prompt on Windows):
docker --version
You should see something like Docker version 27.x.x. Also check:
docker run hello-world
Docker will pull the hello-world image and run it. If you see "Hello from Docker!", everything works.
Step 2: Understand the Dockerfile
A Dockerfile is a recipe. Here is the most common pattern:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]
Line by line:
| Instruction | What it does |
|---|---|
FROM node:20-alpine | Start from the official Node.js 20 image (Alpine Linux variant — small and fast) |
WORKDIR /app | Set the working directory inside the container to /app |
COPY package*.json ./ | Copy your package.json into the container |
RUN npm install | Run this command during the image build — installs dependencies |
COPY . . | Copy all your project files into the container |
EXPOSE 3000 | Tell Docker your app listens on port 3000 |
CMD ["node", "index.js"] | The command that runs when the container starts |
Step 3: Build your first image
Create a project folder and navigate into it:
mkdir my-docker-app
cd my-docker-app
Create a simple index.js file:
cat > index.js << 'EOF'
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello from Docker!\n');
});
server.listen(3000, () => {
console.log('Server running on port 3000');
});
EOF
Create a package.json:
cat > package.json << 'EOF'
{
"name": "my-docker-app",
"version": "1.0.0",
"main": "index.js"
}
EOF
Create the Dockerfile (no extension):
cat > Dockerfile << 'EOF'
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]
EOF
Now build the image:
docker build -t my-docker-app .
-t my-docker-app— gives the image the namemy-docker-app.— tells Docker to look for the Dockerfile in the current directory
You'll see Docker execute each step. The first build takes a minute because it downloads the base image. Subsequent builds are much faster because Docker caches each layer.
Step 4: Run your container
docker run -p 3000:3000 my-docker-app
-p 3000:3000— maps port 3000 on your laptop to port 3000 inside the container
Open your browser and go to http://localhost:3000. You'll see "Hello from Docker!"
Press Ctrl + C to stop the container.
Step 5: Run in the background (detached mode)
The previous command ran the container in your terminal window. To run it in the background:
docker run -d -p 3000:3000 --name my-app my-docker-app
-d— detached mode (runs in the background)--name my-app— gives the container a human-readable name
The container is running now without occupying your terminal. Check it:
docker ps
This lists all running containers. You'll see my-app in the list.
Essential Docker commands
# List running containers
docker ps
# List all containers (including stopped ones)
docker ps -a
# List all local images
docker images
# Stop a running container
docker stop my-app
# Start a stopped container
docker start my-app
# Remove a container
docker rm my-app
# Remove an image
docker rmi my-docker-app
# View container logs
docker logs my-app
# Follow logs in real time
docker logs -f my-app
# Run a command inside a running container
docker exec -it my-app sh
Step 6: Add a .dockerignore file
Just like .gitignore tells Git which files to ignore, .dockerignore tells Docker which files to exclude from the image build. This keeps images small and prevents accidentally copying secrets.
Create .dockerignore:
node_modules
.env
.git
*.log
Dockerfile
.dockerignore
Always exclude node_modules — they get installed inside the container during RUN npm install, so you don't need to copy them in from your machine.
Step 7: Build a Python image (same pattern, different base)
The Dockerfile pattern is the same for any language. Here's a Python example:
Create a new folder:
mkdir my-python-app
cd my-python-app
Create app.py:
print("Hello from a Python container!")
Create requirements.txt (empty for now):
touch requirements.txt
Create the Dockerfile:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
Build and run:
docker build -t my-python-app .
docker run my-python-app
You'll see "Hello from a Python container!" printed in your terminal.
What changes between runs vs rebuilds
| Scenario | What to do |
|---|---|
| Changed code, same dependencies | docker build again — the COPY . . layer rebuilds |
| Added new packages | Update package.json or requirements.txt, then docker build again |
| Just want to restart the app | docker stop my-app && docker start my-app |
| Want a fresh container from the same image | docker run again |
The layered caching means rebuilds are fast when only your code changes — Docker reuses the cached layers for npm install / pip install.
What to learn next
- Docker Compose — run multiple containers together (e.g., your app + a database) with a single
docker-compose.ymlfile - Docker Hub — publish your images publicly so anyone can pull them with
docker pull - Volumes — persist data between container restarts
- Environment variables — pass config to containers with
-e KEY=valueor a.envfile - Multi-stage builds — build images with a compiler stage and a smaller runtime stage, reducing final image size
The core skill you've built here — Dockerfile → build → run — is the foundation everything else sits on.