What is Docker Used For? A Docker Container Tutorial for Beginners

Docker has taken the software development world by storm since its release in 2013. According to a 2020 survey by Stack Overflow, Docker is the #1 most loved and #2 most wanted platform by developers[^1]. Another survey by Datadog found that Docker adoption increased to 25% in 2018[^2]. So what exactly is Docker, and why has it become so popular?

In this comprehensive tutorial, we‘ll dive deep into the world of Docker from the perspective of a full-stack developer. We‘ll cover what Docker is, how it works under the hood, and most importantly – what problems it solves for developers and organizations. By the end, you‘ll have a solid understanding of Docker and hands-on experience creating and running your own containers. Let‘s get started!

What are Containers?

At its core, Docker is a platform for running applications in containers. But what exactly is a container?

A container is a standard unit of software that packages up code and all its dependencies so the application runs quickly and reliably from one computing environment to another[^3]. Containers provide process isolation, allowing multiple applications to run independently on a single host machine.

If you‘re familiar with virtual machines (VMs), containers may sound similar. However, while VMs virtualize an entire machine down to the hardware layers, containers only virtualize the host operating system. This makes containers far more lightweight – they share the host machine‘s kernel and do not require a full OS instance per container.

Docker Architecture

This architecture provides several key benefits over traditional VMs:

  • Lightweight: Containers share the machine‘s OS kernel, eliminating the need for a full OS instance per application. A single server can host many more containers than VMs.

  • Fast: With no OS to boot, containers can start in milliseconds, compared to minutes for a VM. This enables rapid scaling in response to demand.

  • Portable: Containers encapsulate all dependencies into a single package that can reliably run on any platform with Docker installed, from a developer‘s laptop to a production server.

  • Efficient: The small size and fast startup of containers allow far more efficient use of computing resources, as capacity can be precisely matched to demand.

How Docker Works

Docker achieves these benefits through its unique architecture. At a high level, the Docker platform consists of several key components:

  • Docker images: Read-only templates that define a container and its contents. An image includes everything needed to run an application – code, runtime, libraries, environment variables, and configuration files.

  • Docker containers: Runnable instances of an image. Containers are created from images and can be started, stopped, and deleted using Docker commands.

  • Docker daemon: The background service that manages building, running, and distributing Docker containers. The daemon listens for Docker API requests and manages Docker objects.

  • Docker client: The command-line tool that allows users to interact with the Docker daemon. The client sends commands to the daemon via the Docker API.

  • Docker registries: Stores for distributing and sharing Docker images. Docker Hub is a public registry, but enterprises can also set up private registries.

One key concept to understand is how Docker uses a layered filesystem to build and store images. Each instruction in a Dockerfile (the file used to define an image) creates a new layer:

FROM node:14
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
CMD ["npm", "start"]

When this Dockerfile is built into an image, Docker creates a layer for each instruction:

  1. A base layer from the node:14 image
  2. A new layer with the /app working directory
  3. A layer with the package.json file
  4. A layer with the installed npm packages
  5. A layer with the rest of the application code
  6. A final layer with the CMD instruction to run on startup

These layers are read-only. When a container is created from this image, Docker adds a new writable layer on top for any changes the container makes during its lifetime. This layered approach enables some powerful features:

  • Layer caching: When rebuilding an image, Docker can skip any layers that haven‘t changed, greatly speeding up build times. Only the modified layers need to be rebuilt.

  • Layer sharing: Multiple images can share access to the same underlying layers, conserving disk space and memory.

  • Rollbacks: By keeping the old layers when an image is updated, Docker can easily revert a container to the previous version of the image if needed.

Docker Layers

When a container is started from an image, Docker creates a new writable layer on top of the image layers. Any changes the container makes to the filesystem are stored in this layer. When the container is deleted, this layer is also removed, leaving the underlying image unchanged.

Docker in Action: A Hands-On Example

To solidify these concepts, let‘s walk through creating a simple Docker image and running a container. For this example, you‘ll need to have Docker installed on your machine. You can find installation instructions for Windows, Mac, and Linux on the Docker website.

First, create a new directory and navigate into it:

mkdir my-docker-app
cd my-docker-app

Next, create a Dockerfile (with no extension) in this directory with the following contents:

# Use the official Node.js 14 image as a parent image
FROM node:14

# Set the working directory to /app
WORKDIR /app

# Copy package.json to the working directory
COPY package.json .

# Install dependencies
RUN npm install

# Copy the rest of the application code
COPY . .

# Make port 3000 available to the world outside this container
EXPOSE 3000

# Run app.js using node when the container launches
CMD ["node", "app.js"]

This Dockerfile defines a simple Node.js application. It starts from the official Node.js 14 base image, copies the application code into the image, installs dependencies, and specifies the command to run when the container starts.

Next, create a minimal package.json file in the same directory:

{
  "name": "my-docker-app",
  "version": "1.0.0",
  "description": "A simple Node.js Docker app",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "^4.17.1"
  }
}

This package.json defines a simple Express.js application with a single dependency.

Finally, create an app.js file with the following code:

const express = require(‘express‘);
const app = express();

app.get(‘/‘, (req, res) => {
  res.send(‘Hello from Docker!‘);
});

const port = 3000;
app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

This is a minimal Express.js app that responds with "Hello from Docker!" when accessed on the root route.

Now we‘re ready to build the Docker image. From the my-docker-app directory, run:

docker build -t my-nodejs-app .

This command builds a Docker image tagged my-nodejs-app from the current directory (.). The -t flag allows us to specify a friendly name for the image.

Once the build completes, you can see the new image by running:

docker images

To run a container from this image, use the docker run command:

docker run -p 3000:3000 my-nodejs-app

This command starts a new container from the my-nodejs-app image. The -p flag maps port 3000 in the container to port 3000 on the host machine, allowing you to access the Express.js app from your browser at http://localhost:3000.

Congratulations! You‘ve just built and run your first Docker application. This simple example demonstrates the core workflow of building images and running containers.

Real-World Use Cases for Docker

The simple Express.js app is a good starting point, but Docker‘s true power shines in more complex, real-world scenarios. Here are a few common use cases where Docker excels:

  1. Microservices: Docker is a natural fit for microservices architectures, where an application is split into smaller, independently deployable services. Each service can be built into a separate Docker image and run in its own container, making it easy to develop, deploy, and scale services independently. Docker‘s lightweight nature allows high density of services per host, and its fast startup time enables rapid scaling.

  2. CI/CD Pipelines: Docker can greatly simplify continuous integration and deployment pipelines. By building the application into a Docker image in the CI stage, you ensure a consistent artifact is carried through to deployment. The same image can be used in testing, staging, and production environments, eliminating discrepancies between stages. Docker also enables advanced deployment strategies like blue-green deployments and canary releases.

  3. Local Development Environments: Docker can be used to create local development environments that closely mirror production. By defining the application‘s dependencies and environment in a Dockerfile, developers can quickly spin up a consistent development setup, regardless of their local OS and installed packages. This "develop with Docker" approach eliminates the classic "works on my machine" problem.

  4. Multi-Language Projects: For applications that use multiple languages, Docker provides a clean separation of concerns. Each language‘s build and runtime dependencies can be encapsulated in its own Docker image, allowing the application to be broken down by language while still being easy to assemble and deploy as a whole.

  5. Legacy Application Migration: Docker can be a valuable tool when migrating legacy applications to newer infrastructure. By containerizing the application, you abstract it from the underlying infrastructure, making it easier to move between on-premise servers, cloud providers, or even to a serverless platform like AWS Fargate or Azure Container Instances.

  6. Horizontal Scaling: Docker enables applications to scale horizontally by adding more container instances as needed. When coupled with an orchestration platform like Kubernetes, Docker applications can automatically scale based on CPU usage, request volume, or custom metrics. This allows applications to smoothly handle spikes in traffic or demand.

Docker Best Practices and Expert Tips

