Skip to content

Commit

Permalink
95 110 case studies (#111)
Browse files Browse the repository at this point in the history
* 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
UmairJibran authored Oct 20, 2024
1 parent 5eca82c commit 22678e7
Show file tree
Hide file tree
Showing 15 changed files with 460 additions and 60 deletions.
233 changes: 233 additions & 0 deletions _blogs/docker-nginx-reverse-proxy-local-dev.md
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.
8 changes: 4 additions & 4 deletions src/app/(blogs)/blogs/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Metadata } from "next";
import { notFound } from "next/navigation";
import { getAllBlog, getBlogBySlug } from "@/lib/api";
import markdownToHtml from "@/lib/markdownToHtml";
import { BlogBody } from "@/components/BlogBody";
import { BlogHeader } from "@/components/BlogHeader";
import { StoryBody } from "@/components/StoryBody";
import { StoryHeader } from "@/components/StoryHeader";

import profile from "@/data/profile.json";

Expand All @@ -19,13 +19,13 @@ export default async function Blog({ params }: Params) {
return (
<main>
<article className="container mx-auto">
<BlogHeader
<StoryHeader
title={blog.title}
coverImage={blog.coverImage}
date={blog.date}
author={blog.author}
/>
<BlogBody content={content} />
<StoryBody content={content} />
</article>
</main>
);
Expand Down
21 changes: 15 additions & 6 deletions src/app/(blogs)/blogs/page.tsx
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>
);
}
70 changes: 70 additions & 0 deletions src/app/(caseStudies)/case-studies/[slug]/page.tsx
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,
}));
}
Loading

0 comments on commit 22678e7

Please sign in to comment.