Docker Tutorial for Developers: From Basics to Full-Stack Deployment
Hi there! I'm Aditya, a passionate Full-Stack Developer driven by a love for turning concepts into captivating digital experiences. With a blend of creativity and technical expertise, I specialize in crafting user-friendly websites and applications that leave a lasting impression. Let's connect and bring your digital vision to life!
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 buildanddocker 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:
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:
Verify your installation:
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
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
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 runcreates and starts a new container.docker startstarts an existing stopped container.docker pslists running containers.docker ps -aalso lists stopped containers.docker execruns a command inside a running container.docker logsdisplays container output.docker stopstops a container without deleting it.
Understanding port mapping
docker run -d -p 8080:80 nginx:stable
The syntax is:
-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:
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.
CMD ["node", "server.js"]
ENTRYPOINT defines the primary executable.
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.
RUN npm ci
CMD executes when the container starts.
CMD ["npm", "start"]
Building and Running Your First Docker Image
Create a Node.js project:
mkdir docker-demo
cd docker-demo
npm init -y
npm install express
Create server.js:
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:
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:
node_modules
.git
.env
npm-debug.log
coverage
Build the image:
docker build -t docker-demo:1.0 .
Run the container:
docker run -d --name docker-demo -p 3000:3000 docker-demo:1.0
Test the application:
curl http://localhost:3000
Expected response:
{"message":"Hello from Docker!"}
Inspect the logs:
docker logs docker-demo
Stop and remove the container:
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:
npm create vite@latest frontend -- --template react
cd frontend
npm install
Create frontend/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:
node_modules
dist
.git
.env
Build the image from the parent directory:
docker build -t my-frontend:1.0 ./frontend
Run the container:
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:
backend/
├── server.js
├── package.json
├── package-lock.json
└── Dockerfile
Create backend/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:
docker build -t my-backend:1.0 ./backend
Run it:
docker run -d --name backend -p 4000:4000 my-backend:1.0
The API is accessible at http://localhost:4000.
Passing environment variables
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:
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:
docker logs postgres-db
Connect to PostgreSQL:
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.
docker volume create postgres-data
Start PostgreSQL with the volume:
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:
docker network create app-network
Start PostgreSQL on that network:
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:
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
localhostinside a container refers to that container itself.Containers on the same user-defined bridge network can communicate using container names.
-ppublishes 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:
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
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.
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
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:
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:
docker images
docker history my-backend:1.0
docker system df
Debugging Docker Containers
Container exits unexpectedly
docker ps -a
docker logs backend
Check the startup command, missing dependencies, environment variables, and application errors.
Port is already in use
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
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:
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
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.
Authenticate:
docker login
Tag your image:
docker tag my-backend:1.0 username/my-backend:1.0
Push it:
docker push username/my-backend:1.0
Pull it on another machine:
docker pull username/my-backend:1.0
Run it:
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.
Official references: Docker Documentation, Dockerfile Reference, Build Best Practices, and Docker Networking.

