In the previous episode, we set up the foundation of our DevOps Capstone project and successfully ran the QR code application locally to transform URLs into QR codes. In this episode, we’ll dive into containerizing the application with Docker. This step will not only streamline deployment but also enhance the scalability and portability of our project, moving us closer to production readiness.
What is an application container?
An application container is a lightweight, self-contained package that bundles an application with its dependencies, libraries, runtime, and configurations, enabling it to run consistently across various environments. Containers isolate applications, sharing the host OS kernel, which reduces resource usage and allows fast, efficient scaling. Unlike virtual machines (VMs), which require a full OS per instance, containers are quicker to start and consume fewer resources, though VMs offer stronger instance isolation due to separate OS environments.
Why containerize applications?
Portability: Containers ensure consistent behavior across different environments by packaging dependencies and configurations together.
Scalability: With container orchestration, applications can scale quickly and adapt to changing loads, optimizing resource usage.
Productivity: Containers streamline environment setup with predictable, reproducible runtime environments, boosting developer efficiency.
Flexibility: Containers support a microservices architecture, allowing apps to be split into smaller services without significant overhead.
How to containerize an application using Docker
To containerize the application, we’ll start by creating a Dockerfile. This file contains instructions for building a Docker image, which packages everything needed to run the application, including code, runtime, libraries, and dependencies. Once the image is built, it can be used to create a Docker container.
Here’s the workflow for containerizing the application:
The steps below will guide you through the process of creating a Dockerfile for the QR code application.
Step 1: Clone QR code App
If you haven’t cloned the QR Code application from GitHub yet, refer to the previous episode for detailed instructions on completing this step.
Step 2: Create the Dockerfiles
In this step, we’ll Dockerize both the API and the front-end. We’ll use multi-stage Dockerfiles to keep image sizes small and follow best practices.
Dockerizing the Backend API
Let's start by creating a Dockerfile for our FastAPI backend.
Navigate to the API directory and create a Dockerfile:
$ cd qr-code-app/api $ touch Dockerfile
Write the following content in the Dockerfile:
# Build Stage FROM python:3.9 as base ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 WORKDIR /app COPY requirements.txt ./ RUN pip install -r requirements.txt COPY . . FROM python:3.9-slim RUN apt update && apt install -y dnsutils RUN pip install awscli WORKDIR /app ARG UID=10001 RUN adduser \ --disabled-password \ --home "/nonexistent" \ --shell "/sbin/nologin" \ --no-create-home \ --uid "${UID}" \ apiuser COPY --from=base /app ./ COPY --from=base /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages COPY --from=base /usr/local/bin /usr/local/bin USER apiuser EXPOSE 8000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
3. Breaking Down the Dockerfile:
Our Dockerfile uses two main stages: Build Stage and Run Stage.Build Stage:
FROM python:3.9 as base
: Uses Python 3.9 as the base image for the build stage.ENV PYTHONDONTWRITEBYTECODE=1
: Disables writing.pyc
files, keeping the image clean.ENV PYTHONUNBUFFERED=1
: Prevents Python from buffering output, which is helpful for logging and debugging.WORKDIR /app
: Sets the working directory to/app
.COPY requirements.txt ./
: Copies therequirements.txt
file to the container.RUN pip install -r requirements.txt
: Installs the required Python packages.COPY . .
: Copies the application source code into the container.
Run Stage:
FROM python:3.9-slim
: Switches to a lighter Python image to keep the final image size minimal.RUN apt update && apt install -y dnsutils
: Installs DNS utilities, which may be needed for network troubleshooting.RUN pip install awscli
: Installs AWS CLI for managing AWS resources.WORKDIR /app
: Sets the working directory.ARG UID=10001
andRUN adduser --disabled-password ...
: Creates a non-root user (apiuser
) with a specific user ID, enhancing security.COPY --from=base /app ./
and related lines: Copies the application files and Python packages from the build stage to the run stage.USER apiuser
: Switches to the non-root user for running the application, following security best practices.EXPOSE 8000
: Exposes port 8000, where the API will be accessible.CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
: Starts the FastAPI server using Uvicorn.
Following this Dockerfile, we create an optimized, multi-stage build that’s ready for deployment. The build stage installs dependencies, and the run stage uses a slimmer image with only what’s needed for running the application, reducing the final image size.
With the backend Dockerized, we’ll proceed to containerizing the front-end in the next steps.
Dockerizing the Frontend
Now let’s containerize the Next.js frontend application.
Navigate to the frontend directory and create a Dockerfile:
$ cd ../front-end-nextjs $ touch Dockerfile
Write the following content in the Dockerfile:
FROM node:18-alpine AS base WORKDIR /app COPY package*.json ./ RUN npm install COPY . . COPY .env .env RUN mkdir .next FROM node:18-alpine WORKDIR /app ARG UID=10001 RUN adduser \ --disabled-password \ --home "/nonexistent" \ --shell "/sbin/nologin" \ --no-create-home \ --uid "${UID}" \ nextjs-user COPY --from=base --chown=nextjs-user:nextjs-user /app /app USER nextjs-user EXPOSE 3000 CMD ["npm", "run", "dev"]
3. Breaking Down the Dockerfile:
Similar to the backend, this Dockerfile uses multi-stage builds with a Build Stage and a Run Stage to keep the final image light and optimized.
Build Stage:
FROM node:18-alpine AS base
: Uses Node.js 18 on an Alpine Linux base image for a lightweight environment.WORKDIR /app
: Sets the working directory to/app
within the container.COPY package*.json ./
: Copies bothpackage.json
andpackage-lock.json
files to the container.RUN npm install
: Installs the project dependencies listed inpackage.json
.COPY . .
: Copies all application code to the container.COPY .env .env
: Copies the environment configuration file,.env
, to the container.RUN mkdir .next
: Creates a.next
directory, which Next.js uses for build artifacts. This step prepares the directory structure needed during the build.
Run Stage:
FROM node:18-alpine
: Begins a new stage with the same Node.js 18 Alpine image to keep the final image small.WORKDIR /app
: Sets the working directory.ARG UID=10001
andRUN adduser --disabled-password ...
: Defines a user ID and creates a non-root user namednextjs-user
to follow best security practices.COPY --from=base --chown=nextjs-user:nextjs-user /app /app
: Copies the application code and dependencies from the build stage and changes ownership to thenextjs-user
, ensuring that our application runs as a non-root user.USER nextjs-user
: Switches to the non-root user for added security.EXPOSE 3000
: Specifies that the application will be accessible on port 3000.CMD ["npm", "run", "dev"]
: Starts the Next.js development server in watch mode for easy local testing and development.
This multi-stage Dockerfile keeps the image size minimal by separating the build environment from the runtime environment. With the front-end Dockerized, our application is now ready to be deployed as portable containers, enabling consistent development and deployment environments.
In the next step, we’ll build and run our Docker images to ensure that both the API and frontend are containerized and functioning as expected.
Step 3: Build and Run the Docker Images
With the Dockerfiles ready, the next step is to build Docker images for both the API and frontend components and then run containers based on these images.
Build and Run the API Container
Build the API Docker Image:
$ docker build -t <Username>/qr-api:v1 .
This command builds a Docker image for the Backend API of the QR code application.
The
t <Username>/qr-api:v1
option tags the image with a name (qr-api
) and a version (v1
), helping you manage different versions of the image.The
.
at the end specifies that Docker should use the Dockerfile in the current directory.
Note: Replace
<Username>
with your Docker Hub username or preferred tag.Run the API Container:
$ docker run -p 8000:8000 <Username>/qr-api:v1
This command runs a container based on the
qr-api
image you just built.
The
p 8000:8000
option maps port 8000 on your local machine to port 8000 in the container, making the API accessible atlocalhost:8000
.After running this command, your FastAPI server should be available at
http://localhost:8000
Build and Run the Frontend Container
Build the Frontend Docker Image:
$ docker build -t <Username>/qr-frontend:v1 .
This command builds a Docker image for the Next.js frontend of the QR code application.
The
t <Username>/qr-frontend:v1
option tags the image with a name (qr-frontend
) and version (v1
).As with the API, the
.
specifies that Docker should look for the Dockerfile in the current directory.Note: Replace
<Username>
with your Docker Hub username or preferred tag.
Run the Frontend Container:
$ docker run -p 3000:3000 <Username>/qr-frontend:v1
This command runs a container based on the
qr-frontend
image.
The
p 3000:3000
option maps port 3000 on your machine to port 3000 in the container, making the frontend accessible atlocalhost:3000
.After running this command, your Next.js frontend should be available at
http://localhost:3000
Step 4: Access Your Application
With both containers running, you can now access the complete application in your web browser:
Frontend (QR Code Generator): Visit
http://localhost:3000
to access the Next.js frontend. This interface allows you to enter URLs and generate QR codes.
If you're running Docker on a remote server, replace localhost
with your server's IP address (e.g., http://<server_ip_address>:3000
for the frontend.
With the QR code application, you should see a page like this:
Now, let’s generate a QR code from a URL.
It works fine just like before! Give it a try yourself.
Wrapping Up!
In this guide, we focused on the process of containerization, successfully building and running the images for our QR Code Generator application on our local machine. This foundational work allows us to ensure the application operates seamlessly, setting the stage for efficient deployment and management.
In the next guide, we will explore how to create a CI/CD pipeline to automate the process of building and pushing our Docker image to Docker Hub using GitHub Actions. This automation will streamline our workflow, making it easier to deploy updates and enhancements to the application.
Stay tuned as we take the next step in our DevOps journey!