One of the importance of NGINX is its use as a reverse proxy between user requests and a web server. To explain what a reverse proxy is, think of it like a traffic warden on a busy road. Since we are referring to web requests, a reverse proxy acts as a traffic warden, receiving user requests and sending them to the appropriate destination based on the URL. This process eventually would help to reduce request congestions, simplify routing, and enhance scalability.
This tutorial guides you through creating a multi-container application with Nginx acting as a reverse proxy for the microservices. We’ll use Docker Compose to manage the containers and simplify deployment locally.
Prerequisites:
- Docker: Download and install Docker Desktop from Docker’s official website.
- Docker Compose: Install Docker Compose following the instructions on Docker’s documentation.
- Basic understanding of Python: The assumption is that you are mildly familiar with python and concepts like APIs for creating a simple backend service.
Project Steps:
Step 1. Building a Microservices-Based Task Management Application
In this tutorial, we’ll create two simple backend services for a Task Management Application:
- User Service: Handles user login and authentication.
- Task Service: Manages task creation, modification, and retrieval.
The code for this project can be found on my Github repo.
We’ll start by creating a folder and two sub-folders in the terminal:
mkdir nginxxdocker
mkdir nginxxdocker/user_service
mkdir nginxxdocker/task_service
Next, activate a virtual environment and install Flask for each service:
pip install Flask
User Service
Create an app.py file inside the user_service
directory with the following code:
from flask import Flask, request, jsonify
from functools import wraps
app = Flask(__name__)
if __name__ == '__main__':
app.run(debug=True)
users = [
{"username": "user1", "password": "password1", "email": "[email protected]", "role": "user"},
{"username": "user2", "password": "password2", "email": "[email protected]", "role": "admin"}
]
# defining route
@app.route('/login', methods=['POST'])
def login():
auth = request.authorization
if not auth or not auth.username or not auth.password:
return jsonify({"error": "Authentication failed"}), 401
user = next((user for user in users if user['username'] == auth.username), None)
if not user or user['password'] != auth.password:
return jsonify({"error": "Authentication failed"}), 401
return jsonify({"message": "Login successful", "user": user}), 200
# Authorization for Admin Dashboard
def requires_role(role):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
auth = request.authorization
if not auth or not auth.username:
return jsonify({"error": "Unauthorized"}), 401
user = next((user for user in users if user['username'] == auth.username), None)
if not user or user['role'] != role:
return jsonify({"error": "Unauthorized"}), 401
return func(*args, **kwargs)
return wrapper
return decorator
@app.route('/admin/dashboard')
@requires_role('admin')
def admin_dashboard():
return jsonify({"message": "Welcome to admin dashboard"}), 200
In this code, we have implemented two endpoint URLs for user authentication - /login and /admin/dashboard. Now let’s go ahead to create the task service.
Task Service
Similarly, create an app.py
file inside the task_service
directory with the following code:
from flask import Flask, request, jsonify
from datetime import datetime
app = Flask(__name__)
if __name__ == '__main__':
app.run(debug='True')
class Task:
def __init__(self, id, title, description, due_date, assignee):
self.id = id
self.title = title
self.description = description
self.due_date = due_date
self.assignee = assignee
self.created_at = datetime.now()
tasks = []
# Defining routes for CRUD tasks
@app.route('/tasks', methods=['GET'])
def get_tasks():
return jsonify([task.__dict__ for task in tasks])
@app.route('/tasks/<int:task_id>', methods=['GET'])
def get_task(task_id):
task = next((task for task in tasks if task.id == task_id), None)
if task:
return jsonify(task.__dict__)
else:
return jsonify({"error": "Task not found"}), 404
@app.route('/tasks', methods=['POST'])
def create_task():
data = request.json
task = Task(id=len(tasks) + 1, title=data['title'], description=data['description'], due_date=data['due_date'], assignee=data['assignee'])
tasks.append(task)
return jsonify(task.__dict__), 201
@app.route('/tasks/<int:task_id>', methods=['PUT'])
def update_task(task_id):
data = request.json
task = next((task for task in tasks if task.id == task_id), None)
if task:
task.title = data.get('title', task.title)
task.description = data.get('description', task.description)
task.due_date = data.get('due_date', task.due_date)
task.assignee = data.get('assignee', task.assignee)
return jsonify(task.__dict__)
else:
return jsonify({"error": "Task not found"}), 404
@app.route('/tasks/<int:task_id>', methods=['DELETE'])
def delete_task(task_id):
global tasks
tasks = [task for task in tasks if task.id != task_id]
return '', 204
In the above code, we have implemented a service APIfor managing tasks, with /tasks as the URL endpoint for performing CRUD requests.
Step 2. Testing with Postman:
Now we need to make sure these services can accept requests on Postman.
- Test User Service: Run the user service with the flask run command locally, and then send a POST request with the username and password in the request’s Authorization header for /login and GET request for the /admin/dashboard route.
- Test Task Service: Similarly, test endpoints for creating, reading, updating, and deleting tasks in your task service can be done with the /tasks endpoint.
Step 3. Dockerizing the Services:
Now that the services are running and tested, let’s containerize them using Docker.
Dockerfile for User Service
Create a file named Dockerfile
inside the user_service
directory with the following content:
FROM python:3.10
# Set the working directory in the container to /app
WORKDIR /app
COPY . .
# Install Flask and other dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Run the command to start the Flask app
CMD ["flask", "run", "--host", "0.0.0.0"]
Dockerfile for Task Service
Similarly, create a file named Dockerfile
inside the task_service
directory with the following content:
FROM python:3.10
# Set the working directory in the container to /app
WORKDIR /app
COPY . .
# Install Flask and other dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Run the command to start the Flask app
CMD ["flask", "run", "--host", "0.0.0.0"]
Run the following commands in each service directory to build Docker images:
docker build -t user_service .
docker build -t task_service .
Step 4: Setting Up Nginx Reverse Proxy
Now that we have our Docker images ready, let’s set up Nginx as a reverse proxy to route requests to the appropriate services.
Inside the root directory, nginxxdocker
, create a file named nginx.conf
with the following code in it:
events{
worker_connections 1024;
}
http {
server {
listen 80;
server_name localhost 127.0.0.1;
location /login {
proxy_pass http://user_service:5000;
proxy_set_header X-Forwarded-For $remote_addr;
}
location /admin/dashboard {
proxy_pass http://user_service:5000;
proxy_set_header X-Forwarded-For $remote_addr;
}
location /tasks {
proxy_pass http://task_service:5000;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
}
It is important to note that I initially had a 502 Gateway error after running Docker Compose, (something we’ll do in the next step). What I had to do was to add the port 5000 to the each of the proxy_pass directives so NGINX can forward any requests made to that port. Without specifying the port, Nginx will attempt to use the default port (80), which is not the same port on which the containers running the services are listening on.
Step 5: Docker Compose Setup
Now that we have set up Nginx and configured it to act as a reverse proxy, let’s use Docker Compose to manage our containers.
First, pull the official Nginx Docker image from Docker Hub using the following command:
docker pull nginx
Next, create a file named docker-compose.yml
in the root directory of the project. This file defines the services to be run and their configurations.
version: '3.8'
services:
nginx_proxy:
image: nginx
container_name: nginx-proxy-reverse
depends_on:
- user_service
- task_service
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
ports:
- 80:80
links:
- user_service
- task_service
# Runs the user service
user_service:
image: user_service
container_name: user_service_container
# Runs the task service
task_service:
image: task_service
container_name: task_service_container
This Docker Compose
file defines three services: nginx, user_service, and task_service. It also has the Nginx image we pulled from Docker Hub, exposes port 80 for the Nginx container, and mounts the custom Nginx configuration file from the nginx directory. The depends_on directive ensures that the nginx service starts after the user_service
and task_service
are up and running.
Now that we have the services defined Docker Compose
file, we can now run the following command in the root directory of the project to start the containers:
docker-compose up -d
When we initially ran our services without Nginx, we could access them directly and locally on the 127.0.0.1:5000/login endpoint for the user service, for example.
Now when you start the containers and go on Docker Desktop, notice how the requests are now routed through Nginx before reaching the services. Nginx listens on port 80, the default HTTP port, and forwards the requests to the appropriate backend service based on the URL path.
Using the user service as an example, when you access localhost/login in the browser, the request first goes to Nginx. Nginx then looks at its configuration and forwards the request to the user service running on port 5000 internally.
In conclusion, we created two microservices built on Flask for a task management application. We then leveraged Docker containers and Nginx as a reverse proxy to direct requests to the services based on their endpoint URLS.