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.
Understand the build context before copying files
The dot at the end of the build command is an input location, not decorative punctuation. It selects the directory whose files the builder can use. Docker calls this the build context. The build-context documentation explains how Dockerfile copy operations refer to files supplied in that context.
Run a build from the project directory you intended. If you build from a parent folder, you may send unrelated projects, large downloads, or sensitive configuration to the builder. A Dockerfile in a subdirectory does not automatically limit the context to that subdirectory. When reading a build command, inspect both the Dockerfile location and the final context argument.
Use the ignore file to make that boundary explicit. It should exclude local dependency folders, version-control data, generated artifacts you do not need, and secret files. Check the copied files rather than assuming a successful build proves the selection was correct. An image can start perfectly while still containing material you never meant to distribute.
Follow one change from source to running container
Change the greeting in your JavaScript file and refresh the browser. With the example above, the response will remain the old greeting until you build a new image and run a container from it. Copying source during a build captures its state at that point; the running container is not watching your laptop folder.
Rebuild with the same image tag, stop the old container, and start a new container from the rebuilt image. Restarting the existing container restarts its original filesystem and configuration. It does not replace that container with the newly built image just because both are associated with a familiar name.
This distinction explains a common beginner report: “Docker built successfully, but my app did not change.” First check which image the running container uses. Then check whether you replaced the container after rebuilding. Only after confirming those facts should you investigate browser caching or application-level caches.
For development, a bind mount can expose a host folder inside a container, but that is a different workflow from the copied-source example. Document which workflow your project uses so teammates know whether a code change requires rebuilding. Mixing the two mental models makes stale code surprisingly hard to diagnose.
Diagnose a container that exits immediately
A container stays running while its main process is running. The Python example prints a greeting and completes, so an immediate exit is expected. The JavaScript server should keep listening. If that server exits, inspect the stopped container list and its logs rather than repeatedly starting more anonymous containers.
Read the first meaningful application error. A missing file points toward a copy or working-directory problem. A missing package points toward dependency installation. A permission failure may require checking the runtime user and filesystem ownership. Each symptom identifies a different stage of the image recipe.
A successful build proves the build instructions completed; it does not prove the application can serve requests. Verify separately that the container starts, remains running for a server workload, and answers through the published port. If it answers internally but not from your laptop, inspect the port mapping and the address on which the app listens.
Keep persistent data outside disposable containers
Treat a container as replaceable. Files written into its writable layer are associated with that container and can disappear when it is removed. A named volume gives persistent data a lifecycle separate from an individual container. Docker's volume documentation describes this separation.
Imagine adding an uploaded-file feature to the demonstration server. If uploads live only inside the container, replacing it during a deployment may remove the files. Decide where data belongs before calling the service deployable: a volume, external object storage, or another explicitly managed service. Persistence is a design decision, not an incidental consequence of the first successful run.
For an exercise, create a harmless text file in a running test container, stop and restart that same container, and inspect it. Then compare that behavior with creating a new container from the original image. Use disposable sample data. The difference makes the relationship between image contents, container writes, and external persistence concrete.
What the first image teaches, and what remains
The sample Dockerfile is a learning recipe. A production image also needs deliberate dependency versions, a suitable base-image update policy, an appropriate runtime user, and a clear way to supply configuration. Never assume Docker packaging alone provides application authentication, safe secret storage, or monitoring.
Write a short handoff for your first image: the build command, run command, expected port, expected response, and how to inspect logs. Have a teammate follow it from a fresh project checkout. When those instructions reproduce the greeting, you have demonstrated a portable application package and a troubleshooting path. That is a useful first milestone before introducing registries, orchestration, or deployment automation.
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.
