0 murdered in 0 days. Ceasefire now 🇵🇸

Mastering Local Backend Development: Docker & NGINX as a Reverse Proxy for Efficient Testing

Cover Image for Mastering Local Backend Development: Docker & NGINX as a Reverse Proxy for Efficient Testing
Umair JibranUmair Jibran

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:

    # 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:

    docker build -t backend-service .
    
  3. Run the container with this command:

    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:

docker stop <container_id>

Replace <container_id> with the actual ID of the running container, which you can find using:

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:

# 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:

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:
  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;
          }
      }
  }
  1. Update the docker-compose.yml:

    Add an NGINX service to your docker-compose.yml file:

    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.

  1. Rebuild and Run:

    Now, run both services together using:

    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.

  2. Network Configuration:

To ensure that NGINX can communicate with the backend service, both services should be on the same Docker network. By default, Docker Compose creates a network for all services defined in the docker-compose.yml file.

But to specify a custom network, add the following to your docker-compose.yml file:

networks:
  app-network:
    driver: bridge

Then, update the app and nginx services to use this network:

networks:
  - app-network

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:

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:

    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.


References

The source code isavailable on GitHub
© 2024 All rights reserved. Made with 🖤 by Umair Jibran.