As you start working with Docker more extensively, keep these best practices and expert tips in mind:

  1. Keep Images Small: Wherever possible, start with a minimal base image and only add what your application needs. This keeps your images lightweight and reduces the surface area for potential vulnerabilities. Multi-stage builds can help keep final images small by only including runtime dependencies.

  2. Use Official Images: When choosing a base image, start with official images from reputable sources like the Docker Hub official repositories. These images are regularly updated and maintained with the latest security patches.

  3. Version Your Images: Use tags to version your Docker images. This allows you to roll back to a previous version if needed and maintains a clear history of your application‘s evolution.

  4. Avoid Latest Tag: While it‘s tempting to always pull the latest tag, this can lead to unpredictable behavior as the image changes over time. Instead, specify a specific version tag to ensure a consistent experience.

  5. Use Docker Compose for Multi-Container Apps: Docker Compose is a powerful tool for defining and running multi-container applications. With a single docker-compose.yml file, you can configure your application‘s services, networks, and volumes and manage them as a single unit.

  6. Leverage Docker Networking: Docker provides several networking modes to fit different use cases. By default, containers run in a bridge network, but you can also use host networking for maximum performance, overlay networks for multi-host communication, or custom plugins for advanced use cases.

  7. Mount Volumes for Persistent Data: Any data written inside a container is lost when the container is removed. For data that needs to persist across container restarts, use Docker volumes or bind mounts to store data on the host filesystem.

  8. Use Docker Secrets for Sensitive Data: Docker includes a secrets management system that allows you to store sensitive data like API keys, passwords, and certificates securely. Secrets are encrypted at rest and only made available inside the containers that need them.

  9. Implement Health Checks: Docker‘s built-in health check system allows you to specify a command that‘s run periodically to determine if your application is healthy. This can be used by orchestration platforms to automatically restart unhealthy containers.

  10. Monitor and Log Everything: As with any distributed system, comprehensive monitoring and logging is critical with Docker. Use tools like Prometheus, Grafana, ELK stack, or commercial solutions to gain visibility into your containers‘ performance and behavior.

Frequently Asked Questions

  1. Is Docker a virtual machine?
    No, Docker is not a virtual machine. While Docker and VMs both provide isolation for applications, they work in fundamentally different ways. VMs virtualize the entire machine down to the hardware level, while Docker containers share the host machine‘s kernel and only virtualize the operating system.

  2. Do I need to know Linux to use Docker?
    While a basic understanding of Linux and the command line is helpful, it‘s not strictly necessary to start using Docker. Docker provides a consistent command-line interface across platforms, so the same Docker commands work on Windows, Mac, and Linux.

  3. Can I run GUI applications in Docker?
    Yes, it‘s possible to run GUI applications in Docker, but it requires some additional setup. On Linux, you can mount the host‘s X11 socket into the container. On Windows and Mac, you need to use a third-party X server and set the DISPLAY environment variable in the container.

  4. How do I persist data in Docker?
    To persist data beyond the lifetime of a container, you can use Docker volumes or bind mounts. Volumes are managed by Docker and are the preferred mechanism for persisting data. Bind mounts allow you to mount a directory from the host machine into the container.

  5. Can I run Docker in production?
    Absolutely! Docker is widely used in production environments. However, for production use, you‘ll typically want to use an orchestration platform like Kubernetes, Docker Swarm, or Amazon ECS to manage your containers at scale.

Conclusion

In this comprehensive tutorial, we‘ve covered what Docker is, how it works, and why it‘s become so popular for application development and deployment. We‘ve seen how Docker packages applications and their dependencies into portable, lightweight containers that can run consistently across environments.

Through a hands-on example, we‘ve experienced the core Docker workflow of building images and running containers. We‘ve also explored real-world use cases where Docker shines, from microservices to CI/CD pipelines to local development environments.

As a full-stack developer, Docker is a powerful tool to have in your toolkit. It can simplify your development workflow, streamline your deployment process, and enable new, efficient architectures for your applications.

Of course, Docker is a vast ecosystem, and we‘ve only scratched the surface in this tutorial. As you continue your Docker journey, you‘ll encounter advanced topics like orchestration, networking, security, and more. But armed with a solid understanding of Docker‘s fundamentals, you‘re well-equipped to tackle these challenges.

Remember, like any tool, Docker is not a silver bullet. It‘s important to understand when and how to use containers effectively. But when used appropriately, Docker can be a game-changer for your applications and your organization.

Happy Dockerizing!

[^1]: Stack Overflow Developer Survey 2020, https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-platforms-loved
[^2]: 8 surprising facts about real Docker adoption, https://www.datadoghq.com/docker-adoption/
[^3]: What is a Container?, https://www.docker.com/resources/what-container/

Similar Posts