# Docker Tutorial for Developers: From Basics to Full-Stack Deployment

Docker is a platform that allows developers to package applications and their dependencies into containers, making them easier to run across different environments.

Suppose you build a full-stack application using React, Node.js, and PostgreSQL. It works on your computer but fails on another machine because of different runtime versions, missing dependencies, or incorrect configuration.

Docker helps solve this problem by packaging the application environment into a reusable image.

In this guide, you will learn the Docker concepts and commands most useful for development, deployment, and technical interviews.

## What Is Docker?

Docker is a platform for building, distributing, and running containerized applications.

A container packages an application's filesystem and runtime configuration while isolating its processes from other applications.

Docker is commonly used for:

*   Maintaining consistent development environments.
    
*   Deploying applications across different environments.
    
*   Running frontend, backend, and database services independently.
    
*   Automating builds and deployments through CI/CD.
    
*   Simplifying application setup and onboarding.
    

## Docker Architecture

Docker uses a client-server architecture.

*   **Docker CLI:** Accepts commands such as `docker build` and `docker run`.
    
*   **Docker daemon:** Manages images, containers, networks, and volumes.
    
*   **Docker image:** A packaged template used to create containers.
    
*   **Docker container:** A running or stopped instance of an image.
    
*   **Docker registry:** Stores and distributes images, such as Docker Hub.
    

The basic workflow is:

```text
Source Code
    |
    v
Dockerfile
    |
    v
Docker Image
    |
    v
Docker Container
    |
    v
Running Application
```

## Dockerfile vs. Image vs. Container

| Concept | Meaning |
| --- | --- |
| Dockerfile | Instructions for building an image |
| Image | Packaged application environment |
| Container | Instance created from an image |
| Registry | Storage for distributing images |
| Volume | Storage that can persist beyond a container's lifecycle |
| Network | Enables communication between containers |

**Interview point:** A Dockerfile builds an image, and an image is used to create containers.

## Installing Docker

Install Docker Desktop on Windows or macOS, or Docker Engine on Linux.

Official installation guides:

