-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add APIs for case studies * dynamise blog to story * update blogs to be more generic update the existing components and types to be more generic so they can be reused by case studies * introduce case studies * ✨ feat: Use generalized type in API * 🐛 fix: pages for no blogs and case studies * 📜 BLOG: Docker+Nginx
- Loading branch information
1 parent
5eca82c
commit 22678e7
Showing
15 changed files
with
460 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
--- | ||
title: "Mastering Local Backend Development: Docker & NGINX as a Reverse Proxy for Efficient Testing" | ||
excerpt: "Learn how to set up a local backend environment using Docker and NGINX as a reverse proxy. Streamline your development and testing with this step-by-step guide, optimized for efficient scaling and traffic management." | ||
coverImage: "/assets/blogs/docker-nginx-reverse-proxy-local-dev/cover.webp" | ||
date: "2024-10-20T18:13:00.000Z" | ||
author: | ||
name: Umair Jibran | ||
picture: "/assets/authors/jibran.webp" | ||
ogImage: | ||
url: "/assets/blogs/docker-nginx-reverse-proxy-local-dev/cover.webp" | ||
tags: | ||
- Docker | ||
- NGINX | ||
- Backend Development | ||
- Reverse Proxy | ||
--- | ||
|
||
When developing distributed applications that rely on multiple services, it's crucial to replicate a production-like environment locally. This setup helps you catch bugs early, optimize performance, and reduce the risk of costly errors during deployment. Docker and NGINX are powerful tools for this purpose: Docker containerizes services, while NGINX acts as a reverse proxy to manage traffic and expose a unified interface via a single port. | ||
|
||
In this guide, we’ll explore how to: | ||
|
||
1. Set up a backend environment locally using Docker. | ||
2. Configure NGINX as a reverse proxy to efficiently route traffic between services. | ||
3. Expose all services on a single port for streamlined access. | ||
|
||
Whether you're a backend developer managing multiple microservices or someone scaling containerized applications, this tutorial will enhance your local development environment. | ||
|
||
## Step 1: Setting Up the Docker Environment | ||
|
||
We’ll start by simulating a local backend environment using Docker. Docker offers a streamlined way to containerize services, making it easier to replicate complex production environments on a local machine. Our goal is to run a backend service on port 4000, with requests being routed through NGINX. | ||
|
||
### Prerequisites: | ||
|
||
- **Docker** installed on your system (Windows, macOS, or Linux). | ||
- Basic understanding of Docker commands and containerization. | ||
|
||
### Steps: | ||
|
||
1. **Create a Dockerfile** for your backend service. If you are using a **Node.js** backend, your `Dockerfile` might look something like this: | ||
|
||
```dockerfile | ||
# Use the official Node.js image | ||
FROM node:22-alpine | ||
|
||
# Create and set the working directory inside the container | ||
WORKDIR /app | ||
|
||
# Copy package files and install dependencies | ||
COPY package*.json ./ | ||
RUN npm install | ||
|
||
# Copy the rest of the application code | ||
COPY . . | ||
|
||
# Expose the port the app runs on | ||
EXPOSE 4000 | ||
|
||
# Command to start the backend service | ||
CMD ["npm", "start"] | ||
``` | ||
|
||
2. **Build the Docker image.** In the directory containing your Dockerfile, run the following command: | ||
|
||
```bash | ||
docker build -t backend-service . | ||
``` | ||
|
||
3. **Run the container** with this command: | ||
|
||
```bash | ||
docker run -d -p 4000:4000 backend-service | ||
``` | ||
|
||
This command runs your backend service inside a Docker container, mapping **port 4000** on the host machine to **port 4000** inside the container. You can test the service by visiting `http://localhost:4000` in your browser. | ||
|
||
To stop a running container, use: | ||
|
||
```bash | ||
docker stop <container_id> | ||
``` | ||
|
||
Replace `<container_id>` with the actual ID of the running container, which you can find using: | ||
|
||
```bash | ||
docker ps | ||
``` | ||
|
||
While you now have a working backend service, we’re aiming for a more complex and realistic setup. Next, we’ll introduce **NGINX** to manage traffic between multiple services using **Docker Compose**. | ||
|
||
## Step 2: Configuring Docker Compose for Service Orchestration | ||
|
||
Instead of manually managing individual containers, we’ll use **Docker Compose** to define and orchestrate multiple services, including our backend and NGINX. | ||
|
||
### Why Use Docker Compose? | ||
|
||
- **Multi-Service Management**: Easily manage multiple services in a single configuration file. | ||
- **Scaling**: Define replicas of services for load balancing. | ||
- **Portability**: Simplifies the process of spinning up environments locally and in production. | ||
|
||
### Docker Compose Configuration | ||
|
||
Here’s a basic `docker-compose.yml` file to define our backend service with **3 replicas**: | ||
|
||
```yaml | ||
# docker-compose.yml | ||
name: find-a-cat-backend | ||
|
||
services: | ||
app: | ||
build: . | ||
command: node server | ||
ports: | ||
- "4000" # Expose the app on port 4000 but don't bind it to the host | ||
# Load environment variables from a file | ||
env_file: | ||
- .env | ||
# Define the number of replicas and resource limits | ||
deploy: | ||
mode: replicated | ||
replicas: 3 | ||
resources: | ||
limits: | ||
cpus: "2" | ||
memory: "4096M" | ||
``` | ||
- **Replicas**: Defines 3 instances of the backend service. | ||
- **Resource Limits**: Limits CPU usage to 2 cores and memory to 4 GB per container. | ||
- **Environment Variables**: Use an `.env` file to manage your backend configuration. | ||
|
||
Once your `docker-compose.yml` file is ready, run the services with: | ||
|
||
```bash | ||
docker-compose up --build | ||
``` | ||
|
||
This command will build and run all services defined in your `docker-compose.yml` file. Now that we have a scalable backend service, let’s move on to setting up **NGINX** as a reverse proxy. | ||
|
||
## Step 3: Configuring NGINX as a Reverse Proxy | ||
|
||
With your backend running in Docker, the next step is to use **NGINX** as a reverse proxy. The reverse proxy directs incoming requests to the correct service, while exposing only one port, simplifying access. | ||
|
||
### Why Use NGINX as a Reverse Proxy? | ||
|
||
- **Unified Access**: Expose multiple services through a single port. | ||
- **Security**: Hide service details from the outside world. | ||
- **Performance**: NGINX offers load balancing and caching, improving overall performance. | ||
|
||
### NGINX Configuration | ||
|
||
1. **Create an NGINX Configuration File**: | ||
Create a file named `nginx.conf` in your project directory with the following configuration: | ||
|
||
```nginx | ||
events {} | ||
http { | ||
client_max_body_size 100M; # Set the maximum request size | ||
upstream backend { | ||
server app:4000; # Define the backend service running on port 4000 | ||
} | ||
server { | ||
listen 80; | ||
location / { | ||
proxy_pass http://backend; | ||
proxy_set_header Host $host; | ||
proxy_set_header X-Real-IP $remote_addr; | ||
} | ||
} | ||
} | ||
``` | ||
|
||
2. **Update the `docker-compose.yml`**: | ||
|
||
Add an NGINX service to your `docker-compose.yml` file: | ||
|
||
```yml | ||
nginx: | ||
container_name: nginx | ||
image: nginx:alpine | ||
ports: | ||
- "80:80" | ||
volumes: | ||
- ./nginx.conf:/etc/nginx/nginx.conf | ||
depends_on: | ||
- app | ||
``` | ||
|
||
This configuration sets up an NGINX service that listens on **port 80** and forwards all requests to the backend service running on **port 4000**. The `depends_on` directive ensures that NGINX waits for the backend to be ready. | ||
|
||
3. **Rebuild and Run**: | ||
|
||
Now, run both services together using: | ||
|
||
```bash | ||
docker-compose up --build | ||
``` | ||
|
||
At this stage, NGINX is set up as a reverse proxy, routing incoming traffic on **port 80** to any of the 3 backend replicas running on **port 4000**. | ||
|
||
At this point, NGINX is set up to act as a reverse proxy, forwarding all incoming traffic on **port 80** to any of the 3 services running backend on **port 4000**. | ||
|
||
## Step 4: Testing and Troubleshooting | ||
|
||
One of the advantages of using NGINX is that it allows you to route multiple services through a single port, simplifying the interface for external clients or other services. Here, NGINX handles all the traffic management, which makes scaling the architecture easy. | ||
|
||
### Testing the Setup | ||
|
||
With both the backend and NGINX running, test the setup by navigating to `http://localhost` in your browser. You can also test it using **curl**: | ||
|
||
```bash | ||
curl http://localhost | ||
``` | ||
|
||
This request should return the response from one of the backend services. Since NGINX is acting as a reverse proxy, it will forward traffic to any available replica. | ||
|
||
### Common Issues and Solutions | ||
|
||
- **502 Bad Gateway**: If NGINX returns a 502 error, it may be unable to connect to the backend service. Ensure both NGINX and the backend service are running and can communicate on the same Docker network. | ||
- **Port Conflicts**: If port 80 is already in use, modify the `docker-compose.yml` file to expose NGINX on a different port (e.g., `8080:80`). | ||
- **Container Logs**: Use the following command to inspect logs and identify issues: | ||
|
||
```bash | ||
docker-compose logs | ||
``` | ||
|
||
## Conclusion | ||
|
||
By combining Docker and NGINX, you can efficiently replicate a complex backend environment for local development. Docker Compose simplifies managing multi-service setups, while NGINX handles traffic routing and load balancing across services. This architecture not only mirrors production environments but also streamlines testing and scaling. | ||
|
||
Whether you’re developing microservices or handling a monolithic backend, this approach provides flexibility and scalability. By following these steps, you can create a local environment that closely resembles your production setup, improving both your workflow and your deployment pipeline. |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,32 @@ | ||
import { HeroBlog } from "@/components/HeroBlog"; | ||
import { HeroStory } from "@/components/HeroStory"; | ||
import { MoreStories } from "@/components/MoreStories"; | ||
import { getAllBlog } from "@/lib/api"; | ||
|
||
export default function Index() { | ||
const allBlog = getAllBlog(); | ||
const heroBlog = allBlog[0]; | ||
const moreBlog = allBlog.slice(1); | ||
const allBlogs = getAllBlog(); | ||
const heroBlog = allBlogs[0]; | ||
const moreBlogs = allBlogs.slice(1); | ||
|
||
if (allBlogs.length === 0) { | ||
return ( | ||
<main className="container mx-auto flex justify-center"> | ||
<div>oops... I should write some up</div> | ||
</main> | ||
); | ||
} | ||
|
||
return ( | ||
<main className="container mx-auto"> | ||
<HeroBlog | ||
<HeroStory | ||
title={heroBlog.title} | ||
coverImage={heroBlog.coverImage} | ||
date={heroBlog.date} | ||
author={heroBlog.author} | ||
slug={heroBlog.slug} | ||
excerpt={heroBlog.excerpt} | ||
type={heroBlog.type} | ||
/> | ||
{moreBlog.length > 0 && <MoreStories blogs={moreBlog} />} | ||
{moreBlogs.length > 0 && <MoreStories stories={moreBlogs} />} | ||
</main> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { Metadata } from "next"; | ||
import { notFound } from "next/navigation"; | ||
import { getAllCaseStudies, getCaseStudyBySlug } from "@/lib/api"; | ||
import markdownToHtml from "@/lib/markdownToHtml"; | ||
import { StoryBody } from "@/components/StoryBody"; | ||
import { StoryHeader } from "@/components/StoryHeader"; | ||
|
||
import profile from "@/data/profile.json"; | ||
|
||
export default async function CaseStudy({ params }: Params) { | ||
const caseStudy = getCaseStudyBySlug(params.slug); | ||
|
||
if (!caseStudy) { | ||
return notFound(); | ||
} | ||
|
||
const content = await markdownToHtml(caseStudy.content || ""); | ||
|
||
return ( | ||
<main> | ||
<article className="container mx-auto"> | ||
<StoryHeader | ||
title={caseStudy.title} | ||
coverImage={caseStudy.coverImage} | ||
date={caseStudy.date} | ||
author={caseStudy.author} | ||
/> | ||
<StoryBody content={content} /> | ||
</article> | ||
</main> | ||
); | ||
} | ||
|
||
type Params = { | ||
params: { | ||
slug: string; | ||
}; | ||
}; | ||
|
||
export function generateMetadata({ params }: Params): Metadata { | ||
const CaseStudy = getCaseStudyBySlug(params.slug); | ||
|
||
if (!CaseStudy) { | ||
return notFound(); | ||
} | ||
|
||
const title = `${CaseStudy.title} | Case Study | ${profile.name.firstName} ${profile.name.lastName}`; | ||
|
||
return { | ||
title, | ||
openGraph: { | ||
title, | ||
images: [CaseStudy.ogImage.url], | ||
}, | ||
twitter: { | ||
card: "summary_large_image", | ||
}, | ||
description: CaseStudy.excerpt, | ||
authors: [CaseStudy.author], | ||
keywords: CaseStudy.tags, | ||
}; | ||
} | ||
|
||
export async function generateStaticParams() { | ||
const caseStudies = getAllCaseStudies(); | ||
|
||
return caseStudies.map((caseStudy) => ({ | ||
slug: caseStudy.slug, | ||
})); | ||
} |
Oops, something went wrong.