Docker
Hi, I'm vaseem akram, a dedicated and passionate DevOps Engineer with extensive experience in automation, continuous integration, and infrastructure management.
How things work in the industry:
Let's consider the following scenario:
You get assigned a ticket to implement a login functionality. One developer works on this feature, creates the code, commits it, and pushes it to GitHub.
The developer then tells the DevOps engineer to deploy this code.
The DevOps engineer goes to the appropriate environment, clones the code from GitHub, and asks the developer what the procedure is for running the code.
The developer suggests checking the
README.mdfor the setup instructions. The DevOps engineer follows the steps from theREADME.md, but ends up getting an error, like "Java not found."The DevOps engineer then complains, saying it’s not working, and the developer responds, "It’s working on my local machine, but not in production."
This kind of issue arises because the local development environment and the production environment often don’t match, causing discrepancies like missing software versions or dependencies. The solution to this is containers.
Why do we need containers?
Containers help solve these issues by bundling all the required code and software dependencies into a single unit that can run consistently across different environments, without any mismatch. This container can be created using Docker.
The history of Docker and why it’s useful:
A few years ago, the common approach for running applications on multiple systems was using Virtual Machines (VMs).
In a VM, you have an operating system (like Linux) that runs on a host system. Under this operating system, you have the kernel, and under the kernel, there are resources like RAM, ROM, and networking. This operating system manages all resources and can run multiple virtual machines using a hypervisor.
You can run different operating systems (Windows, Linux, Mac) on top of this host OS using a hypervisor. For example, you could allocate 4GB of RAM to a virtual machine running Windows, while the host system might have 8GB of RAM.
However, this approach has some inefficiencies. Virtual machines are heavy, requiring significant resources, and each VM needs its own operating system and kernel.
Docker as a solution:
Docker is an alternative that helps solve these issues.
Just like a VM has an OS under the hood, Docker operates on top of the host operating system. However, in Docker, you don’t need to allocate resources for a full OS. Instead, you use the Docker Engine to run containers.
In Docker, containers share the host OS kernel, meaning you don’t have the overhead of running an entire OS in each container. Containers are lightweight because they only include what’s necessary to run an application (without duplicating the entire OS).
This makes Docker much more efficient than VMs. With Docker, you can run more containers on the same system, which is not only more resource-efficient but also allows for faster application deployment.
Docker Engine:
The Docker Engine consists of two key components:
Docker CLI: The Command-Line Interface, which runs commands to interact with Docker.
Docker Daemon (dockerd): The background process that manages Docker containers and images.
Docker daemon (dockerd) interacts with containerd, which is an open-source container management tool. Containerd provides the necessary tools to create, manage, and run containers.
When you run Docker commands from the CLI, they are passed to the Docker daemon (dockerd), which in turn communicates with containerd to manage the containers.
Docker Components:
Dockerfile: A text file that contains instructions for building a Docker image.
Docker Image: A snapshot of a filesystem and application code that can be executed in a container.
Docker Container: A running instance of a Docker image.
The process flows as follows:
A Dockerfile is used to create a Docker Image.
The Docker Image is used to run a Docker Container.
Containerd is the underlying tool that Docker uses to manage and interact with containers, allowing you to create, destroy, and manage containers.
Docker Overview and Setup
Docker allows you to package applications and all their dependencies into a standardized unit called a container. Containers are lightweight, portable, and run consistently across various environments.
Installing Docker on Ubuntu:
You used the following commands to install Docker:sudo apt-get updateupdates the package manager's local repository index.sudo apt-get installdocker.io-yinstalls Docker.systemctl status dockerchecks if the Docker service is running.
After installing Docker, you may face a permission denied issue when trying to use Docker commands as a regular user. This happens because Docker commands require root access or membership in the docker group.
To resolve this, you added the user to the Docker group:
bashCopyEditsudo usermod -aG docker <username>
sudo reboot
Creating Containers
Once Docker is set up and you've resolved the permission issue, you can start creating containers from Docker images. Docker images are like blueprints for containers.
Example 1: Running a MySQL Container
You used the following command to run a MySQL container:
bashCopyEditdocker run -e MYSQL_ROOT_PASSWORD=test@123 mysql:5.7
Explanation:
docker run: Creates and starts a container from a specified image.-e MYSQL_ROOT_PASSWORD=test@123: Sets an environment variableMYSQL_ROOT_PASSWORDto configure the MySQL root password.mysql:5.7: Specifies the image to use (in this case, MySQL version 5.7).
After the container is running, you can access it with:
bashCopyEditdocker exec -it <container_id> bash
mysql -u root -p
This starts a MySQL client inside the running container.
Building Docker Images
A Dockerfile is a script that contains a series of instructions to create a Docker image. Here's how you can create a Java application image:
Example: Building a Java Application Docker Image
dockerfileCopyEdit# Get a base image with JDK installed
FROM openjdk:11
# Set the working directory to /app
WORKDIR /app
# Copy the Java code into the container
COPY Hello.java .
# Compile the Java code
RUN javac Hello.java
# Run the compiled Java code
CMD ["java", "Hello"]
Dockerfile Instructions
FROM
TheFROMinstruction specifies the base image for your Docker image. Every Dockerfile starts with theFROMinstruction.Theory:
It sets the base image for the Docker image.
You can use official images from Docker Hub or custom images.
Example:DockerfileCopyEdit# Use an official Node.js runtime as the base image FROM node:14Here, we are using the official Node image tagged 14 as the base image.
LABEL
TheLABELinstruction adds metadata to an image, like the maintainer’s name, version, description, etc.Theory:
It helps with adding key-value pairs of metadata that can describe the image.
Example:DockerfileCopyEditLABEL maintainer="your-email@example.com" LABEL version="1.0" LABEL description="This is a simple Node.js application"Here, the
LABELinstruction adds metadata to the Docker image.RUN
TheRUNinstruction executes a command inside the Docker image during the image build process. It can be used to install software or make modifications to the image.Theory:
RUNis often used to install software packages or execute shell commands during the build process.
Example:DockerfileCopyEdit# Install dependencies in the container RUN apt-get update && apt-get install -y curlThis example updates the package list and installs
curlinside the image.CMD
TheCMDinstruction provides the default command that gets executed when a container is started from the image. If a command is passed while running the container, it overrides theCMDinstruction.Theory:
CMDdefines the default executable for the container. You can use either the exec form (CMD ["executable", "param1", "param2"]) or the shell form (CMD ["param1", "param2"]).
Example:DockerfileCopyEditCMD ["node", "app.js"]This will run
node app.jswhen the container starts. If you specify a command while running the container, this command will be overridden.ENTRYPOINT
TheENTRYPOINTinstruction sets the main command for the container to execute. UnlikeCMD, which can be overridden when running a container,ENTRYPOINTcannot be overridden unless specified explicitly with--entrypoint.Theory:
TheENTRYPOINTdefines the primary command to be executed.
It is typically used when you always want the container to run the same command.
Example:DockerfileCopyEditENTRYPOINT ["python3", "app.py"]This makes sure that whenever the container is started,
python3app.pyis executed.COPY
TheCOPYinstruction copies files from the host system into the container’s file system.Theory:
COPYis used to copy files or directories from the build context to the container. It’s simpler and more efficient thanADDif you're just copying files.
Example:DockerfileCopyEditCOPY . /appThis copies all files from the current directory on the host to the
/appdirectory inside the container.ADD
TheADDinstruction is similar toCOPYbut with some additional features, such as automatically extracting tar archives or downloading remote files.Theory:
ADDcan fetch files from remote URLs and untar archives.COPYcannot do this.
Example:DockerfileCopyEditADD https://example.com/file.tar.gz /appThis will download the file from the URL and add it to the
/appdirectory inside the container.WORKDIR
TheWORKDIRinstruction sets the working directory inside the container for anyRUN,CMD,ENTRYPOINT,COPY, andADDinstructions that follow.Theory:
Sets the working directory for commands. If the directory doesn't exist, it’s created.
Example:DockerfileCopyEditWORKDIR /appThis sets
/appas the working directory for subsequent commands.ENV
TheENVinstruction sets environment variables that can be used inside the container.Theory:
Environment variables are often used for configuration or to pass data inside the container.
Example:DockerfileCopyEditENV NODE_ENV=productionThis sets the
NODE_ENVenvironment variable toproduction.EXPOSE
TheEXPOSEinstruction informs Docker that the container will listen on the specified network ports at runtime.Theory:
It does not publish the port but serves as documentation to let the user know which ports the container will use.
Example:DockerfileCopyEditEXPOSE 8080This informs Docker that the container will listen on port 8080.
VOLUME
TheVOLUMEinstruction creates a mount point with a specified path inside the container for persistent storage.Theory:
Volumes are used to persist data and share it between containers.
Data inside a volume remains even if the container is stopped or removed.
Example:DockerfileCopyEditVOLUME /dataThis creates a volume at
/datainside the container.USER
TheUSERinstruction sets the user name or UID (and optionally the group name or GID) to use when running the container.Theory:
It’s a good practice to run containers as a non-root user for security reasons.
Example:DockerfileCopyEditUSER nodeThis makes the
nodeuser the default user inside the container.ARG
TheARGinstruction defines a build-time variable that can be passed to the Docker build process.Theory:
ARGallows for parameterizing the Dockerfile with variables that can be set during the build.
Example:DockerfileCopyEditARG VERSION=1.0This defines a build argument
VERSIONwith a default value of1.0. You can override this value at build time like so:bashCopyEditdocker build --build-arg VERSION=2.0 .STOPSIGNAL
TheSTOPSIGNALinstruction sets the signal that Docker should send to stop the container.Theory:
It allows you to specify a custom stop signal other than the defaultSIGTERM.
Example:DockerfileCopyEditSTOPSIGNAL SIGINTThis will make Docker use
SIGINTinstead ofSIGTERMwhen stopping the container.SHELL
TheSHELLinstruction allows you to specify the default shell to use when running commands in the Dockerfile.Theory:
By default, Docker uses/bin/sh -c, but you can override it with this instruction.
Example:DockerfileCopyEditSHELL ["/bin/bash", "-c"]This changes the default shell to
/bin/bash.HEALTHCHECK
TheHEALTHCHECKinstruction tells Docker how to test if a container is healthy or not. It can help Docker manage containers by automatically restarting unhealthy containers.Theory:
It runs a command inside the container to determine if the container is healthy.
Example:DockerfileCopyEditHEALTHCHECK CMD curl --fail http://localhost:8080 || exit 1This checks if the service inside the container on port 8080 is responding.
Example Dockerfile:
Here’s a simple example using several instructions:
DockerfileCopyEdit# Step 1: Use Node.js base image
FROM node:14
# Step 2: Set working directory
WORKDIR /app
# Step 3: Copy the package.json file
COPY package.json .
# Step 4: Install dependencies
RUN npm install
# Step 5: Copy the rest of the application files
COPY . .
# Step 6: Expose the port the app will run on
EXPOSE 3000
# Step 7: Set environment variable
ENV NODE_ENV=production
# Step 8: Define the default command
CMD ["npm", "start"]
This Dockerfile builds an image for a Node.js application, setting up the environment, installing dependencies, and defining a default command to start the app.
You can build the Docker image using:
bashCopyEditdocker build -t java_app:latest .
Running Containers from Images
To run a container from the built image:
bashCopyEditdocker run java_app:latest
Docker Volumes (Simplified)
Docker volumes are used to persist data generated by and used by Docker containers. They allow data to survive container restarts and removal, and also enable data sharing between containers. Volumes are stored outside the container's filesystem, making them ideal for data that needs to persist even after containers are stopped or deleted.
Types of Docker Volumes
Named Volumes
Anonymous Volumes
Host Volumes (Bind Mounts)
1. Named Volumes
What are Named Volumes?
Named volumes are Docker-managed volumes that have a specific name and are stored in Docker's default volume directory (
/var/lib/docker/volumeson Linux).Named volumes are useful when you need persistent storage that can be easily referenced by container names.
Example:
bashCopyEdit# Create a named volume docker volume create my-volume # Run a container and mount the named volume docker run -d -v my-volume:/data --name my-container my-imageHere, a named volume
my-volumeis created and mounted to/datainside the container. Data written to/datawill persist in the volume, even after the container is removed.Use case:
Useful for database containers where data needs to persist between container restarts.
2. Anonymous Volumes
What are Anonymous Volumes?
Anonymous volumes are automatically created by Docker when you mount a volume but do not specify a name.
These volumes are less manageable compared to named volumes since they don't have a name, but they still allow data persistence.
Example:
bashCopyEdit# Run a container with an anonymous volume docker run -d -v /data --name my-container my-imageHere, Docker creates an anonymous volume and mounts it to
/datainside the container.Use case: Often used when you want Docker to manage the volume without needing to name it. Typically used for temporary storage.
3. Host Volumes (Bind Mounts)
What are Host Volumes?
Host volumes (or bind mounts) allow you to mount a directory or file from the host system directly into the container.
Unlike named volumes, host volumes are tied directly to the file system on the host machine.
This is useful for accessing host system data directly, such as configuration files or logs.
Example:
bashCopyEdit# Run a container and mount a host directory to the container docker run -d -v /path/on/host:/path/in/container --name my-container my-imageIn this example,
/path/on/hostis a directory on your host system, and/path/in/containeris where it will be mounted inside the container. Any changes to the mounted folder will be reflected on both the host and the container.Use case: Ideal for sharing source code or logs between the host system and the container, or when you need to access host-specific files inside a container.
Example: Persisting MySQL Data
Without volumes, data inside a container is ephemeral. To persist data, you can create a volume and mount it inside the container:
docker run -d -e MYSQL_ROOT_PASSWORD=root -v /path/to/volume:/var/lib/mysql --name mysql-safe mysql:latest
-v /path/to/volume:/var/lib/mysql: Mounts a host directory (/path/to/volume) into the container (/var/lib/mysql). This ensures that MySQL's data is persisted on the host system.
Docker Networks
Docker networks allow containers to communicate with each other and with the external world. By default, Docker creates a "bridge" network, which is used by containers unless specified otherwise. Docker provides several types of networks, each with its specific use cases.
Types of Docker Networks
Bridge Network
Default Network: If no network is specified when you run a container, Docker uses the
bridgenetwork.Use Case: It's typically used for containers that need to communicate on the same host.
Example: Useful for small applications or multi-container apps that need to communicate internally.
Host Network
Host’s Networking: In this mode, the container shares the network stack of the host. The container doesn’t get its own IP address but uses the host's IP.
Use Case: Best when performance is crucial and when containers need direct access to the host’s networking (e.g., network monitoring tools or apps requiring high network performance).
None Network
No Network: In this mode, the container does not have access to any network. It’s isolated from the host network.
Use Case: Used when a container should not be able to access any network (e.g., for containers that need no network connectivity or for strict security reasons).
Overlay Network
Multi-Host Networking: This network type is used when containers are deployed across multiple Docker hosts (typically in a Docker Swarm or Kubernetes setup). It creates a virtual network that spans across multiple hosts.
Use Case: Best suited for multi-host or multi-node deployments in distributed systems.
Macvlan Network
Direct MAC Address: This allows containers to appear as physical devices on the network with their own MAC addresses.
Use Case: Useful when containers need to be directly accessible by external network devices (e.g., legacy applications that require direct network access).
Custom User-Defined Network
User-Defined Bridge Networks: This type of network provides more advanced features such as automatic DNS resolution for containers, allowing containers on the same network to refer to each other by container names.
Use Case: This is a good option for communication between containers in applications that require direct and reliable communication.
Explanation of Docker Network Commands in the Example
In the given Dockerfile and usage context, various Docker network commands are relevant:
1. docker network create
This command is used to create a custom Docker network.
Example:
bashCopyEditdocker network create --driver bridge my-network
Usage:
--driver bridge: Specifies the type of network driver. In this case,bridgemeans the containers will communicate on the same host.my-network: The name of the network being created.
Purpose in the Example:
- The
docker network createcommand is useful when you need a custom network for isolated communication between containers, such as in multi-container applications.
2. docker run --network
The docker run command is used to start a new container. The --network flag allows you to specify the network the container should join.
Example:
bashCopyEditdocker run --network my-network --name my-container my-image
Usage:
--network my-network: This flag tells Docker to connect the container tomy-network(the user-defined bridge network).--name my-container: Names the container for easier reference.my-image: The image from which the container will be created.
Purpose in the Example:
- This command ensures that the container runs in the specified custom network (
my-network), allowing containers in the same network to communicate with each other.
3. docker inspect
This command is used to retrieve detailed information about a Docker container or network.
Example:
bashCopyEditdocker inspect my-network
Usage:
my-network: The name of the network whose information you want to inspect.
Purpose in the Example:
- This command helps you view the network configuration, including the connected containers, network settings, IP ranges, etc. It’s useful for troubleshooting and ensuring that the containers are correctly connected to the intended network.
4. docker network ls
This command lists all the Docker networks on the system.
Example:
bashCopyEditdocker network ls
Usage:
- Lists the names and types of all networks created in Docker.
Purpose in the Example:
- This helps you check the available networks, whether the custom network
my-networkis created and available for use.
5. docker network connect
This command allows you to connect an existing container to a network after it’s already running.
Example:
bashCopyEditdocker network connect my-network my-container
Usage:
my-network: The network to which the container should be connected.my-container: The running container that should be connected to the network.
Purpose in the Example:
- If a container was not started with the
--networkoption but you want it to join a network afterward, this command adds it to the specified network.
6. docker network disconnect
This command disconnects a container from a network.
Example:
bashCopyEditdocker network disconnect my-network my-container
Usage:
- Disconnects
my-containerfrommy-network.
Purpose in the Example:
- This command can be useful if you want to isolate a container from a network or change its network configuration.
Conclusion
Docker networks provide a way to control how containers communicate. Using the appropriate network type based on your application’s requirements can improve performance, security, and manageability.
In the above commands, the --network option is crucial for ensuring containers are part of the desired network for communication. Additionally, using docker network create, docker network connect, and docker network inspect gives you full control over the networking environment of your Dockerized applications.
Example: Connecting Flask and MySQL Containers
Create a network:
docker network create twotierRun MySQL and Flask containers on this network:
docker run -d --network twotier --name mysql -e MYSQL_ROOT_PASSWORD=root mysql:5.7 docker run -d --network twotier --name flask -e MYSQL_HOST=mysql -e MYSQL_PASSWORD=root flask-image
What is Docker Compose?
Docker Compose is a tool that allows you to define and manage multi-container Docker applications. With Docker Compose, you can define all your services (containers), networks, and volumes in a single YAML file (docker-compose.yml). This simplifies the process of running multiple containers together, such as a frontend and backend application or a database with its application.
Docker Compose is especially useful for applications that need to interact with multiple services (like databases, caches, etc.), as it lets you define and orchestrate all the services in a single command.
Explanation of the Example Docker Compose File
Here's the breakdown of the provided docker-compose.yml example for a two-tier application (backend and MySQL database):
yamlCopyEditversion: "3"
services:
backend:
build: .
ports:
- "5000:5000"
environment:
MYSQL_HOST: mysql
MYSQL_USER: root
MYSQL_PASSWORD: test@123
MYSQL_DB: KYC
depends_on:
- mysql
mysql:
image: mysql:5.7
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: test@123
MYSQL_DATABASE: KYC
volumes:
- mysql-data:/var/lib/mysql
volumes:
mysql-data:
Key Sections:
version: "3"
Specifies the version of the Docker Compose file format.
Version 3 is the most commonly used format for production setups.
services:
- This section defines the containers or services that make up the application. Here, we have two services:
backendandmysql.
- This section defines the containers or services that make up the application. Here, we have two services:
backend service:
build: .
- Specifies that the backend container will be built from the Dockerfile in the current directory (
.).
- Specifies that the backend container will be built from the Dockerfile in the current directory (
ports:
"5000:5000": Maps port5000on the host to port5000on the container. This allows you to access the backend application atlocalhost:5000(if running on your local machine).
environment:
Defines environment variables that are passed into the container. These variables are used to configure the backend application to connect to the MySQL database.
MYSQL_HOSTrefers to the MySQL service (because it's defined asmysqlin Docker Compose).MYSQL_USER,MYSQL_PASSWORD, andMYSQL_DBare used to specify credentials and the database name for the connection.
depends_on:
- Ensures that the
backendservice will not start until themysqlservice is up and running. This is important for ensuring the backend doesn't try to connect to the database before it's ready.
- Ensures that the
mysql service:
image: mysql:5.7
- Specifies the MySQL Docker image to use, in this case, version
5.7.
- Specifies the MySQL Docker image to use, in this case, version
ports:
"3306:3306": Maps port3306on the host to port3306on the MySQL container, making the database accessible from the host system on port3306.
environment:
- Defines the root password and the name of the database (
KYC) for MySQL. These variables are used by the MySQL image to set up the database during container startup.
- Defines the root password and the name of the database (
volumes:
- mysql-data:/var/lib/mysql: This is a named volume (defined later) that stores MySQL data. It persists data outside the container so that even if the container is removed, the data remains intact. The volume is mounted to the MySQL data directory (/var/lib/mysql), where MySQL stores its databases.
volumes:
- This section defines named volumes that Docker will manage. In this case, the volume
mysql-datais used to persist MySQL data across container restarts and removals.
- This section defines named volumes that Docker will manage. In this case, the volume
What We Used and Why:
services:- Used to define the containers (
backendandmysql) that make up the multi-container application. This is where you specify container details like images, build contexts, environment variables, ports, etc.
- Used to define the containers (
build: .:- Used for the backend service to build the Docker image from the Dockerfile in the current directory. This is useful for custom applications that need to be built from source.
ports:- Used to expose container ports to the host system. This is crucial for enabling communication with the container. For example, the backend is accessible via
localhost:5000, and the MySQL database is accessible via port3306.
- Used to expose container ports to the host system. This is crucial for enabling communication with the container. For example, the backend is accessible via
environment:- Sets environment variables that configure the behavior of the services. For example, the backend needs to know how to connect to MySQL, so it gets the database credentials through environment variables.
depends_on:- Ensures that the
backendcontainer waits for themysqlcontainer to be ready before starting. This is important to avoid the backend trying to connect to MySQL before it's up and running.
- Ensures that the
volumes:- Volumes are used to persist data. The
mysql-datavolume ensures that the database data is stored outside the MySQL container and survives container restarts and removal. It keeps your data safe and persistent.
- Volumes are used to persist data. The
Running the Services:
To start the multi-container application, use this command:
bashCopyEditdocker-compose up -d
-dstands for "detached mode," which means the containers will run in the background.
This will build the containers (if needed), start the MySQL database and the backend application, and connect them based on the configuration you provided in the docker-compose.yml file.
Summary:
Docker Compose simplifies managing multi-container applications by defining services, networks, and volumes in a single YAML file. In the above example:
We have a backend service and a MySQL database service.
The backend waits for MySQL to start, and the data from MySQL is persisted using a named volume (
mysql-data).Ports are mapped to allow communication between the host and the containers.
What is a Multistage Dockerfile?
A multistage Dockerfile is a Dockerfile that uses multiple FROM instructions to define different stages in the build process. Each stage can use a different base image, and you can selectively copy files from one stage to another. This allows you to keep the final Docker image small and clean by excluding unnecessary dependencies that were only needed during the build phase.
The key benefit of multistage builds is that they allow you to separate the build environment (which may require many tools and dependencies) from the runtime environment (which only needs the final application and its runtime dependencies). By doing this, you can create a smaller, more efficient Docker image for deploying your application.
Why Do We Use Multistage Dockerfiles?
Smaller Final Image: By separating build and runtime stages, you can avoid including unnecessary tools, libraries, and dependencies in the final image, reducing its size.
Cleaner and More Secure: Only the essential files and dependencies are included in the final image, reducing potential security risks.
Improved Build Process: Multistage builds can make your Dockerfile easier to maintain and more modular, especially for complex applications that need different build environments.
Efficiency: You don't need to keep the intermediate images around once they have served their purpose, making the process more efficient.
Example of a Multistage Dockerfile for a Flask Application
Let's break down the example provided:
dockerfileCopyEdit# Stage 1: Build stage
FROM python:3.9 AS backend-builder
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
# Stage 2: Runtime stage
FROM python:3.9-slim
WORKDIR /app
COPY --from=backend-builder /usr/local/lib/python3.9/site-packages/ /usr/local/lib/python3.9/site-packages/
COPY --from=backend-builder /app /app
CMD ["python", "run.py"]
Explanation of Each Stage:
Stage 1: Build Stage
FROM python:3.9 AS backend-builder: This defines the first stage using the
python:3.9image, which includes all the tools needed for building the application, such as pip and other dependencies.WORKDIR /app: Sets the working directory to
/appinside the container. All following commands will be executed in this directory.COPY . .: Copies the entire current directory (the application code) into the
/appdirectory inside the container.RUN pip install -r requirements.txt: Installs all the dependencies listed in the
requirements.txtfile, which is typical for Python-based applications. This is done in the build stage, as this stage has all the necessary dependencies and tools.
Stage 2: Runtime Stage
FROM python:3.9-slim: This defines the second stage using the
python:3.9-slimimage, which is a much smaller image compared to the first one because it doesn't include the build tools (likepipor other development dependencies).WORKDIR /app: Sets the working directory to
/appinside the container, similar to the first stage.COPY --from=backend-builder /usr/local/lib/python3.9/site-packages/ /usr/local/lib/python3.9/site-packages/: This copies the installed dependencies (from the first stage) to the appropriate directory in the runtime image. We're using the
--from=backend-builderoption to copy files from the first stage (backend-builder).COPY --from=backend-builder /app /app: This copies the application code from the first stage into the runtime stage.
CMD ["python", "run.py"]: Specifies the command to run when the container starts. In this case, it starts the Flask application using the
run.pyscript.
Use Case and Why It Works:
Why Use a Multistage Dockerfile for a Flask Application?
Build Stage (Stage 1):
In the first stage, we use a full Python image (
python:3.9), which includes all the dependencies and tools required to install Python packages (pip), install the application's dependencies, and compile any required assets.However, we don't want to carry over these build tools into the final image because they are unnecessary for running the Flask application.
Runtime Stage (Stage 2):
In the second stage, we use a much smaller base image (
python:3.9-slim). This image only includes the necessary runtime components for running Python applications, which drastically reduces the image size.We copy over only the essential parts from the build stage (application code and installed dependencies) to the final image. This way, we avoid carrying over unnecessary files, such as build tools, caches, and other development dependencies.
Benefits in This Example:
Smaller Image Size: The final image is smaller because the build tools and dependencies are not included in the runtime image.
Security: By removing unnecessary build tools from the runtime image, you reduce the attack surface and make the image more secure.
Faster Deployment: With a smaller image, deployment is faster and uses fewer resources.
Summary:
A multistage Dockerfile allows you to optimize the Docker build process by separating the build environment from the runtime environment. In the provided example:
The build stage installs dependencies and prepares the application.
The runtime stage uses a smaller image, reducing the size of the final Docker image and only includes what is necessary to run the application.
This approach is highly beneficial when working with applications that require complex build dependencies, ensuring a clean, efficient, and secure final image.
Docker Security (Docker Scout)
Docker Scout is a tool that helps you identify vulnerabilities and potential risks in your Docker images by scanning them for known security issues, outdated dependencies, and other risks. Below are some examples of using Docker Scout and similar tools like Trivy for scanning Docker images.
1. Using Docker Scout to Scan an Image:
Docker Scout can be used to scan your images for security vulnerabilities. Here's a basic command to scan a Docker image:
bashCopyEditdocker scout quickscan <image-name>
Example:
bashCopyEditdocker scout quickscan myapp:latest
This command will scan the myapp:latest Docker image for known security vulnerabilities and provide a summary of the results.
2. Scan with Detailed Results:
To get more detailed information about the vulnerabilities detected in an image, use the --detail flag:
bashCopyEditdocker scout quickscan --detail <image-name>
Example:
bashCopyEditdocker scout quickscan --detail myapp:latest
This will give you a more in-depth report, including specifics about each vulnerability, such as severity, CVE (Common Vulnerabilities and Exposures) identifiers, and potential fixes.
3. Scan Local Docker Image (Local Image Scanning):
To scan a local image that has already been built (and is present in your local Docker environment), you can use:
bashCopyEditdocker scout quickscan <local-image-name>
Example:
bashCopyEditdocker scout quickscan my-local-image
4. Using Trivy to Scan Docker Images (Alternative to Docker Scout):
Trivy is another popular security scanning tool for Docker images. Here's how you can use Trivy to scan an image:
Install Trivy:
If you haven't installed Trivy yet, you can do so with:
bashCopyEditbrew install aquasecurity/trivy/trivy # macOS
sudo apt install trivy # Ubuntu
Scan a Docker Image Using Trivy:
bashCopyEdittrivy image <image-name>
Example:
bashCopyEdittrivy image myapp:latest
This will scan the myapp:latest image for vulnerabilities, and you'll get a detailed output including the vulnerabilities found, their severity levels, and possible remediation steps.
5. Scan Docker Image for Specific Vulnerability Types (Trivy Example):
You can also limit the scan to specific types of vulnerabilities like "severity", "config", etc., using the --severity option in Trivy:
bashCopyEdittrivy image --severity HIGH,CRITICAL <image-name>
Example:
bashCopyEdittrivy image --severity HIGH,CRITICAL myapp:latest
This will scan the image but only report vulnerabilities that are of HIGH or CRITICAL severity.
6. Scan a Specific Directory Using Trivy (For Local Dockerfiles/Contexts):
You can also scan the local Dockerfile or directory that contains your project files:
bashCopyEdittrivy fs <path-to-project-directory>
Example:
bashCopyEdittrivy fs /path/to/project
This command will scan your local directory for vulnerabilities related to files and configurations within it.
Summary of Common Commands:
Docker Scout (Quick Scan):
bashCopyEditdocker scout quickscan <image-name>Docker Scout (Detailed Scan):
bashCopyEditdocker scout quickscan --detail <image-name>Trivy (Scan Docker Image):
bashCopyEdittrivy image <image-name>Trivy (Scan with Severity Filter):
bashCopyEdittrivy image --severity HIGH,CRITICAL <image-name>Trivy (Scan Local Files):
bashCopyEdittrivy fs <path-to-project-directory>
Using these tools, you can identify vulnerabilities, outdated dependencies, and security risks in your Docker images, ensuring a more secure and stable environment for your application.
Summary
Docker allows you to build, run, and manage containers.
Dockerfile is a script that defines how to build a Docker image.
Volumes help to persist data outside of containers.
Networks allow containers to communicate with each other.
Docker Compose helps manage multi-container applications easily.
Multistage Dockerfiles help to create smaller and more efficient images by separating build and runtime stages.
Security tools like Docker Scout and Trivy help identify vulnerabilities in images.