*   [Docker Desktop for Windows](https://docs.docker.com/desktop/setup/install/windows-install/)
    
*   [Docker Desktop for macOS](https://docs.docker.com/desktop/setup/install/mac-install/)
    
*   [Docker Engine for Linux](https://docs.docker.com/engine/install/)
    

Verify your installation:

```bash
docker --version
docker version
docker info
docker run hello-world
```

The `hello-world` command checks whether Docker can successfully run a test container.

## Essential Docker Commands

### Working with images

```bash
docker pull nginx:stable
docker images
docker build -t my-app:1.0 .
docker tag my-app:1.0 username/my-app:1.0
docker push username/my-app:1.0
docker rmi my-app:1.0
docker history my-app:1.0
```

*   `pull`: Downloads an image.
    
*   `images`: Lists local images.
    
*   `build`: Builds an image from a Dockerfile.
    
*   `tag`: Assigns another image reference.
    
*   `push`: Uploads an image to a registry.
    
*   `rmi`: Removes an image when it is no longer referenced by containers.
    
*   `history`: Displays image layers and build history.
    

### Working with containers

```bash
docker run -d --name my-app -p 3000:3000 my-app:1.0
docker ps
docker ps -a
docker logs my-app
docker logs -f my-app
docker exec -it my-app sh
docker stop my-app
docker start my-app
docker restart my-app
docker rm my-app
docker inspect my-app
docker stats
```

Important distinctions:

*   `docker run` creates and starts a new container.
    
*   `docker start` starts an existing stopped container.
    
*   `docker ps` lists running containers.
    
*   `docker ps -a` also lists stopped containers.
    
*   `docker exec` runs a command inside a running container.
    
*   `docker logs` displays container output.
    
*   `docker stop` stops a container without deleting it.
    

### Understanding port mapping

```bash
docker run -d -p 8080:80 nginx:stable
```

The syntax is:

```text
-p HOST_PORT:CONTAINER_PORT
```

Here, port 8080 on the host forwards traffic to port 80 inside the container.

`EXPOSE` in a Dockerfile documents a container port; it does not publish that port to the host.

## Writing a Dockerfile

A Dockerfile defines how Docker builds an application image.

Consider a Node.js application:

```dockerfile
FROM node:22-bookworm-slim

WORKDIR /app

COPY package*.json ./

RUN npm ci

COPY . .

ENV NODE_ENV=production

EXPOSE 3000

CMD ["npm", "start"]
```

### Important Dockerfile instructions

| Instruction | Purpose |
| --- | --- |
| `FROM` | Selects the base image |
| `WORKDIR` | Sets the working directory |
| `COPY` | Copies files into the image |
| `RUN` | Executes commands during image building |
| `ENV` | Sets environment variables |
| `ARG` | Defines build-time variables |
| `EXPOSE` | Documents the intended container port |
| `CMD` | Defines the default startup command |
| `ENTRYPOINT` | Defines the primary executable |
| `USER` | Selects the user for subsequent execution |

### CMD vs. ENTRYPOINT

`CMD` defines the default command or arguments for a container.

```dockerfile
CMD ["node", "server.js"]
```

`ENTRYPOINT` defines the primary executable.

```dockerfile
ENTRYPOINT ["node"]
CMD ["server.js"]
```

Together, these instructions run `node server.js` by default.

The exec form, which uses a JSON array, is generally preferred for application startup because it handles process signals more predictably.

### RUN vs. CMD

`RUN` executes during image building.

```dockerfile
RUN npm ci
```

`CMD` executes when the container starts.

```dockerfile
CMD ["npm", "start"]
```

## Building and Running Your First Docker Image

Create a Node.js project:

```bash
mkdir docker-demo
cd docker-demo
npm init -y
npm install express
```

Create `server.js`:

```javascript
const express = require("express");

const app = express();
const PORT = process.env.PORT || 3000;

app.get("/", (req, res) => {
  res.json({ message: "Hello from Docker!" });
});

app.listen(PORT, "0.0.0.0", () => {
  console.log(`Server running on port ${PORT}`);
});
```

Create a `Dockerfile`:

```dockerfile
FROM node:22-bookworm-slim

WORKDIR /app

COPY package*.json ./

RUN npm ci --omit=dev

COPY server.js ./

ENV PORT=3000

EXPOSE 3000

CMD ["node", "server.js"]
```

Create `.dockerignore`:

```text
node_modules
.git
.env
npm-debug.log
coverage
```

Build the image:

```bash
docker build -t docker-demo:1.0 .
```

Run the container:

```bash
docker run -d --name docker-demo -p 3000:3000 docker-demo:1.0
```

Test the application:

```bash
curl http://localhost:3000
```

Expected response:

```json
{"message":"Hello from Docker!"}
```

Inspect the logs:

```bash
docker logs docker-demo
```

Stop and remove the container:

```bash
docker stop docker-demo
docker rm docker-demo
```

## Containerizing a Frontend Application

A React application built with Vite typically generates static files that can be served using Nginx.

Create a React application if you do not already have one:

```bash
npm create vite@latest frontend -- --template react
cd frontend
npm install
```

Create `frontend/Dockerfile`:

```dockerfile
FROM node:22-bookworm-slim AS builder

WORKDIR /app

COPY package*.json ./

RUN npm ci

COPY . .

RUN npm run build

FROM nginx:stable-alpine

COPY --from=builder /app/dist /usr/share/nginx/html

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
```

Create `frontend/.dockerignore`:

```text
node_modules
dist
.git
.env
```

Build the image from the parent directory:

```bash
docker build -t my-frontend:1.0 ./frontend
```

Run the container:

```bash
docker run -d --name frontend -p 8080:80 my-frontend:1.0
```

Open `http://localhost:8080`.

### Why use Nginx for frontend applications?

A production React application usually contains static HTML, CSS, and JavaScript files. Nginx can serve these files without requiring a Node.js server at runtime.

Next.js applications that require server-side rendering or dynamic server functionality generally need a Node.js runtime instead.

## Containerizing a Backend Application

A backend application needs its runtime, dependencies, and startup command.

A typical backend project contains:

```text
backend/
├── server.js
├── package.json
├── package-lock.json
└── Dockerfile
```

Create `backend/Dockerfile`:

```dockerfile
FROM node:22-bookworm-slim

WORKDIR /app

COPY package*.json ./

RUN npm ci --omit=dev

COPY . .

ENV NODE_ENV=production
ENV PORT=4000

EXPOSE 4000

CMD ["npm", "start"]
```

Ensure the package manifest defines a valid `start` script and the application listens on port 4000 at `0.0.0.0`.

Build the image:

```bash
docker build -t my-backend:1.0 ./backend
```

Run it:

```bash
docker run -d --name backend -p 4000:4000 my-backend:1.0
```

The API is accessible at `http://localhost:4000`.

### Passing environment variables

```bash
docker run -d \
  --name backend \
  -p 4000:4000 \
  -e NODE_ENV=production \
  -e PORT=4000 \
  my-backend:1.0
```

Your application can read these values through `process.env`.

Never bake private API keys or production passwords into a Docker image.

## Running a Database Container

Docker provides official images for databases such as PostgreSQL and MySQL.

For PostgreSQL:

```bash
docker run -d \
  --name postgres-db \
  -e POSTGRES_USER=appuser \
  -e POSTGRES_PASSWORD=local-dev-password \
  -e POSTGRES_DB=appdb \
  -p 127.0.0.1:5432:5432 \
  postgres:17
```

This is a local development example. Use secure credentials and suitable secret management in production.

Check the database logs:

```bash
docker logs postgres-db
```

Connect to PostgreSQL:

```bash
docker exec -it postgres-db psql -U appuser -d appdb
```

### Persisting database data

Use a named volume to keep database files outside the container's writable layer.

```bash
docker volume create postgres-data
```

Start PostgreSQL with the volume:

```bash
docker run -d \
  --name postgres-db \
  -e POSTGRES_USER=appuser \
  -e POSTGRES_PASSWORD=local-dev-password \
  -e POSTGRES_DB=appdb \
  --mount source=postgres-data,target=/var/lib/postgresql/data \
  postgres:17
```

Named volumes can survive container deletion. However, they are not backups, and deleting the volume itself removes its data.

## Docker Networking

Containers need a network to communicate with each other.

Create a user-defined bridge network:

```bash
docker network create app-network
```

Start PostgreSQL on that network:

```bash
docker run -d \
  --name postgres-db \
  --network app-network \
  -e POSTGRES_USER=appuser \
  -e POSTGRES_PASSWORD=local-dev-password \
  -e POSTGRES_DB=appdb \
  --mount source=postgres-data,target=/var/lib/postgresql/data \
  postgres:17
```

Start the backend on the same network:

```bash
docker run -d \
  --name backend \
  --network app-network \
  -p 4000:4000 \
  -e DATABASE_URL=postgresql://appuser:local-dev-password@postgres-db:5432/appdb \
  my-backend:1.0
```

The backend can reach PostgreSQL using `postgres-db` as its hostname because both containers share the user-defined network.

Your backend must read `DATABASE_URL`, use a PostgreSQL client library, and handle connection failures appropriately.

### Important networking concepts

*   `localhost` inside a container refers to that container itself.
    
*   Containers on the same user-defined bridge network can communicate using container names.
    
*   `-p` publishes a container port to the host.
    
*   Containers do not need host-published ports to communicate over their shared internal network.
    

For browser-based frontend applications, API requests must use a browser-accessible address, such as `http://localhost:4000`, or an appropriately configured reverse-proxy route. The browser cannot normally resolve Docker-only container names.

Useful commands:

```bash
docker network ls
docker network inspect app-network
docker network connect app-network backend
docker network disconnect app-network backend
```

## Docker Volumes and Bind Mounts

Volumes and bind mounts let containers access data outside their writable layers.

### Named volumes

```bash
docker volume create app-data
docker volume ls
docker volume inspect app-data
```

Use a named volume when application data must persist beyond the lifecycle of a container.

### Bind mounts

A bind mount connects a host directory to a container directory.

```bash
docker run -d \
  --name local-nginx \
  --mount type=bind,source="$(pwd)/html",target=/usr/share/nginx/html,readonly \
  -p 8080:80 \
  nginx:stable
```

The host directory must exist, and the application needs appropriate filesystem permissions.

**Interview point:** Named volumes are managed by Docker, while bind mounts use an explicit path on the host.

## Docker Image Optimization

A production image should contain only what the application needs to run.

### Use `.dockerignore`

```text
node_modules
.git
.env
coverage
dist
```

Adjust the file to avoid excluding anything required by the build.

### Optimize layer caching

Copy dependency manifests before the rest of the application:

```dockerfile
COPY package*.json ./
RUN npm ci
COPY . .
```

This lets Docker reuse the dependency installation layer when only source files change.

### Use multi-stage builds

Multi-stage builds separate build dependencies from runtime dependencies.

For example, a React application can be built in a Node.js stage and served from an Nginx stage. The final image does not need the Node.js build tools.

### Use suitable base images

Choose supported images that meet your application's compatibility and security requirements.

### Run as a non-root user

Where practical, configure the application to run with only the permissions it needs.

Useful commands:

```bash
docker images
docker history my-backend:1.0
docker system df
```

## Debugging Docker Containers

### Container exits unexpectedly

```bash
docker ps -a
docker logs backend
```

Check the startup command, missing dependencies, environment variables, and application errors.

### Port is already in use

```bash
docker run -d -p 4001:4000 my-backend:1.0
```

This maps host port 4001 to container port 4000.

### Container cannot connect to another service

```bash
docker network inspect app-network
docker logs backend
docker inspect backend
```

Verify network membership, the destination hostname, the container port, and whether the target application is ready.

### Application changes are not appearing

Build a new image:

```bash
docker build -t my-backend:1.1 ./backend
```

Then recreate the container using the new image. Rebuilding an image does not automatically update containers already created from an older image.

### Container resource usage

```bash
docker stats
```

This displays live CPU, memory, and network statistics for running containers.

## Pushing Images to Docker Hub

To share an image, create a repository on [Docker Hub](https://hub.docker.com/).

Authenticate:

```bash
docker login
```

Tag your image:

```bash
docker tag my-backend:1.0 username/my-backend:1.0
```

Push it:

```bash
docker push username/my-backend:1.0
```

Pull it on another machine:

```bash
docker pull username/my-backend:1.0
```

Run it:

```bash
docker run -d -p 4000:4000 username/my-backend:1.0
```

The target machine must also have the required environment variables, network access, and any external dependencies.

Publishing an image does not automatically deploy the application.

## Docker Security Best Practices

*   Never hardcode credentials in a Dockerfile.
    
*   Keep base images and application dependencies updated.
    
*   Prefer specific image versions for reproducible builds.
    
*   Run applications as non-root users when practical.
    
*   Avoid publishing database ports unnecessarily.
    
*   Exclude secrets and unrelated files from the build context.
    
*   Scan images for known vulnerabilities.
    
*   Treat Docker daemon access as a privileged permission.
    

## Docker Interview Questions

### What is Docker?

Docker is a platform for building, distributing, and running applications in containers.

### What is the difference between an image and a container?

An image is a packaged template. A container is an instance created from that image.

### What is the difference between `docker run` and `docker start`?

`docker run` creates and starts a new container. `docker start` starts an existing stopped container.

### What is the difference between `RUN` and `CMD`?

`RUN` executes during image building, while `CMD` defines the default command executed when the container starts.

### What is the difference between `CMD` and `ENTRYPOINT`?

`CMD` supplies a default command or arguments. `ENTRYPOINT` defines the primary executable.

### What is the purpose of `.dockerignore`?

It excludes unnecessary files from the build context, reducing build overhead and helping prevent accidental inclusion of local files or secrets.

### Why does a backend container use a database container's name instead of `localhost`?

On a shared user-defined network, Docker resolves container names to the corresponding network addresses. `localhost` inside the backend container refers to the backend container itself.

### What is the difference between a volume and a bind mount?

A named volume is managed by Docker. A bind mount connects a specified host path to a container path.

### Why use multi-stage builds?

They separate the build environment from the runtime environment, allowing the final image to omit unnecessary build tools and dependencies.

### What happens when you delete a container?

The container and its writable layer are removed, but the image and independently stored named-volume data can remain.

### Does `EXPOSE` publish a port?

No. `EXPOSE` documents the intended container port. Use `-p` when you need to publish that port to the host.

### How do you debug a container?

Use `docker ps -a`, `docker logs`, `docker inspect`, and `docker exec` to investigate its state, logs, configuration, and running environment.

### Does Docker replace virtual machines?

Not completely. Containers generally share the host kernel, while virtual machines run separate guest operating systems. Both technologies have useful roles.

### Do you need Kubernetes to use Docker?

No. Docker containers can run on a single machine without Kubernetes. Kubernetes is a separate container orchestration platform.

## Conclusion

To become confident with Docker, focus on the concepts that directly affect building, running, debugging, and deploying applications:

*   Dockerfiles, images, and containers.
    
*   Image building and container lifecycle commands.
    
*   Port mapping and networking.
    
*   Environment variables and persistent storage.
    
*   Frontend and backend containerization.
    
*   Multi-stage builds and image optimization.
    
*   Logging, troubleshooting, and image publishing.
    

Practice by containerizing a real full-stack application, connecting its backend to PostgreSQL, and publishing the resulting images to Docker Hub.

For multi-container application configuration and orchestration, continue with the separate [Docker Compose Complete Guide: From Beginner to Interview Ready](https://blog.adityaguptareal.com/docker-compose-complete-guide-from-beginner-to-interview-ready).

**Official references:** [Docker Documentation](https://docs.docker.com/), [Dockerfile Reference](https://docs.docker.com/reference/dockerfile/), [Build Best Practices](https://docs.docker.com/build/building/best-practices/), and [Docker Networking](https://docs.docker.com/engine/network/).
