Docker is a powerful containerization service that enables the running of multiple applications on the same operating system, each in logically isolated environments. This isolation allows applications to operate independently without interfering with one another, ensuring that they have their own dependencies, libraries, and configurations. This architecture provides developers with numerous benefits, including:
Key Benefits of Docker
Consistency: Applications run in a pre-defined environment specified in your image configuration, maintaining uniformity across development, testing, and production stages. This significantly reduces the "it works on my machine" problem.
Scalability: Applications can be easily scaled by adding or removing containers as needed. In modern microservices architecture, this is particularly beneficial, as individual components can scale independently based on demand.
CI/CD Integration: The ease of stopping and starting containers facilitates rapid deployment and seamless integration into Continuous Integration/Continuous Deployment (CI/CD) pipelines, allowing for faster iterations and updates to applications.
Docker provides several features covering image creation, storage options, and container orchestration, all of which will be discussed in this post.
Image Creation
A Dockerfile contains the instructions that tell Docker how to build your container. An example of a Dockerfile may look like this:
FROM node:18-alpine WORKDIR /appCOPY . . RUN CMD EXPOSE 3000
Breakdown of the Dockerfile
FROM node:18-alpine: This defines the base image for your application. It uses a Node.js image with Node.js pre-installed, version 18, based on the Alpine Linux distribution, which provides a smaller image size.
WORKDIR /app: This sets the working directory inside the container to /app. If the directory does not exist, Docker will create it.
COPY . .: This command copies all files from your current directory on the host into the /app directory in the container.
RUN : This runs any necessary commands during the container build process, such as installing dependencies or compiling assets.
CMD : This defines the command that will be executed when the container starts.
EXPOSE 3000: This declares that the application will be listening on port 3000, allowing other services or users to connect to it.
Building the Docker Image
To build the Docker image, use the following command:
docker build -t .
-t: This tags your image with a name, making it easier to reference later.
.: This tells Docker to look for the Dockerfile in the current directory.
Running the Docker Container
To run the container, use the following command:
docker run -dp 127.0.0.1:3000:3000
-d: This runs the container in detached mode, allowing it to run in the background.
-p: This creates a port mapping between the host and the container in the format HOST:CONTAINER, publishing the container’s port 3000 to 127.0.0.1:3000 on the host.
Stopping and Removing Containers
Once you are finished with your application, you can either shut it down from the Docker UI or use the following commands:
docker ps: Lists all running containers (note the ID of the container you wish to stop).
docker stop : Stops the specified container.
docker rm : Removes the specified container.
Storage
Due to the ephemeral nature of containers, all data within a container is lost once it is shut down. This can be problematic for applications that require persistent data, such as databases. Docker offers two types of storage options to address this issue:
Volumes
Volumes are fully managed by Docker and are isolated from the host filesystem, making them inaccessible from the host unless explicitly mounted. They also provide better performance than bind mounts.Creating and mounting a volume is straightforward and involves two commands:
docker volume create docker run -dp --mount type=volume,src=,target=
Breaking Down the Docker Run Command
type: Can be volume or bind.
src: The name of the volume you created.
target: Where the volume will be mounted. Any data written to this path will be saved in the volume.
Bind Mounts
Mounting a bind is also simple. The following command is used:
docker run -dp --mount type=bind,src=,target=
In this command, the mount type is “bind,” and the source is a directory path instead of a volume name.As changes are made in the target destination, those changes will be recorded almost instantly in the source destination/bind directory. This functionality can be paired with development tools like Nodemon to listen for changes in the development environment and automatically restart the server it’s hosting.
Orchestration
Another feature Docker offers is container orchestration, which manages multi-container applications. In modern development, it’s common to see applications split into several “microservices,” which helps to decouple an otherwise large and cumbersome application.
Benefits of Microservices Architecture
No Single Point of Failure: If one component of your application fails, the rest can continue running, enhancing the overall reliability of the application.
Better Organization: Microservices help organize your infrastructure better, making it easier to manage and scale individual components.
Docker Compose
Docker provides orchestration capabilities through Docker Compose, which uses YAML files to define your containers and any storage options. A typical Compose file might look like this:
version: '3.8'
services:
app:
image: node:18-alpine
command: sh -c "yarn install && yarn run dev"
ports:
- 127.0.0.1:3000:3000
working_dir: /app
volumes:
- ./:/app
environment:
MYSQL_HOST: mysql
MYSQL_USER: root
MYSQL_PASSWORD: secret
MYSQL_DB: todos
mysql:
image: mysql:8.0
volumes:
- todo-mysql-data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: todos
volumes:
todo-mysql-data:
Breaking Down the Compose File
services: These are the containers you wish to deploy. In the example above, two containers are declared: one for the application and one for the database.
volumes: This defines the persistent storage used to capture database changes.
Once you have you’ve defined your compose file, you start every thing with:
docker compose up
And similarly, shutting everything down with:
docker compose down
Additional Features of Docker Compose
- Watch: Similar to bind mounts, Compose will ‘listen’ for changes in your application and update it as you make those changes.
services:
web:
build: .
command: npm start
develop:
watch:
- action: sync
path: ./web
target: /src/web
ignore:
- node_modules/
- action: rebuild
path: package.json path: package.json
In the example above, whenever a change is detected in the /web directory, Compose write the change to the target directory at /src/web. Once everything has been copied the application is updated.
- Secrets: To adhere to security best practices, you should never reveal secrets in plain text. Compose allows you to specify a secrets file and reference it instead of explicitly stating sensitive information in your configuration.
Final Notes
Docker is a powerful tool that simplifies the development, deployment, and management of applications through containerization. By leveraging Docker's features, developers can create consistent environments, scale applications efficiently, and integrate seamlessly into CI/CD workflows. Whether you're working on a small project or a large-scale microservices architecture, Docker provides the tools you need to streamline your development process.