Docker
Everything a developer needs to understand Docker—from local development environments to production deployments.

Don't only ship your code. Ship the entire environment needed to run it.
Docker solves the classic problem: "It works on my machine.”
Git packages your source code.
Docker packages your runtime environment.
#The use cases
#1. Consistent Development Environment
- Every developer runs the application in the exact same environment.
- Eliminates dependency and version mismatch issues.
- New developers can get started quickly.
#2. Easy Deployment
-
Deploy the same Docker image that was tested locally.
-
No need to manually install runtime dependencies on the server.
-
Reduces deployment errors.
markdown# Without Docker Install Node Install npm Install Redis Install nginx Install OpenSSL Install Prisma Generate Prisma client Set PATH Hope everything works... # With Docker docker run backend-image
#3. Microservices
- Each service runs in its own isolated environment.
- Example:
- Node.js Backend
- Python AI Service
- PostgreSQL
- Redis
- Nginx
- Different services can use different languages and runtime versions without conflicts.
#4. Multiple Project Support
- Different projects can use different runtime versions.
- Example:
- Project A → Node 18
- Project B → Node 24
- No need to constantly switch versions on your machine.
#5. CI/CD Pipelines
- CI servers run the same environment as local development.
- Ensures builds and tests are consistent across all stages.
#6. Scalability
-
Multiple identical containers can be started easily.
-
Load balancers can distribute traffic among containers.
-
Makes horizontal scaling simple.
markdown# Instead of running 1 backend # Run Backend Container 1 Backend Container 2 Backend Container 3 Backend Container 4 * A load balancer distributes requests among them. * Containers are lightweight, so creating more instances is fast.
#Docker Container
-
Instead of installing everything directly on your machine, Docker creates isolated environments. Each isolated environment is called a Container.
-
It is a lightweight, isolated environment that runs an application along with everything it needs.
-
A Container is a running instance of docker image.
markdownYour Computer ├── Container 1 │ ├── Node 24 │ ├── Backend Code │ └── Dependencies │ ├── Container 2 │ ├── Python 3.12 │ ├── AI Model │ └── Python Packages │ ├── Container 3 │ └── PostgreSQL │ └── Container 4 └── Redis
#Are containers virtual machines?
- No, A Virtual Machine (VM) virtualizes an entire operating system.
- A Container shares the host operating system's kernel but keeps the application isolated.
- This is why containers:
- Start in seconds (or less).
- Use much less memory.
- Are lightweight compared to VMs.
#Docker Image
-
A read-only blueprint (snapshot) containing everything required to run an application.
-
A Container is a running instance of that blueprint.
bash# If you create one image: Backend Image # You can start multiple containers from it: Backend Image │ ├── Container 1 ├── Container 2 └── Container 3 # This is exactly how applications are scaled in production # Suppose one backend instance can handle 100 req/second. # Traffic suddenly increased to 300 req/second # Instead of making three copied of your code, you simply run: Backend Image │ ├── Backend Container 1 ├── Backend Container 2 └── Backend Container 3 # A load balancer distributes incoming requests among the three containers. -
When you update your code, Your existing image doesn't magically change because Docker images are immutable (read-only).
-
The flow is:
markdownUpdate Code │ ▼ Build a New Image (v2) │ ▼ Start New Containers from Image v2 │ ▼ Stop & Remove Old Containers (v1)
💡 - Containers are disposable. Images are versioned.
- An image is a snapshot of your application and its environment at a particular point in time. Once it's built, it never changes. If the application changes, you build a new image, leaving the old one untouched.
#Can we revert back to an old image like we do in Git?
-
Yes! In fact, this is one of Docker's biggest advantages.
-
Suppose you’ve built three versions of your backend, each image is stored separately.
markdownImages ├── backend:v1 ├── backend:v2 └── backend:v3 -
Suppose you deploy v3, but users discover a critical bug. Instead of fixing it immediately, you can simply stop the v3 containers and start new containers from v2.
💡 - Everything starts with the
Dockerfile, which is simply a set of instructions to build an image.
#Images are made of Layers
-
Suppose your Dockerfile looks like this:
dockerFROM node:24 # Layer 1 WORKDIR /app # Layer 2 COPY package*.json # Layer 3 RUN npm install # Layer 4 COPY . . # Layer 5 CMD ["npm", "start"] # Layer 6 -
Docker doesn’t create one giant file. Instead, it creates layers. Each instruction creates a new layer.
-
Suppose you change only one file,
app.ts.Because dockercachesthe layers, if only source code changed, only the affected layer is rebuilt. This makes builds much faster.
💡 If a layer changes, Docker rebuilds that layer and every layer after it. It cannot reuse later layers because they depend on the output of the changed layer.
#Why do we need backward-compatible changes?
Database schema is persistent, while application containers are replaceable.
During deployments or rollbacks:
- Different application versions may temporarily interact with the same database.
- If the schema is incompatible with an older version, rolling back the application can cause runtime failures.
- Backward-compatible changes ensure both the old and new application versions can safely use the database during deployment.
#Backward-Compatible Changes (Safe)
These changes generally do not break older application versions:
- Add a new nullable column.
- Add a new table.
- Add an index.
- Add a new optional relationship.
Example:
ALTER TABLE users ADD COLUMN phone TEXT;The old application continues to work because it simply ignores the new column.
#Breaking Changes (Unsafe)
These can break older application versions:
- Remove a column.
- Rename a column.
- Drop a table.
- Change a column's data type incompatibly.
#Best Practices
Instead of making destructive changes immediately:
- Add the new schema while keeping the old one.
- Deploy the new application that supports both schemas.
- Migrate existing data if needed.
- Switch the application to use the new schema.
- Remove the old schema in a later release.
Rule of Thumb: Never make a database change that prevents the previous stable application version from running.
#Docker Hub
- Docker Hub is a cloud-based registry for Docker images.
💡 GitHub is to source code.
Docker Hub is to Docker images.
-
You build an image for your backend, push it to docker hub.
markdownYour Laptop │ ▼ Docker Hub │ ▼ Developer Laptop EC2 Server CI/CD Pipeline -
It contains a lot of public image, like node, python, Postgres, Redis, Nginx, etc.
#Port Mapping
-
Port mapping in docker help to map the docker container port with your local system port.
bashdocker run -p 27018:27017 mongo # Mapping you system port 27018 to the container port 27017. # It will allow you to access the mongo instance with localhost:27018.
#Common Docker Command
docker run- The
docker runcommand creates and starts a brand new container from a specified Docker Image
- The
docker start- The
docker startcommand restarts an existing, stopped container while keeping its past filesystem changes intact.
- The
docker images- The
docker imagescommand lists all top-level Docker images currently stored on your local host system. - It helps you view what software blueprints are downloaded and ready to be turned into running containers.
- The
docker ps- The
docker pscommand lists the running Docker containers on your system.
- The
docker build- The
docker buildcommand creates a reusable Docker image from a text file called aDockerfile - It automates the packaging of your application code, dependencies, and environment configuration into a single, deployable blueprint.
- The
docker kill- The
docker killcommand abruptly terminates one or more running containers. - It acts like a "force quit" or pulling the power plug, instantly stopping the application container when it refuses to shut down normally.
- The
docker stop- The
docker stopcommand gracefully shuts down one or more running containers. - Unlike a forced shutdown, it politely asks the application inside the container to save its work and finish active tasks before stopping.
- The
#Dockerfile
-
If you want to create an image from your code, that you can push to
dockerhub, you need to create aDockerfilefor you application. -
A Dockerfile is a text document that contains all the commands a user could run on the command line to create an image.
-
Every time Docker builds an image, it simply follows the instructions written in the Dockerfile from top to bottom.
-
The docker file has two parts
- Base Image
- Bunch of commands that you run on the base image
-
Example of a node project dockerfile:
dockerFROM node:22-alpine WORKDIR /app COPY package*.json ./ RUN npm install COPY . . EXPOSE 3000 CMD ["npm", "start"] -
FROMcommand- Specifies the base image from which your image starts.
- Without
FROM, Docker doesn't know which operating system or runtime to use. - Every Docker image starts from another image. Saves time because common environments are already prepared.
-
WORKDIRcommand- Sets the current working directory for all following commands.
- Think of it as the directory where you run all the command and copy the files.
-
COPYcommand-
Copies files from your computer into the Docker image.
-
Syntax:
dockerCOPY source destination # Example COPY package.json . # Copies host machine 'package.json' to container '/app/package.json' COPY . . # Copy everything except files ignored by .dockerignore
-
-
RUNcommand- Executes a command while building the image.
RUNexecutes only once during image creation.- It is not executed when the container starts.
-
CMDcommand- Specifies the default command that runs when the container starts.
-
EXPOSEcommand- Documents which port the application listens on.
- It does not publish the port to your host machine.
- You still need,
docker run -p 3000:3000 image-name - Think of it as documentation for anyone using the image.
-
ENVcommand-
Defines environment variables inside the container.
-
Use
ENVfor non-sensitive default configuration.dockerENV NODE_ENV=production ENV PORT=3000 ENV TZ=UTC ENV LANG=en_US.UTF-8 # Access in Node.js process.env.PORT # Suppose the developer want to run the container on port 5000 docker run -e PORT=5000 my-app
-
-
ARGcommand-
Defines variables available only during image build.
dockerARG NODE_VERSION=22 FROM node:${NODE_VERSION} # Build docker build --build-arg NODE_VERSION=20 . # It facilitate the user to choose the version at build time.
-
-
LABELcommand-
Adds metadata to the image.
-
Useful for documentation and tooling.
dockerLABEL maintainer="Prem Deep" LABEL version="1.0" LABEL app="ROXC Backend"
-
#Building images from dockerfile and spinning a container.
-
You have a
dockerfilein your project, to build adocker imagefrom it, run the below command in terminal:bashdocker build -t image_name . # Now, if you run 'docker images', you will notice a new image is created. -
Now, you have the
docker imageof your project, you can start the project by running below command:bashdocker run -p 3000:3000 image_name # Now, visit localhost:3000, you will see the app running. -
If you need to pass an environment variable, the
-eargument let’s you send in environment variables to your node.js appbashdocker run -p 3000:3000 -e DATABASE_URL="postgres://avnadmin:AVNS_EeDiMIdW" image_name
#The significance of ENV over .env file
-
.envandENVsolve different problems, even though they both deal with environment variables. -
Imagine you're shipping this application to another developer or to production. Should they be required to create a
.envfile just to tell your app that it runs on port3000? -
Probably not—that's a sensible default. That's where
ENVcomes in. -
.envshould contain mainly deployment-specific values -
At a high level:
- Dockerfile (
ENV): stable, non-sensitive defaults such asNODE_ENV=productionor a defaultPORT. - Development (
.env): local secrets and machine-specific configuration. - Production: secrets injected by your deployment platform (Docker Compose, Kubernetes, GitHub Actions, AWS, etc.), usually without a
.envfile on the server.
- Dockerfile (
-
Secrets should be injected when the container starts.
bashdocker run \ -e DATABASE_URL=... \ -e JWT_SECRET=... \ my-app -
In production, these are managed using orchestration tools like Docker Compose, Kubernetes Secrets, or cloud secret management services.
#The docker exec command
-
docker execexecutes a command inside an already running container. -
Think of it like SSH-ing into a remote machine.
-
Syntax:
bashdocker exec [OPTIONS] <container-name> <command> # Example: docker exec backend ls # This basically runs ls inside the backend container. docker exec -it backend sh or docker exec -it backend bash # Now, you are inside the container. # From here you can run linux command and explore the complete container. -
Common uses:
- To explore files of the container
- To check whether variables like
DATABASE_URLorNODE_ENVare actually present. - SSH-ing into the container.
- Run a node command like,
docker exec backend node -v - Connect to a database client,
docker exec -it postgres psql -U postgres
#Volume in Docker
-
If you restart a stopped
mongodocker container, you will notice that your data goes away. -
It happens, because docker containers are
transitory(they don’t retain data across restarts) -
To solve this, the volume in container is introduces, which store persistent data outside the container in a special location managed by Docker, so the data survives even if the container is stopped, removed, or recreated.
-
The volume is then mounted into the container at the time of running the container.
-
Suppose you removed the mongo container, but when you create a new mongo container and mount the same volume, the data is immediately available again.
-
A Docker Volume is simply a directory on the host machine that Docker manages for you.
markdownHost Machine +----------------------------+ | Docker Volume | | ├── img1.png | | └── img2.png | +-------------▲--------------+ │ Mounted │ +-------------┴--------------+ | Container | | /uploads | +----------------------------+ -
Volume can be created using the below command:
bashdocker volume create my_volume # Now, when can verify it exists using: docker volume ls # Mount the volume into a container # Suppose your Node application stores uploaded file in /uploads # Start the container with mounting: docker run -d --name my-app -v my-volume:/uploads my-image -
Let’s understand, the
-v my-volume:/uploadspart. -
This has the format:
<Volume Name>:<Path Inside Container>my-volume- The Docker-managed storage./uploads- The directory inside the container.
-
Whenever your app writes to
/uploads, the data actually ends up inmy-volume. -
You need to mount the
specific path inside container, where your application saves the data. Like formongo, it saves to/data/db
#Network in Docker
-
In Docker, a network is a powerful feature that allows containers to communicate with each other and with the outside world.
-
Docker containers can’t talk to each other by default.
-
localhoston a docker container meansit's own networkand not the network of thehost machine -
Command to create a network and see how Node and mongo containers communicate:
bashdocker network create my_custom_netowrk # Start a mongo container and connect it with the my_custom_network docker run -d -v volume_database:/data/db --name mongoapp --network my_custom_network -p 27017:27017 mongo # Start the node application container and connect with the same custom network. docker run -d -p 3000:3000 --name backend --network my_custom_network image_tag # Now, when you put the database url in node application, just replace the 'localhost' from 'mongoapp' # mongoapp is the dns for the network of mongo container created. -
Suppose you have three computers connected to the same Wi-Fi. Since they're on the same network, they can communicate using IP addresses.
A Docker network is exactly the same idea, except the "computers" are containers.
bashDocker Network -------------------- | | | Node.js | | | | | PostgreSQL | | | | | Redis | -------------------- -
The Docker network acts like a virtual switch/router.
-
Whenever a container joins the network:
- It gets its own IP address.
- Docker's DNS records its name.
- Other containers can reach it by name.
#Docker Network Types
#What is a Network Driver?
- A network driver defines how Docker creates and manages networking for containers. It determines how containers communicate with each other and with the host machine.
- The two most commonly used network drivers are:
- Bridge (Default)
- Host
#Bridge Network (Default)
- The Bridge network is Docker's default network driver. Every container connected to a bridge network gets its own:
- IP address
- Network namespace
localhost
- Containers communicate with each other through a virtual bridge created by Docker.
- Characteristics
- Each container has its own network stack.
- Supports Docker's built-in DNS (containers communicate using service/container names).
- Containers on the same bridge network can communicate directly.
- Host-to-container communication requires port mapping (
p).
#Host Network
-
The Host network driver removes Docker's network isolation. The container shares the host machine's network stack.
-
Characteristics
- No separate container IP.
localhostinside the container is the host'slocalhost.- No Docker bridge is created.
- Port mapping (
p) is not required. - Less network isolation.
-
How to run a container with host network
bashdocker run --network host my-app # If the application listens on: app.listen(3000); # It is directly accessible on:localhost:3000, without using -p 3000:3000 -
When to Use
- High-performance networking
- Monitoring tools
- Network utilities
- Applications requiring direct access to the host network
#Docker Compose
- Docker Compose is a tool for defining and managing multi-container Docker applications using a single YAML configuration file (
compose.ymlordocker-compose.yml). Instead of manually creating networks, volumes, and running multipledocker runcommands, you describe the entire application in one file and start everything with a single command.
#Why Docker Compose?
-
Without Docker Compose, every container must be started manually.
bashdocker network create app-network docker run ... docker run ... docker run ... -
With Docker Compose:
bashdocker compose up -
Docker automatically:
- Builds images (if required).
- Creates containers.
- Creates a dedicated bridge network.
- Connects all services to the network.
- Creates named volumes.
- Injects environment variables.
- Starts every service.
-
This makes the application reproducible, portable, and easy for every developer to run.
#Basic Structure of docker-compose
services:
frontend:
...
backend:
...
postgres:
...servicescontains every container that makes up the application.- Each service definition describes how a container should be created and run.
- Running
docker compose upcreates one container for each service.
#Common Configuration Options
-
imageUses an existing image from a registry (usually Docker Hub).
yamlservices: postgres: image: postgres:17It is equivalent to:
bashdocker run postgres:17Use
imagefor third-party services like PostgreSQL, Redis, Nginx, MongoDB, etc. -
buildBuilds an image from a Dockerfile before creating the container.
yamlservices: backend: build: .It is equivalent to:
bashdocker build . docker run <built-image>Use
buildfor your own applications where you have the source code and Dockerfile.Remember: Containers are always created from images.
buildfirst creates an image, then starts a container from it. -
portsMaps a port on the host machine to a port inside the container.
yamlports: - "5000:5000"Ports are only required when something outside Docker needs to access the container.
Examples:
- Browser → Frontend ✅
- Browser → Backend ✅
- Backend → PostgreSQL ❌ (uses Docker network)
-
environmentPasses environment variables into the container at runtime.
yamlenvironment: POSTGRES_USER: prem POSTGRES_PASSWORD: passwordUsing a
.envfile:yamlenvironment: DATABASE_URL: ${DATABASE_URL} JWT_SECRET: ${JWT_SECRET} -
volumesMounts persistent storage or local directories into a container.
yamlservices: postgres: image: postgres:17 volumes: - postgres-data:/var/lib/postgresql/data volumes: postgres-data:Used for databases so data survives container recreation.
-
depends_onControls container startup order.
yamlservices: backend: depends_on: - postgresThis ensures Docker starts the PostgreSQL container before the backend container.
Important:
depends_ononly controls startup order. It does not wait until PostgreSQL is ready to accept connections. Applications should implement retry logic or use health checks for readiness.
#Docker Compose Networking
-
Docker Compose automatically creates a dedicated bridge network for the project.
plainbackend │ postgres │ redis -
Every service joins the same network automatically.
-
Each service name becomes a DNS hostname.
-
Example:
DATABASE_URL=postgres://user:password@postgres:5432/db
# Instead of
DATABASE_URL=postgres://user:password@localhost:5432/db#Common Docker Compose Commands
# Start all services
docker compose up
#Build images before starting
docker compose up --build
# Start in a detached mode
docker compose up -d
#Stop and remove containers and netowrks
docker compose down
# Stop and remove everything including named volumes
docker compose down -v
# View running services
docker compose ps
# View logs
docker compose logs
# View logs of a specific service
docker compose logs backend
# Execute commands inside a running container
docker compose exec backend bash/s#Docker Bind Mounts
-
A bind mount creates a live connection between a directory on the host machine and a directory inside a Docker container. Instead of using the copy of the files stored inside the Docker image, the container directly accesses the files from your local machine.
-
Example:
yamlservices: backend: volumes: - ./backend:/appThis means everything inside
./backendon the host is immediately available inside/appin the container. -
Bind Mounts enable hot load and make the development smooth.
-
A common Docker Compose configuration is:
yamlservices: backend: volumes: - ./backend:/app - /app/node_modules
#Why is /app/node_modules added?
-
When the project directory is bind mounted:
yaml- ./backend:/app -
the host's
node_modulesdirectory is also mounted into the container. -
This can cause compatibility issues because:
- The host may be running Windows or macOS.
- The container is usually running Linux.
- Some Node.js packages contain platform-specific compiled binaries.
-
To avoid this, Docker creates an anonymous volume for
/app/node_modules. -
Result:
plainHost Project │ ├── src/ ├── package.json └── node_modules ❌ Not used Container │ ├── src/ ├── package.json └── node_modules ✅ Linux dependencies -
Bind mounts are not used in production, it is for development purpose.
#A production-oriented development compose.yml file
# ===================================================================
# compose.yml
# Purpose:
# Local development environment for:
# - React (Vite)
# - Node.js + Express
# - PostgreSQL
# ===================================================================
# Project Structure
#
# project-root/
# │
# ├── compose.yml
# ├── .env
# ├── frontend/
# │ ├── Dockerfile
# │ ├── package.json
# │ └── ...
# ├── backend/
# │ ├── Dockerfile
# │ ├── package.json
# │ └── ...
# └── README.md
# ===================================================================
services:
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: roxc-frontend
ports:
- "5173:5173"
volumes:
# Bind mount source code for hot reload
- ./frontend:/app
# Prevent local node_modules from overwriting container node_modules
- /app/node_modules
environment:
VITE_API_URL: http://localhost:5000
depends_on:
- backend
restart: unless-stopped
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: roxc-backend
ports:
- "5000:5000"
volumes:
- ./backend:/app
- /app/node_modules
environment:
PORT: 5000
DATABASE_URL: postgresql://postgres:password@postgres:5432/roxc
JWT_SECRET: ${JWT_SECRET}
depends_on:
- postgres
restart: unless-stopped
postgres:
image: postgres:17
container_name: roxc-postgres
ports:
# Optional.
# Needed only if you want to connect from pgAdmin,
# TablePlus, DBeaver, etc.
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: roxc
volumes:
- postgres-data:/var/lib/postgresql/data
restart: unless-stopped
volumes:
postgres-data: