Introduction
In the previous chapter, we set up our Docker development environment. Now, it’s time to put Docker to work by containerizing our first application. This chapter guides you through taking a simple web application and packaging it into a Docker image, making it portable and isolated.
By the end of this milestone, you will have a functional Python Flask web application running inside a Docker container. You’ll understand the fundamental components of a Dockerfile and how to build and run your custom images. This is a critical step towards building complex, multi-service applications, as it establishes the core pattern for isolating individual services.
Planning & Design: Our First Container
Our goal is to create a minimal web application and define its environment using a Dockerfile. This Dockerfile will serve as a blueprint for building an immutable Docker image. The image will then be used to launch a container, which is an isolated process running our application.
For this chapter, we’ll use a very simple Python Flask application that returns “Hello, Docker!”. This choice allows us to focus purely on Docker concepts without getting bogged down in application-specific complexities.
The file structure will be straightforward:
.
├── app.py
├── Dockerfile
└── requirements.txtHere’s the conceptual flow we’ll implement:
This diagram illustrates how our application code and dependencies are combined with a Dockerfile to create a Docker image. This image is then run as a container, which we can access via our browser.
Step-by-Step Implementation
Let’s get started by creating the necessary files for our simple web application.
1. Create the Project Directory
First, create a new directory for our project.
mkdir docker-web-app
cd docker-web-app2. Write the Web Application Code (app.py)
Inside the docker-web-app directory, create a file named app.py and add the following Python code.
# docker-web-app/app.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return "Hello, Docker! This is a containerized web app."
if __name__ == '__main__':
# Listen on all public IPs (0.0.0.0) and port 8000
# This is crucial for Docker containers to be accessible externally.
app.run(host='0.0.0.0', port=8000)Explanation:
from flask import Flask: Imports the Flask framework.app = Flask(__name__): Initializes a Flask application.@app.route('/'): Defines a route for the root URL (/).def hello(): return "Hello, Docker! ...": The function executed when the root URL is accessed.app.run(host='0.0.0.0', port=8000): Starts the Flask development server.host='0.0.0.0'is critical inside a Docker container. It tells the server to listen on all available network interfaces, making it accessible from outside the container. If you used127.0.0.1orlocalhost, the application would only be accessible within the container’s loopback interface, not from the host machine.port=8000: The port our application will listen on inside the container.
3. Define Application Dependencies (requirements.txt)
Next, create a file named requirements.txt in the same directory. This file lists the Python packages our application needs.
# docker-web-app/requirements.txt
Flask==2.3.3Explanation:
Flask==2.3.3: Specifies that our application requires Flask version 2.3.3. Pinning versions is a good practice for reproducibility, ensuring your application runs with the exact dependencies it was developed and tested with.
4. Create the Dockerfile
Now, create the Dockerfile in the docker-web-app directory. This file contains the instructions Docker will use to build your image.
# docker-web-app/Dockerfile
# Stage 1: Build Stage (using a larger image for build tools if needed, though not strictly for Flask)
# This uses the official Python image, version 3.9, based on Debian Buster slim.
# The 'slim-buster' variant is preferred over 'latest' or full images
# because it contains only the minimal packages needed, resulting in a smaller
# and more secure final image.
FROM python:3.9-slim-buster AS base
# Set the working directory inside the container.
# All subsequent commands will run from this directory.
WORKDIR /app
# Copy the requirements.txt file into the container at /app.
# This step is intentionally separated from copying the rest of the application
# code. Docker layers are cached. If only app.py changes, but requirements.txt
# does not, Docker can reuse the cached layer for dependency installation,
# speeding up subsequent builds.
COPY requirements.txt .
# Install any Python dependencies specified in requirements.txt.
# The --no-cache-dir flag helps keep the image size down by not storing pip's cache.
# The -r flag tells pip to install from the specified requirements file.
RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of the application code into the container.
# The '.' indicates copying everything from the current host directory (where Dockerfile is)
# to the current working directory in the container (/app).
COPY . .
# Expose port 8000. This informs Docker that the container listens on this port
# at runtime. It's metadata; it doesn't actually publish the port.
EXPOSE 8000
# Define the command to run when the container starts.
# Use the exec form (CMD ["executable", "param1", "param2"]) for better signal handling.
# This command starts our Flask application.
CMD ["python", "app.py"]Explanation of Dockerfile Instructions:
FROM python:3.9-slim-buster AS base:FROM: Specifies the base image for our build. We’re usingpython:3.9-slim-buster, which is a light-weight, official Python image based on Debian Buster. Usingslimvariants is a best practice for production images as they reduce image size and attack surface.AS base: Assigns a name to this build stage. While not strictly a multi-stage build yet, it’s good practice for future expansion.
WORKDIR /app:WORKDIR: Sets the working directory for subsequent instructions. All commands likeCOPYandRUNwill operate relative to/appinside the container.
COPY requirements.txt .:COPY: Copies files from the host machine (where you rundocker build) into the Docker image.- This copies
requirements.txtfrom our project directory to/appinside the image. - Caching Strategy: By copying
requirements.txtand installing dependencies before copying the rest of the application, Docker can cache this layer. If onlyapp.pychanges, Docker won’t re-runpip install, significantly speeding up builds.
RUN pip install --no-cache-dir -r requirements.txt:RUN: Executes commands during the image build process.- This command installs the Python packages listed in
requirements.txt. --no-cache-dir: Preventspipfrom storing downloaded packages in a cache, further reducing the final image size.
COPY . .:- Copies all remaining files from the current directory (
.) on the host into the/appdirectory in the image. This includesapp.py.
- Copies all remaining files from the current directory (
EXPOSE 8000:EXPOSE: Documents that the container listens on the specified network port(s) at runtime. It’s purely informational and doesn’t publish the port to the host. Port publishing is done with thedocker run -pcommand.
CMD ["python", "app.py"]:CMD: Specifies the default command to execute when a container is launched from this image.- Using the “exec form” (
["executable", "param1", "param2"]) is generally recommended as it allows Docker to handle signals (likeSIGTERMfor graceful shutdown) correctly.
5. Build the Docker Image
With the Dockerfile and application code in place, navigate to your docker-web-app directory in your terminal and build the Docker image.
docker build -t my-web-app:1.0 .Explanation:
docker build: The command to build a Docker image.-t my-web-app:1.0:-t(or--tag): Tags the image with a name and optional tag (version).my-web-app: The chosen name for our image.1.0: The version tag. It’s good practice to tag images for version control.
.: Specifies the build context (the directory containing theDockerfileand application files). Docker will send all files in this directory to the Docker daemon for the build process.
You should see output indicating each step of your Dockerfile being executed, followed by a successful build message.
6. Run the Docker Container
Now that the image is built, let’s run a container from it.
docker run -p 8000:8000 --name my-flask-app my-web-app:1.0Explanation:
docker run: The command to create and start a container from an image.-p 8000:8000:-p(or--publish): Publishes (maps) a container’s port to a host’s port.- The format is
HOST_PORT:CONTAINER_PORT. - Here, we map port
8000on our host machine to port8000inside the container. This means you can access the application from your host’s browser athttp://localhost:8000.
--name my-flask-app: Assigns a human-readable name to the container. This makes it easier to refer to the container later (e.g., for stopping or logging).my-web-app:1.0: The name and tag of the image we want to run.
Your terminal will now display the Flask application’s logs, indicating it’s running.
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://0.0.0.0:8000
Press CTRL+C to quitTesting & Verification
With the container running, let’s verify that our web application is accessible.
Access the Application: Open your web browser and navigate to
http://localhost:8000. You should see the message: “Hello, Docker! This is a containerized web app.”Inspect Container Logs: While the container is running in your current terminal, you’ll see its output. If you had run it in detached mode (
-d), you could check logs with:docker logs my-flask-appThis command shows the standard output and error streams from your running container, which is invaluable for debugging.
Stop and Remove the Container: To stop the running container, press
Ctrl+Cin the terminal wheredocker runis active. To remove the container (if it’s stopped), use:docker rm my-flask-appIf it’s still running, you’d first stop it:
docker stop my-flask-app docker rm my-flask-appdocker ps -awill show all containers, including stopped ones. Use it to confirmmy-flask-appis gone.
Production Considerations
Even with this simple application, we can apply some production-minded thinking.
- Image Size: Using
python:3.9-slim-bustersignificantly reduces image size compared to a fullpython:3.9image, which includes many development tools and libraries not needed at runtime. Smaller images mean faster downloads, less storage, and a reduced attack surface. - Non-Root User: For enhanced security, containers should ideally run processes as a non-root user. Our current
Dockerfileimplicitly runspython app.pyas root (the default user in most base images). In later chapters, we’ll implement explicit user creation and switching using theUSERinstruction. - Resource Limits: In a production environment, you would configure CPU and memory limits for your containers to prevent a single misbehaving application from consuming all host resources. This is typically done at runtime with
docker run --cpusand--memory. - Development vs. Production Servers: The Flask development server (
app.run()) is not suitable for production. It’s single-threaded and lacks robustness. In a real application, you would use a production-ready WSGI server like Gunicorn or uWSGI to serve your Flask app. We’ll introduce this in a future chapter.
Common Issues & Solutions
- “Hello, Docker!” not appearing in browser:
- Check
docker run -p: Ensure you correctly mapped the host port to the container port (-p 8000:8000). - Check
app.run(host='0.0.0.0'): Verify your Flask app is listening on0.0.0.0inside the container. If it’s127.0.0.1, it won’t be reachable from the host. - Check container logs: Use
docker logs my-flask-appto see if the application started successfully or if there are any errors.
- Check
ModuleNotFoundError: No module named 'flask':- This indicates Flask wasn’t installed inside the container. Double-check your
requirements.txtfile and theRUN pip installcommand in yourDockerfile. Ensurerequirements.txtis copied before theRUNinstruction.
- This indicates Flask wasn’t installed inside the container. Double-check your
docker buildfails with “no such file or directory”:- Ensure your
Dockerfile,app.py, andrequirements.txtare all in thedocker-web-appdirectory and that you are runningdocker buildfrom that directory (docker build -t ... .). The.at the end is crucial.
- Ensure your
Summary & Next Step
You’ve successfully containerized your first web application! You now have a solid understanding of:
- Creating a basic
Dockerfileto define an application’s environment. - Using
FROM,WORKDIR,COPY,RUN,EXPOSE, andCMDinstructions. - Building a Docker image with
docker build. - Running a Docker container and mapping ports with
docker run. - Basic verification steps and initial production considerations.
This isolated, portable application is now ready to be integrated with other services. In the next chapter, we will introduce a database service and learn how to orchestrate multiple containers using Docker Compose, moving us closer to a full-stack, production-ready environment.
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.