Docker Explained
Understand how Docker containers and images work, learn the basics of Dockerfiles, volumes and networks, and discover common Docker workflows and best practices.
Docker is a platform for building, packaging and running applications in isolated environments called containers. Instead of installing an application's dependencies directly on a machine, developers can package the application together with its runtime environment and run the resulting container consistently across different systems.
Docker has become a common part of modern development workflows because it simplifies application setup, testing, deployment and collaboration. A developer can use the same container image locally, in a CI pipeline and on a production server, reducing differences between environments.
What Is Docker?
Docker is a containerization platform that uses operating system features to isolate application processes while allowing them to share the host operating system kernel. A Docker container contains an application and the files, libraries and runtime components required to run it.
Unlike a traditional virtual machine, a container does not normally include an entire guest operating system. This makes containers lightweight and allows many of them to run on the same host with relatively little overhead.
Why Use Docker?
- Create reproducible development environments.
- Package applications with their dependencies.
- Run services in isolated containers.
- Simplify deployment between environments.
- Make CI and testing environments more consistent.
- Run multiple application versions on the same host.
- Simplify local development for multi-service applications.
The Basic Docker Workflow
A typical Docker workflow starts with application source code and a Dockerfile. Docker uses the Dockerfile to build an image. A container is then created from that image and started as a running process.
Application source code
↓
Dockerfile
↓
Docker image
↓
Docker container
↓
Running applicationDocker Images
A Docker image is a read-only package containing the filesystem, application files and configuration needed to create a container. Images are built from layers, which allows Docker to reuse unchanged parts between builds.
Images can be created locally, shared through private registries or downloaded from public registries such as Docker Hub. A single image can be used to create multiple containers.
docker pull nginx
docker imagesDocker Containers
A container is a running or stopped instance created from a Docker image. The image provides the initial filesystem and configuration, while the container represents a specific runtime instance of that image.
Multiple containers can be created from the same image. Each container has its own isolated process environment, networking configuration and writable container layer.
docker run -d --name web nginx
docker ps
docker stop web
docker start webImages vs Containers
| Feature | Image | Container |
|---|---|---|
| Purpose | Template for an application environment | Runtime instance |
| State | Read-only layers | Has a writable runtime layer |
| Created by | Docker build or image registry | Docker run or container creation |
| Can run? | No | Yes |
| Reusable? | Yes | Can be recreated from an image |
Dockerfile
A Dockerfile is a text file containing instructions used to build a Docker image. It describes the base image, application files, dependencies, commands and default process that should run inside the container.
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "start"]Common Dockerfile Instructions
| Instruction | Purpose |
|---|---|
| FROM | Selects the base image |
| WORKDIR | Sets the working directory |
| COPY | Copies files into the image |
| ADD | Adds files or other supported sources |
| RUN | Executes a command during image creation |
| ENV | Defines environment variables |
| EXPOSE | Documents a container port |
| CMD | Defines the default command |
| ENTRYPOINT | Defines the main executable |
| USER | Selects the user used to run processes |
Building an Image
The docker build command reads a Dockerfile and creates an image from its instructions. The build context contains the files Docker is allowed to access during the build.
docker build -t my-app:1.0 .The -t option assigns a repository name and optional tag to the resulting image. The final dot specifies the current directory as the build context.
Running a Container
Once an image exists, docker run can create a container from it. Options can configure the container name, ports, environment variables, volumes and other runtime settings.
docker run -d --name my-app -p 3000:3000 my-app:1.0The -d option runs the container in the background, while -p maps a host port to a port inside the container. Port mapping allows applications inside containers to be reached from outside the container.
Container Ports
Containers have their own network namespace, so a service listening on a container port is not automatically exposed on the host. Docker port publishing creates a connection between a host port and a container port.
docker run -p 8080:80 nginxIn this example, requests sent to port 8080 on the host are forwarded to port 80 inside the Nginx container.
| Value | Meaning |
|---|---|
| 8080 | Host port |
| 80 | Container port |
Container Logs
Docker captures the standard output and standard error streams of containers. The docker logs command can be used to inspect application output, which is particularly useful when debugging containers running in the background.
docker logs my-app
docker logs -f my-appExecuting Commands Inside Containers
The docker exec command runs a command inside an already running container. It is useful for debugging, inspecting files and performing administrative tasks during development.
docker exec -it my-app shThe -i option keeps standard input available and -t allocates a terminal. The exact shell available inside a container depends on the image. Minimal images may provide sh without including bash.
Docker Volumes
Container filesystems are generally tied to the container lifecycle. If important data is stored only inside a container's writable layer, removing the container can cause that data to be lost. Docker volumes provide persistent storage that exists independently of individual containers.
docker volume create app-data
docker run -d --name database -v app-data:/var/lib/data my-database| Storage Type | Typical Use |
|---|---|
| Container writable layer | Temporary runtime data |
| Named volume | Persistent application data |
| Bind mount | Sharing host files with a container |
Bind Mounts
A bind mount maps a directory or file from the host machine directly into a container. Bind mounts are particularly useful during development because source code can be edited on the host while the container accesses the updated files.
docker run --rm -v "$(pwd):/app" node:22-alpineDocker Networks
Docker networks allow containers to communicate with each other while remaining isolated from unrelated containers. User-defined bridge networks are commonly used to connect application services such as web servers, APIs and databases.
docker network create app-network
docker run -d --name database --network app-network postgres
docker run -d --name api --network app-network my-apiContainers connected to the same user-defined network can generally communicate using container names as DNS names. This makes service-to-service communication easier to configure than relying on changing container IP addresses.
Environment Variables
Environment variables are commonly used to provide configuration to containers without hard-coding environment-specific values into the image. Database URLs, application modes, service endpoints and other configuration values can be supplied when the container starts.
docker run -d -e NODE_ENV=production -e API_URL=https://api.example.com my-appEnvironment variables are convenient for configuration, but they should not automatically be treated as a secure secret-management system. Sensitive credentials may require dedicated secret-management solutions depending on the deployment environment.
The .dockerignore File
The .dockerignore file specifies files and directories that should not be included in the Docker build context. Excluding unnecessary files can make builds faster and prevent sensitive or irrelevant data from being sent to the Docker builder.
node_modules/
.git/
.next/
dist/
.env
*.logDocker Image Layers
Docker images are composed of layers. Many Dockerfile instructions create filesystem changes that become part of the resulting image. Docker can reuse unchanged layers during later builds, which makes repeated builds faster.
Base image
↓
Install dependencies
↓
Copy application files
↓
Configure runtime
↓
Final imageThe order of Dockerfile instructions therefore matters for build performance. Instructions that change frequently are often placed later, while relatively stable dependency installation steps can be placed earlier so their layers remain cacheable.
Docker Image Tags
Tags provide human-readable references for image versions. A project can publish multiple tags for different versions or environments, although teams should avoid relying on mutable tags when they require strict reproducibility.
docker build -t my-app:1.4.0 .
docker build -t my-app:latest .A version-specific tag such as 1.4.0 clearly identifies a particular release, while latest is commonly used as a moving reference to the most recently published image associated with that tag.
Docker Registries
A container registry stores and distributes Docker images. Developers can push images to a registry and deployment systems can pull those images when starting application containers.
docker login
docker tag my-app:1.0 registry.example.com/my-app:1.0
docker push registry.example.com/my-app:1.0| Operation | Purpose |
|---|---|
| docker pull | Download an image |
| docker build | Build an image |
| docker tag | Assign an image reference |
| docker push | Upload an image |
| docker images | List local images |
Docker Compose
Docker Compose is used to define and run multi-container applications. Instead of manually starting each service with a long docker run command, a Compose file can describe the application's services, networks, volumes, environment variables and dependencies.
services:
web:
build: .
ports:
- "3000:3000"
depends_on:
- database
database:
image: postgres:17
environment:
POSTGRES_PASSWORD: exampleWith Compose, an application consisting of an API, database and frontend can be started from a single configuration file. This is especially useful for local development and testing.
docker compose up -d
docker compose ps
docker compose logs
docker compose downDocker Compose Services
| Service | Possible Responsibility |
|---|---|
| web | Frontend or web server |
| api | Backend application |
| database | Persistent database |
| redis | Cache or message broker |
| worker | Background processing |
Docker and Virtual Machines
Docker containers and virtual machines both provide isolation, but they operate at different levels. A virtual machine typically includes a complete guest operating system, while containers share the host kernel and isolate application processes.
| Aspect | Containers | Virtual Machines |
|---|---|---|
| Guest OS | Usually not included | Included |
| Startup time | Usually fast | Usually slower |
| Resource overhead | Lower | Higher |
| Isolation | Process-level | Hardware virtualization |
| Typical unit | Application or service | Complete machine environment |
Docker Is Not a Virtual Machine
Although containers can provide strong isolation, they are not miniature virtual machines. Containers share the host kernel, which is one reason they can start quickly and consume fewer resources than full virtual machines.
Docker in Development
Docker can simplify development by standardizing dependencies and services. Instead of asking every developer to install a specific database version, cache server or runtime manually, a project can define those dependencies as containers.
- Run databases locally without native installation.
- Use consistent runtime versions.
- Reproduce project environments quickly.
- Test multiple service configurations.
- Reduce setup differences between developers.
Docker in CI/CD
Docker is commonly used in continuous integration and deployment pipelines. A CI system can build an image, run tests inside containers and publish the resulting image to a registry. Deployment systems can then use that exact image as the application artifact.
Source code
↓
Build Docker image
↓
Run automated tests
↓
Push image to registry
↓
Deploy image
↓
Run containersMulti-Stage Docker Builds
Multi-stage builds allow a Dockerfile to use multiple FROM instructions and copy only the required output from an earlier build stage into the final image. This is useful for applications that require compilers, package managers or development dependencies during the build but do not need them at runtime.
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --omit=dev
CMD ["node", "dist/server.js"]The final image can therefore contain only the runtime files and dependencies needed by the application instead of the entire build environment.
Running as a Non-Root User
Processes inside containers should run as a non-root user whenever possible. Running applications as a non-root user reduces the potential impact of vulnerabilities that allow an attacker to execute commands inside a container.
FROM node:22-alpine
WORKDIR /app
COPY --chown=node:node . .
USER node
CMD ["node", "server.js"]Docker Resource Limits
Containers can be configured with resource limits such as CPU and memory constraints. Limits help prevent a single service from consuming excessive host resources and affecting other workloads.
docker run -d --memory=512m --cpus=1 my-appCommon Docker Commands
| Command | Purpose |
|---|---|
| docker ps | List running containers |
| docker ps -a | List all containers |
| docker images | List local images |
| docker pull | Download an image |
| docker build | Build an image |
| docker run | Create and start a container |
| docker stop | Stop a container |
| docker start | Start a stopped container |
| docker rm | Remove a container |
| docker rmi | Remove an image |
| docker logs | View container logs |
| docker exec | Execute a command inside a container |
Common Docker Mistakes
- Using unnecessarily large base images.
- Copying the entire project into the build context.
- Ignoring node_modules, build output or .git directories.
- Storing important persistent data only inside containers.
- Running applications as root without a reason.
- Hard-coding secrets into Dockerfiles.
- Using mutable image tags when reproducibility is required.
- Creating containers without health or monitoring considerations.
- Installing development dependencies into production images.
- Keeping stopped and unused containers and images indefinitely.
Docker Best Practices
- Use small and appropriate base images.
- Create a useful .dockerignore file.
- Order Dockerfile instructions to take advantage of build caching.
- Use multi-stage builds when they reduce the final image size.
- Run applications as a non-root user when possible.
- Keep secrets outside Dockerfiles and container images.
- Use explicit image versions for reproducible deployments.
- Store persistent data in volumes or external storage.
- Keep containers focused on a single primary responsibility.
- Make container logs available through standard output and error.
- Regularly update base images and dependencies.
- Remove unnecessary packages and build tools from production images.
Docker Security Considerations
Containerization does not automatically make an application secure. Docker security depends on the container image, application, host system, runtime configuration, network exposure and credentials used by the application.
| Risk | Recommended Protection |
|---|---|
| Vulnerable base image | Regularly update and scan images |
| Exposed secrets | Use dedicated secret management |
| Root processes | Use non-root users |
| Unnecessary services | Keep images minimal |
| Excessive permissions | Use least privilege |
| Untrusted images | Use trusted and verified sources |
Docker and Persistent Data
Containers are often treated as replaceable application instances. Persistent state should therefore usually live outside the container's writable layer. Databases, uploaded files and other important data can be stored using Docker volumes or external storage systems.
Docker for Local Databases
One of Docker's practical benefits for developers is the ability to run databases without installing them directly on the host operating system. A project can define a database image, configure its environment and attach persistent storage.
docker run -d --name postgres-dev -e POSTGRES_PASSWORD=example -p 5432:5432 postgres:17This approach makes it easier to create disposable development databases and recreate environments when necessary. For important data, however, appropriate persistent storage and backup procedures are still required.
Docker Lifecycle
A useful way to understand Docker is to separate the lifecycle of an image from the lifecycle of a container. Images are built and distributed, while containers are created, started, stopped and removed as runtime instances.
Dockerfile
↓
Build
↓
Image
↓
Create
↓
Container
↓
Start
↓
Running
↓
Stop / RemoveFrequently Asked Questions
What is Docker used for?
Docker is used to package and run applications in isolated containers. It helps create reproducible environments, simplify deployment, run development dependencies and standardize application environments across machines.
What is the difference between a Docker image and a container?
A Docker image is a read-only package used as a template, while a container is a runtime instance created from that image.
Is Docker a virtual machine?
No. Containers normally share the host operating system kernel, while virtual machines include their own guest operating system.
What is a Dockerfile?
A Dockerfile is a text file containing instructions that Docker uses to build an image. It defines things such as the base image, dependencies, application files and startup command.
Why is .dockerignore important?
The .dockerignore file prevents unnecessary or sensitive files from being included in the Docker build context. This can reduce build time, image-related overhead and accidental exposure of local files.
What are Docker volumes used for?
Docker volumes provide persistent storage that exists independently of individual containers. They are commonly used for databases and other application data that must survive container replacement.
What is Docker Compose?
Docker Compose is a tool for defining and running applications made up of multiple containers. A Compose file can describe services, networks, volumes, ports and environment variables.
Can multiple containers use the same Docker image?
Yes. A Docker image can be used to create many independent containers, with each container having its own runtime state and configuration.
Should Docker containers run as root?
Applications should generally run as a non-root user when they do not require elevated privileges. This follows the principle of least privilege and can reduce the impact of certain security vulnerabilities.
Are Docker containers persistent?
Containers themselves can be stopped and restarted, but their writable filesystem should not normally be treated as durable application storage. Important data should be stored in volumes or external storage.
Helpful Docker Tools
A Dockerfile Generator helps create Dockerfiles for common application stacks, a Docker Compose Generator creates multi-service Compose configurations, a Docker Compose Formatter formats and organizes Compose files, a Docker Ignore Generator creates .dockerignore patterns for common projects, and an Environment Variable Generator helps prepare environment configuration for containerized applications.
Conclusion
Docker provides a practical way to package and run applications in isolated, reproducible environments. Understanding the relationship between Dockerfiles, images and containers is the foundation for using Docker effectively. Volumes provide persistent storage, networks allow services to communicate, environment variables provide runtime configuration and Docker Compose simplifies multi-container applications. By using small images, minimizing privileges, managing secrets correctly and treating containers as replaceable application instances, developers can build more consistent and maintainable development and deployment workflows.