Applications often work correctly on a developer’s computer but fail when moved to a testing or production server. The new environment may use a different operating system, runtime version, dependency or configuration.
Docker helps solve this problem by packaging an application and its required environment into a portable container image.
A team can build the image once and run it consistently on a developer laptop, CI/CD runner, virtual machine or cloud platform.
This guide explains the Docker fundamentals every DevOps beginner should understand, including containers, images, Dockerfiles, storage, networking, Docker Compose, registries, security and troubleshooting.
If you are following our learning series, begin with the complete DevOps roadmap for beginners. You can also study Linux fundamentals for DevOps, Git and GitHub for DevOps and networking fundamentals for DevOps.
What Is a Container?
A container is an isolated process that runs an application with the files, libraries and configuration it needs.
For example, a containerized Node.js application can include:
- A compatible Node.js runtime
- Application source code
- Installed production dependencies
- Default configuration
- A command that starts the application
Containers reduce differences between environments because the same image can be used during development, testing and deployment.
A container is not a complete physical or virtual computer. It normally shares the host operating system’s kernel while maintaining an isolated process environment.
Why Containers Are Useful
Without containers, a team may need to install and configure every application dependency directly on each server.
This can create problems such as:
- Different runtime versions
- Missing system libraries
- Conflicting dependencies
- Inconsistent configuration
- Difficult application removal
- Complicated server setup
- “It works on my machine” failures
Containers provide a repeatable way to package and run applications.
They are useful for:
- Web applications
- APIs
- Background workers
- Databases during development
- Automated tests
- CI/CD jobs
- Microservices
- Development environments
- Temporary command-line tasks
Containers do not automatically solve every deployment problem. Applications still require correct security, networking, storage, monitoring and configuration.
Containers Versus Virtual Machines
Containers and virtual machines both provide isolation, but they work differently.
| Feature | Container | Virtual machine |
|---|---|---|
| Operating system | Shares the host kernel | Includes a complete guest OS |
| Startup time | Usually fast | Usually slower |
| Resource usage | Generally lower | Generally higher |
| Image size | Often MBs or a few GBs | Often several GBs |
| Isolation | Process-level | Machine-level |
| Common use | Packaging applications | Providing complete machines |
A virtual machine includes its own operating system, kernel and virtual hardware. A container normally contains only the application and its required userspace files.
Containers and virtual machines are frequently used together. For example, an AWS EC2 virtual machine can run Docker and host several containers.
What Is Docker?
Docker is a platform and collection of tools for building, distributing and running containerized applications.
Important Docker components include:
- Docker Engine: Runs and manages containers.
- Docker CLI: Provides commands such as
docker run. - Docker daemon: Performs operations including building images and starting containers.
- Docker Desktop: A desktop application for running Docker on Windows, macOS and supported Linux environments.
- Docker Compose: Defines and manages multi-container applications.
- Docker Hub: A public image registry for storing and sharing images.
- Dockerfile: A text file containing instructions for building an image.
The Docker CLI sends instructions to the Docker daemon, which manages images, containers, networks and volumes.
Docker Architecture
The basic Docker workflow is:
Dockerfile → Image → Container
A more complete workflow is:
Source code
↓
Dockerfile
↓
docker build
↓
Container image
↓
Image registry
↓
docker pull
↓
Running container
The image is the packaged template. A container is a running or stopped instance created from that image.
Image Versus Container
An image is a read-only package containing the files and configuration required to create a container.
A container is an instance of an image.
One image can be used to create many containers:
Node.js image
├── API container 1
├── API container 2
└── API container 3
A useful comparison is:
- An image is like a class.
- A container is like an object created from that class.
The comparison is not technically exact, but it helps beginners understand the relationship.
What Is an Image Layer?
Docker images are built from layers.
Dockerfile instructions such as RUN, COPY and ADD can create filesystem changes represented through image layers.
Layers provide several benefits:
- Docker can reuse cached build steps.
- Images can share common layers.
- Only changed layers may need to be transferred.
- Builds can become faster when instructions are ordered carefully.
Image layers are immutable after they are created. When a container starts, Docker adds a writable container layer above the image layers.
Changes made inside that writable layer normally disappear when the container is removed unless the data is stored in a volume or bind mount.
What Is a Container Registry?
A container registry stores and distributes container images.
Common registries include:
- Docker Hub
- Amazon Elastic Container Registry
- GitHub Container Registry
- GitLab Container Registry
- Azure Container Registry
- Google Artifact Registry
- Self-hosted registries such as Harbor
A registry contains repositories, and each repository can contain different image versions identified by tags.
For example:
khwaja/my-api:1.0.0
In this name:
khwajais the namespace.my-apiis the repository.1.0.0is the tag.
If no tag is provided, Docker normally uses latest. The latest tag does not necessarily mean the newest secure or production-ready version. It is simply a tag name.
Production deployments should use controlled version tags or immutable image digests.
Installing Docker
Installation steps differ between Ubuntu, Windows, macOS and other operating systems.
Use Docker’s official installation instructions for your operating system instead of downloading packages from unknown sources.
After installation, verify the CLI:
docker --version
Display detailed system information:
docker info
On a Linux server using systemd, check the Docker service:
sudo systemctl status docker
Start Docker:
sudo systemctl start docker
Enable it during system startup:
sudo systemctl enable docker
Run Docker’s test container:
sudo docker run --rm hello-world
Depending on your Linux configuration, Docker commands may require sudo.
Adding a user to the docker group allows that user to control the Docker daemon and effectively grants root-level capabilities on the host. Treat that access as privileged.
Running Your First Container
Start an Nginx container:
docker run --name beginner-nginx -d -p 8080:80 nginx
The options mean:
--name beginner-nginxassigns a readable container name.-druns the container in detached mode.-p 8080:80maps host port 8080 to container port 80.nginxspecifies the image.
Open the following address:
http://localhost:8080
On a remote server, use its permitted IP address or domain instead of localhost.
List running containers:
docker ps
List running and stopped containers:
docker ps -a
View the container logs:
docker logs beginner-nginx
Follow new log output:
docker logs -f beginner-nginx
Stop the container:
docker stop beginner-nginx
Start it again:
docker start beginner-nginx
Remove the stopped container:
docker rm beginner-nginx
Removing the container does not necessarily remove its image.
Understanding docker run
The docker run command creates and starts a new container.
Its general form is:
docker run [OPTIONS] IMAGE [COMMAND]
Run an interactive Ubuntu container:
docker run --rm -it ubuntu bash
Important options include:
| Option | Purpose |
|---|---|
--name | Assign a container name |
-d | Run in detached mode |
-it | Attach an interactive terminal |
--rm | Remove the container after it exits |
-p | Publish a container port |
-v | Mount a volume or host path |
--env or -e | Provide an environment variable |
--network | Connect to a selected network |
--restart | Configure a restart policy |
--memory | Set a memory limit |
--cpus | Set a CPU limit |
Do not confuse docker run with docker start.
docker runcreates a new container.docker startstarts an existing stopped container.
Essential Container Commands
List containers
docker container ls
Include stopped containers:
docker container ls -a
Inspect a container
docker container inspect beginner-nginx
View resource usage
docker stats
Execute a command inside a running container
docker exec beginner-nginx nginx -v
Open a shell when the image contains one:
docker exec -it beginner-nginx sh
Some images contain Bash:
docker exec -it container-name bash
Minimal images may not include Bash or other troubleshooting utilities.
Rename a container
docker rename old-name new-name
Stop a container
docker stop container-name
Remove a container
docker rm container-name
Force-removing a running container should be used carefully:
docker rm -f container-name
Essential Image Commands
List local images:
docker image ls
Pull an image:
docker pull nginx:alpine
Inspect an image:
docker image inspect nginx:alpine
View image history:
docker image history nginx:alpine
Remove an unused image:
docker image rm nginx:alpine
Show Docker disk usage:
docker system df
Only remove images, containers or volumes after confirming that they are no longer required.
Port Publishing
A container can listen on an internal port without being publicly reachable.
This command maps host port 8080 to container port 80:
docker run -d -p 8080:80 nginx
The format is:
HOST_PORT:CONTAINER_PORT
Traffic flows as follows:
Browser → Host port 8080 → Container port 80
Publishing on all interfaces may expose the service beyond your local computer, depending on the host firewall and network.
To bind only to the IPv4 loopback interface:
docker run -d -p 127.0.0.1:8080:80 nginx
This is useful when a reverse proxy on the same host should access the container but the container port should not be directly exposed externally.
The EXPOSE instruction in a Dockerfile documents the intended container port. It does not publish that port on the host.
Environment Variables
Applications commonly receive configuration through environment variables.
Pass one variable:
docker run --rm -e APP_ENV=production my-api
Use an environment file:
docker run --env-file .env my-api
An example .env file might contain:
APP_ENV=production
PORT=3000
DATABASE_HOST=database
Do not build passwords, access keys or tokens into container images.
Environment files containing secrets should not be committed to Git. Production systems should use an appropriate secrets-management solution.
Remember that environment variables may be visible through container inspection, process information, logs or platform interfaces. They should not automatically be treated as perfectly hidden.
Container Logs
Docker captures output written by a container’s main process to standard output and standard error.
View logs:
docker logs container-name
Follow logs:
docker logs -f container-name
Display recent lines:
docker logs --tail 100 container-name
Include timestamps:
docker logs --timestamps container-name
Applications running in containers should normally write operational logs to standard output and standard error. A logging system can then collect, store and search those logs.
Avoid allowing local container logs to grow without limits. Production environments should configure log rotation or use a centralized logging platform.
Container Lifecycle
A container passes through lifecycle states such as:
Created → Running → Stopped → Removed
The container continues running while its main process runs.
If the main process exits, the container stops.
For example, this container prints a message and immediately exits:
docker run --name short-task alpine echo "Task completed"
It may no longer appear in docker ps, but it appears in:
docker ps -a
Check its exit code:
docker inspect short-task --format '{{.State.ExitCode}}'
A successful process normally returns exit code 0. Other codes indicate an error or termination condition.
Restart Policies
A restart policy controls whether Docker tries to restart a stopped container.
Example:
docker run -d \
--name production-api \
--restart unless-stopped \
my-api:1.0.0
Common policies include:
| Policy | Behaviour |
|---|---|
no | Do not restart automatically |
on-failure | Restart after an unsuccessful exit |
always | Restart automatically |
unless-stopped | Restart unless it was explicitly stopped |
A restart policy can improve recovery from simple process failures, but it does not replace monitoring, health checks or proper orchestration.
What Is a Dockerfile?
A Dockerfile is a text file containing instructions for building a container image.
A simple Dockerfile for a Node.js application could be:
FROM node:24-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV NODE_ENV=production
EXPOSE 3000
USER node
CMD ["node", "server.js"]
Build the image from the directory containing the Dockerfile:
docker build -t beginner-node-api:1.0.0 .
Run it:
docker run --rm -p 3000:3000 beginner-node-api:1.0.0
The final dot in the build command is important. It identifies the current directory as the build context.
Common Dockerfile Instructions
| Instruction | Purpose |
|---|---|
FROM | Selects the base image |
WORKDIR | Sets the working directory |
COPY | Copies files into the image |
RUN | Executes a build-time command |
ENV | Defines an environment variable |
ARG | Defines a build-time variable |
EXPOSE | Documents an intended port |
USER | Selects the runtime user |
CMD | Defines the default command |
ENTRYPOINT | Defines the primary executable |
HEALTHCHECK | Defines an image-level health test |
A Dockerfile normally begins with FROM, except for supported parser directives, comments or global build arguments.
RUN Versus CMD
Beginners often confuse RUN and CMD.
RUN executes while the image is being built:
RUN npm ci
Its result becomes part of the image.
CMD defines the default command that runs when a container starts:
CMD ["node", "server.js"]
A Dockerfile can contain several RUN instructions, but only the effective final CMD provides the default container command.
CMD Versus ENTRYPOINT
Both instructions affect the container’s startup command.
ENTRYPOINT is useful for defining the primary executable. CMD can provide default arguments or define the complete default command.
Example:
ENTRYPOINT ["node"]
CMD ["server.js"]
The resulting default command is:
node server.js
For many beginner applications, using the JSON-array form of CMD is sufficient:
CMD ["node", "server.js"]
The JSON-array form helps the process receive operating-system signals correctly and avoids invoking an unnecessary shell.
What Is the Build Context?
The build context is the collection of files made available to the Docker builder.
In this command:
docker build -t my-api .
the dot sends the current directory as the context.
Large build contexts can make builds slower and may unintentionally include sensitive or unnecessary files.
Use a .dockerignore file to exclude them.
Using .dockerignore
A Node.js project could use:
node_modules
npm-debug.log
.git
.github
.env
.env.*
coverage
dist
README.md
Dockerfile*
compose*.yaml
Review the exclusions for your application. For example, do not exclude dist if your chosen build process expects to copy an already-generated dist directory into the image.
A .dockerignore file can:
- Reduce build-context size
- Improve build performance
- Prevent unnecessary cache invalidation
- Reduce the chance of copying secrets
- Keep development files out of production images
It is an important security and performance control, but it does not replace proper secret management.
Docker Build Cache
Docker can reuse results from earlier build steps.
Consider:
COPY package*.json ./
RUN npm ci
COPY . .
Dependency files are copied before the rest of the source code. If application code changes but package-lock.json does not, Docker may reuse the dependency-installation layer.
If the entire project were copied before npm ci, almost every source-code change could invalidate that layer.
A useful principle is:
- Place stable and expensive steps earlier.
- Place frequently changing files later.
- Keep the build context small.
Test the final image even when the build cache is used.
Multi-Stage Builds
Multi-stage builds use several FROM instructions to separate build dependencies from the final runtime image.
A Node.js and TypeScript example is:
FROM node:24-alpine AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:24-alpine AS builder
WORKDIR /app
COPY --from=dependencies /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:24-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/main.js"]
The final stage contains the compiled application and production dependencies without including the complete build environment.
Multi-stage builds can:
- Reduce final image size
- Separate build and runtime tools
- Reduce the attack surface
- Make Dockerfiles easier to organize
- Support separate test, development and production stages
Always test the runtime stage because an overly aggressive reduction can remove files or libraries the application needs.
Persistent Data
A container’s writable layer should not be treated as permanent storage.
If a database writes data only inside the container layer, removing the container can remove access to that data.
Docker provides two common storage approaches:
- Volumes
- Bind mounts
Docker Volumes
Volumes are persistent data stores managed by Docker.
Create a volume:
docker volume create postgres-data
List volumes:
docker volume ls
Inspect a volume:
docker volume inspect postgres-data
Run PostgreSQL with the volume:
docker run -d \
--name beginner-postgres \
-e POSTGRES_PASSWORD=replace-with-a-strong-password \
-v postgres-data:/var/lib/postgresql/data \
postgres:18-alpine
The data is stored outside the container’s writable layer. The container can be replaced while the named volume remains.
Do not place real passwords directly in shell history in production. This inline example is only intended to demonstrate the option.
Removing a container does not automatically remove its named volume.
Before deleting a volume, confirm that it does not contain important data and that an appropriate backup exists.
Bind Mounts
A bind mount connects a specific host path to a container path.
Example:
docker run --rm \
-p 8080:80 \
--mount type=bind,source="$PWD/site",target=/usr/share/nginx/html,readonly \
nginx
Bind mounts are useful when:
- Developing source code locally
- Providing configuration files
- Sharing generated files with the host
- Mounting content that already exists on the host
Volumes are usually preferable for data managed by Docker, such as database data. Bind mounts are useful when the host and container must access the same known files.
Bind mounts can expose host files to the container. Use read-only mounts when writing is unnecessary.
Docker Networking
Docker networks allow containers to communicate.
List networks:
docker network ls
Create a bridge network:
docker network create application-network
Start a PostgreSQL container on it:
docker run -d \
--name database \
--network application-network \
-e POSTGRES_PASSWORD=replace-with-a-strong-password \
postgres:18-alpine
Start an application container on the same network:
docker run -d \
--name api \
--network application-network \
-e DATABASE_HOST=database \
my-api:1.0.0
The application can use the container name database as the hostname.
Inside the api container:
localhost
refers to the api container itself. It does not refer to the database container or automatically refer to the Docker host.
This is one of the most common container-networking mistakes.
Common Docker Network Drivers
| Driver | Purpose |
|---|---|
bridge | Common networking for containers on one Docker host |
host | Uses the host network directly where supported |
none | Disables container networking |
overlay | Connects containers across participating hosts in supported orchestration setups |
macvlan | Gives containers network identities on a physical network |
For most beginner single-host applications, a user-defined bridge network is sufficient.
User-defined bridge networks provide container-name-based discovery and better application separation than placing everything on the default bridge network.
What Is Docker Compose?
Docker Compose defines a multi-container application in a YAML file.
Instead of starting the application, database and other services through several long docker run commands, you describe them together.
A simple compose.yaml file is:
services:
app:
build:
context: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://appuser:${POSTGRES_PASSWORD}@database:5432/appdb
depends_on:
database:
condition: service_healthy
restart: unless-stopped
database:
image: postgres:18-alpine
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: appdb
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
postgres-data:
Create a local .env file:
POSTGRES_PASSWORD=replace-with-a-strong-password
Add .env to .gitignore.
Start the stack:
docker compose up -d
View running services:
docker compose ps
View logs:
docker compose logs
Follow logs:
docker compose logs -f
Stop and remove the containers and Compose network:
docker compose down
The named volume remains unless you explicitly request volume removal.
This command also removes declared volumes and their stored data:
docker compose down --volumes
Use it only when the data is disposable or safely backed up.
Modern Docker installations use:
docker compose
Older tutorials may show the legacy command:
docker-compose
Follow the current Compose documentation for your installed version.
Compose Service Discovery
Compose normally creates a default network for the application.
Services on that network can find one another using service names.
In the previous example, the application connects to:
database:5432
It should not connect to:
localhost:5432
from inside the application container.
The application does not need the database port published to the host unless a host application or administrator must connect to it directly.
Avoid publishing database ports unnecessarily.
depends_on and Application Readiness
A dependency being started does not always mean it is ready to accept requests.
For example, a database container process may have started but may still be initializing.
A health check can help Compose determine whether the database is responding:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
interval: 10s
timeout: 5s
retries: 5
Applications should also handle temporary dependency failures using controlled retries and clear error reporting.
Do not rely entirely on startup ordering for resilience.
Building and Tagging an Image
Build an image:
docker build -t my-api:1.0.0 .
Add another tag:
docker image tag my-api:1.0.0 your-dockerhub-name/my-api:1.0.0
List images:
docker image ls
Tags should help identify deployable versions.
Examples include:
my-api:1.0.0
my-api:1.0.1
my-api:git-a1b2c3d
Avoid depending only on latest in production because it does not clearly identify which application version is deployed.
Pushing an Image to a Registry
Authenticate with the registry:
docker login
Push the tagged image:
docker push your-dockerhub-name/my-api:1.0.0
Another machine can pull it:
docker pull your-dockerhub-name/my-api:1.0.0
For automated pipelines, use short-lived or restricted credentials where supported. Store credentials in the CI/CD platform’s secret store instead of committing them to the repository.
Do not expose authentication tokens in command output or build logs.
Docker in CI/CD
Docker is commonly used in continuous integration and deployment.
A typical pipeline is:
- Check out the source code.
- Install or prepare required tools.
- Run linting and automated tests.
- Build a container image.
- Scan the image.
- Tag it with a version or commit identifier.
- Authenticate with a registry.
- Push the image.
- Deploy the selected image.
- Perform health checks.
- Roll back if deployment fails.
The flow can be represented as:
Git push
↓
Automated tests
↓
Docker build
↓
Security scan
↓
Registry push
↓
Deployment
Build once and promote the same immutable image through testing and production where practical. Rebuilding separately for each environment can produce different artifacts.
Environment-specific configuration should be supplied during deployment instead of creating a completely different image for every environment.
Image Tags in CI/CD
A CI/CD pipeline can tag images using:
- Application version
- Git commit identifier
- Release tag
- Build number
- Environment alias
Example:
docker build -t registry.example.com/my-api:${GIT_COMMIT_SHA} .
The exact variable depends on the CI/CD platform.
A useful strategy is to push an immutable tag for traceability and optionally add a readable alias.
For example:
my-api:git-a1b2c3d
my-api:production
The immutable tag identifies the exact build. The alias can point to the currently selected version.
Health Checks
A health check determines whether an application is functioning, not merely whether its process exists.
A Dockerfile example is:
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -qO- http://127.0.0.1:3000/health || exit 1
This works only if the image contains wget and the application provides the /health endpoint.
Inspect health status:
docker inspect container-name --format '{{json .State.Health}}'
A health endpoint should test the application appropriately without performing an expensive operation on every check.
Container health checks help detect failure, but the surrounding platform must decide how to react.
Resource Limits
A container can consume excessive CPU or memory if no controls are applied.
Run a container with limits:
docker run -d \
--name limited-api \
--memory 512m \
--cpus 1.0 \
my-api:1.0.0
Monitor usage:
docker stats
Resource limits help prevent one container from consuming all available host resources. Select realistic values by measuring the application under representative workloads.
If a container exceeds its memory limit, the operating system may terminate it. Check the container state and host logs when investigating unexpected exits.
Running as a Non-Root User
Containers often run as root by default. If an attacker compromises the application, excessive container privileges can increase the potential impact.
Use a non-root user when the application supports it.
Example:
FROM node:24-alpine
WORKDIR /app
COPY --chown=node:node package*.json ./
RUN npm ci --omit=dev
COPY --chown=node:node . .
USER node
CMD ["node", "server.js"]
Running as a non-root user does not make a container completely secure, but it is an important defence.
Ensure mounted files and required directories have appropriate permissions.
Docker Security Best Practices
Follow these practices:
- Use trusted and maintained base images.
- Use specific image versions or digests in controlled deployments.
- Keep Docker Engine and host packages updated.
- Use multi-stage builds.
- Keep images as small as practical.
- Run the application as a non-root user.
- Remove unnecessary tools and packages.
- Never copy secret files into images.
- Scan images for known vulnerabilities.
- Sign and verify images when your supply-chain process supports it.
- Restrict registry access.
- Use read-only mounts where possible.
- Drop unnecessary Linux capabilities.
- Avoid privileged containers.
- Limit CPU and memory.
- Publish only required ports.
- Separate applications with appropriate networks.
- Review third-party images before using them.
- Protect the Docker socket.
- Centralize and monitor logs.
- Rebuild images regularly to include security updates.
Do not mount the Docker socket into an application container unless you fully understand the security consequences. Control over the Docker daemon can provide extensive control over the host.
Docker Secrets and Build Secrets
Never store a password in a Dockerfile:
ENV DATABASE_PASSWORD=my-secret-password
It can remain visible in the image configuration or history.
Do not copy .env files or cloud credential directories into an image.
If a build needs private credentials, use the build system’s supported secret-mounting functionality instead of build arguments or ordinary environment variables where possible.
A secret passed through a Dockerfile ARG or persisted by a build command may leak into image metadata, cache or layers.
After any accidental secret exposure:
- Revoke or rotate the secret.
- Remove it from the source and build configuration.
- Rebuild the image.
- Remove or restrict affected images.
- Review logs and registry access.
- Investigate whether the credential was used.
Deleting the visible line alone is not sufficient.
Containerizing Microservices
A microservice architecture may use separate containers for:
- API gateway
- Authentication service
- User service
- Payment service
- Notification worker
- Database
- Cache
- Message broker
Separating services can provide independent packaging and deployment, but it also introduces:
- Network failures
- Distributed logging
- Service discovery
- Data consistency challenges
- More complicated monitoring
- Version compatibility
- Security between services
- Deployment coordination
Do not split an application into microservices only because containers are available. The architecture should reflect real scaling, ownership or isolation requirements.
Docker and Kubernetes
Docker teaches important container concepts used in Kubernetes:
- Images
- Registries
- Container ports
- Environment variables
- Volumes
- Networks
- Health checks
- Resource limits
Kubernetes adds orchestration capabilities such as:
- Scheduling containers across nodes
- Replacing failed workloads
- Scaling replicas
- Service discovery
- Rolling deployments
- Configuration and secret resources
- Persistent storage integration
- Traffic routing
You should understand Docker fundamentals before learning Kubernetes, but Kubernetes does not require every workload to be built or operated exclusively with Docker tools.
Troubleshooting Containers
A structured troubleshooting process is more effective than repeatedly restarting containers.
1. List all containers
docker ps -a
Check the status and exit code.
2. Read the logs
docker logs --tail 100 container-name
3. Inspect the container
docker inspect container-name
Check its environment, mounts, network, ports and state.
4. Check running processes
docker top container-name
5. Check resource usage
docker stats
6. Enter the container when possible
docker exec -it container-name sh
7. Test the application inside the container
docker exec container-name wget -qO- http://127.0.0.1:3000/health
The exact command depends on tools included in the image.
8. Check published ports
docker port container-name
9. Inspect the network
docker network inspect network-name
10. Validate Compose configuration
docker compose config
11. Review host resources
df -h
free -h
Also inspect host logs and Docker service status.
Change one factor at a time and retest.
Common Docker Problems
The container exits immediately
Possible causes include:
- The main process completed.
- The startup command is incorrect.
- A required environment variable is missing.
- The application crashed.
- A file or executable is unavailable.
- File permissions are incorrect.
Check:
docker ps -a
docker logs container-name
docker inspect container-name
The container is running but the application cannot be opened
Check:
- Whether the application listens inside the container
- Whether it listens on
0.0.0.0instead of only127.0.0.1 - Whether the correct port is published
- Host firewall rules
- Cloud security-group rules
- Reverse-proxy configuration
For example, an application listening only on the container’s loopback address may not accept traffic arriving through the container network.
A container cannot connect to the database
Check:
- Both services are connected to the correct network.
- The application uses the database service name.
- The database port is correct.
- Credentials and database name are correct.
- The database is ready.
- Authentication rules permit the connection.
Do not use localhost to identify a separate database container.
A build does not include recent changes
Possible causes include:
- The wrong build context was used.
.dockerignoreexcludes the file.- A bind mount hides files from the image.
- The wrong image tag is running.
- A registry or deployment still references an older image.
Inspect the image tag and recreate the container.
Permission denied on mounted files
The user ID inside the container may not have permission to read or write the mounted host path.
Check:
- Container user
- Host ownership
- Directory permissions
- Mount mode
- Security systems such as SELinux or AppArmor
Avoid solving every permission problem by running the container as root or making files writable by everyone.
No space left on device
Check usage:
docker system df
df -h
Old images, stopped containers, build cache and unused volumes can consume disk space.
Investigate exact usage before deleting anything.
Safe Docker Cleanup
List stopped containers:
docker ps -a
List images:
docker image ls
List volumes:
docker volume ls
List networks:
docker network ls
Remove a known stopped container:
docker rm container-name
Remove a known unused image:
docker image rm image-name:tag
Remove a known unused volume:
docker volume rm volume-name
Docker also provides prune commands, but they can remove multiple unused resources:
docker container prune
docker image prune
docker network prune
docker volume prune
docker system prune
Review the prompt and understand the scope before confirming.
Be especially careful with:
docker volume prune
Volumes may contain databases or other important persistent data.
Never run broad cleanup commands automatically in production without verifying what will be deleted and whether recovery is possible.
Practical Docker Project
Create a small Node.js API and containerize it.
Complete these tasks:
- Create a Git repository.
- Build a basic Node.js API.
- Add a
/healthendpoint. - Add automated tests.
- Create a Dockerfile.
- Add
.dockerignore. - Build the image.
- Run the container.
- Publish the application port.
- Read the logs.
- Execute a command inside the container.
- Add PostgreSQL.
- Create a named volume for database data.
- Connect the services through a Docker network.
- Replace individual commands with
compose.yaml. - Add a database health check.
- Run the application as a non-root user.
- Set CPU and memory limits where supported.
- Scan the final image.
- Tag the image with a version.
- Push it to a container registry.
- Add a CI/CD workflow that tests and builds the image.
- Deploy the selected image to a test server.
- Verify the application health after deployment.
- Document backup and rollback procedures.
Record each command and explain its purpose in the project README.
This project demonstrates images, containers, Dockerfiles, ports, volumes, networks, Compose, registries and CI/CD.
Common Beginner Mistakes
Avoid these mistakes:
- Treating an image and container as the same thing
- Using
latestfor every deployment - Storing secrets in a Dockerfile
- Copying
.envinto an image - Running every container as root
- Publishing every service port
- Using
localhostfor another container - Storing database data only in a container layer
- Forgetting
.dockerignore - Building unnecessarily large images
- Installing debugging tools in production images without need
- Ignoring container exit codes
- Ignoring logs and health checks
- Running privileged containers unnecessarily
- Mounting the Docker socket into untrusted containers
- Deleting volumes without checking their data
- Using broad prune commands carelessly
- Rebuilding different artifacts for every environment
- Assuming a running process means a healthy application
- Deploying unscanned and untested images
Docker makes packaging easier, but reliable deployments still require careful configuration and operational practices.
Essential Docker Command Cheat Sheet
Containers
docker run IMAGE
docker ps
docker ps -a
docker start CONTAINER
docker stop CONTAINER
docker restart CONTAINER
docker logs CONTAINER
docker exec -it CONTAINER sh
docker inspect CONTAINER
docker rm CONTAINER
Images
docker pull IMAGE
docker image ls
docker build -t IMAGE:TAG .
docker image inspect IMAGE
docker image history IMAGE
docker image tag SOURCE TARGET
docker push IMAGE
docker image rm IMAGE
Volumes
docker volume create VOLUME
docker volume ls
docker volume inspect VOLUME
docker volume rm VOLUME
Networks
docker network create NETWORK
docker network ls
docker network inspect NETWORK
docker network connect NETWORK CONTAINER
docker network disconnect NETWORK CONTAINER
docker network rm NETWORK
Compose
docker compose up
docker compose up -d
docker compose ps
docker compose logs
docker compose logs -f
docker compose build
docker compose pull
docker compose down
docker compose config
Monitoring and disk usage
docker stats
docker top CONTAINER
docker system df
docker info
Frequently Asked Questions
Is Docker required for DevOps?
Docker is one of the most widely used tools for packaging applications and working with containers. A DevOps engineer should understand containers, images, registries, Dockerfiles, networks, volumes and Compose.
Is a Docker container a virtual machine?
No. A virtual machine contains a complete guest operating system. A container normally runs as an isolated process while sharing the host kernel.
Does a container keep its data after removal?
Data stored only in the container’s writable layer should not be considered persistent. Use a volume, bind mount or external storage system for important data.
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 COPY and a volume?
COPY places files into an image during the build. A volume provides persistent or externally managed data to a container at runtime.
What is Docker Compose used for?
Docker Compose defines and runs applications containing multiple related services, networks and volumes through a YAML configuration file.
Should databases run in containers?
Databases can run successfully in containers when storage, backups, upgrades, security and recovery are properly managed. Containers are especially convenient for local development and testing. Production decisions should reflect operational requirements and team expertise.
Should I use the latest tag?
Do not depend only on latest for controlled deployments. Use a version, commit-based tag or image digest so you can identify and reproduce the deployed artifact.
Is Docker enough for production deployment?
Docker runs containers, but production systems may also need orchestration, monitoring, centralized logging, secrets management, backups, security scanning, traffic management and automated recovery.
What should I learn after Docker?
After Docker fundamentals, continue with Bash scripting. Bash will help you automate Linux administration, Docker commands, application builds, deployments and CI/CD tasks.
Conclusion
Docker provides a consistent way to package, distribute and run applications.
The most important concepts to understand are:
- A Dockerfile contains build instructions.
- An image is the packaged application template.
- A container is an image instance.
- Volumes persist important data.
- Networks connect related containers.
- Compose defines multi-container applications.
- Registries store and distribute images.
- CI/CD pipelines automate testing, building and deployment.
Start by running existing images. Then build your own image, connect it to a database, add persistent storage and manage the complete application using Docker Compose.
Do not focus only on memorizing commands. Learn what Docker creates, where the application data is stored, how traffic reaches the container and how you can diagnose a failure.
Continue learning through our DevOps, Linux and technology guides. The next article in this series will cover Bash scripting fundamentals for DevOps beginners.
Official Docker Resources
Continue practising with the official documentation:
- Docker getting-started guide
- Docker image-building best practices
- Docker Compose documentation
- Docker volume documentation
Leave a Reply