Modern applications often start as a single monolithic codebase. As they grow, teams break them into smaller microservices to improve maintainability, scalability, and deployment agility. However, microservices introduce new challenges:

Enter Kong, an open-source API gateway that sits in front of your services to:

  1. Unify Endpoints: Expose a single public URL (e.g. api.company.com) and route internally to the correct microservice.
  2. Centralize Security & Policies: Apply authentication, rate-limiting, CORS, and logging in one place.
  3. Enable Declarative Management: Define your services and routes in YAML, version them in Git, and push updates without writing custom proxy code.

Background & Motivation

Our application began as a monolith, one codebase handling authentication, user profiles, memberships, and chat features. As traffic grew and teams expanded, we decided to split the monolith into four independent microservices:

While microservices unlocked faster deployments and team autonomy, we ran into:

  1. Client Confusion: Frontend teams had to remember four different base URLs.
  2. Certificate Sprawl: Managing SSL certificates for each service became a DevOps burden.
  3. Security Drift: Applying consistent rate-limits, auth checks, and CORS settings across services was error-prone.

Kong solved these pain points by providing:

In this guide, we'll deploy Kong as an API gateway using Docker Compose, then define our services and routes declaratively with Deck. By the end, you'll have a reusable, containerized API-proxy setup that can route requests to your microservices with zero-downtime updates.

The Kong setup given here is a basic setup without any plugins. It acts as a proxy only. You can extend it in any way you need.

1. Docker Compose Configuration

Create a file named docker-compose.yml at the root of your project:

version: "3.8"
services:
  api-gateway-database:
    image: postgres:13
    container_name: api-gateway-database
    restart: always
    environment:
      POSTGRES_USER: ${API_GW_PG_USER}
      POSTGRES_DB: api_gateway_db
      POSTGRES_PASSWORD: ${API_GW_PG_PASSWORD}
    networks:
      - api-network
    healthcheck:
      test: [ "CMD-SHELL", "pg_isready -U ${API_GW_PG_USER} -d api_gateway_db" ]
      interval: 5s
      timeout: 5s
      retries: 5

  api-gateway-migrations:
    image: kong:latest
    container_name: api-gateway-migrations
    depends_on:
      api-gateway-database:
        condition: service_healthy
    environment:
      KONG_DATABASE: postgres
      KONG_PG_HOST: ${API_GW_PG_HOST}
      KONG_PG_USER: ${API_GW_PG_USER}
      KONG_PG_PASSWORD: ${API_GW_PG_PASSWORD}
    command: kong migrations bootstrap
    networks:
      - api-network
    restart: no

  api-gateway:
    build:
      context: .
      dockerfile: Dockerfile.kong
    container_name: api-gateway
    restart: always
    depends_on:
      api-gateway-database:
        condition: service_healthy
      api-gateway-migrations:
        condition: service_completed_successfully
    environment:
      - KONG_DATABASE=${API_GW_DATABASE}
      - KONG_PG_HOST=${API_GW_PG_HOST}
      - KONG_PG_USER=${API_GW_PG_USER}
      - KONG_PG_PASSWORD=${API_GW_PG_PASSWORD}
      - KONG_DB_CACHE_REDIS_PASSWORD=${CACHE_PASSWORD}
      - KONG_PROXY_ACCESS_LOG=/dev/stdout
      - KONG_ADMIN_ACCESS_LOG=/dev/stdout
      - KONG_PROXY_ERROR_LOG=/dev/stderr
      - KONG_ADMIN_ERROR_LOG=/dev/stderr
      - KONG_PROXY_LISTEN=0.0.0.0:8000
      - KONG_ADMIN_LISTEN=0.0.0.0:8001
      - KONG_GUI_LISTEN=0.0.0.0:8002
      - KONG_ADMIN_GUI_URL=http://api-gateway-gui:8002
      - KONG_ADMIN_GUI_SESSION_CONF={"secret":"YourSecretHere"}
      - KONG_PORT_MAPS=8100:8000,8101:8001,8102:8002,3101:3000
    ports:
      - "8000:8000"
      - "8443:8443"
    networks:
      - api-network
    healthcheck:
      test: [ "CMD", "curl", "-f", "http://api-gateway:8001" ]
      interval: 5s
      timeout: 5s
      retries: 5

  auth-backend-service:
    build:
      context: ./auth.backend.service
      dockerfile: Dockerfile.local
    image: auth-backend-001
    env_file:
      - ${AUTH_ENV_FILE}
    container_name: auth-backend-service
    environment:
      - NODE_ENV=development
      - PORT=3000
    networks:
      - api-network

  users-backend-service:
    build:
      context: ./users.backend.service
      dockerfile: Dockerfile.local
    image: users-backend-001
    env_file:
      - ${USERS_ENV_FILE}
    container_name: users-backend-service
    environment:
      - NODE_ENV=development
      - PORT=3000
    networks:
      - api-network

  members-backend-service:
    build:
      context: ./members.backend.service
    container_name: members-backend-service
    environment:
      - NODE_ENV=development
      - PORT=3000
    networks:
      - api-network

  chats-backend-service:
    build:
      context: ./chats.backend.service
    container_name: chats-backend-service
    environment:
      - NODE_ENV=development
      - PORT=3000
    networks:
      - api-network

  redis:
    image: redis:latest
    container_name: redis
    restart: always
    command: redis-server --requirepass ${CACHE_PASSWORD}
    ports:
      - "6379:6379"
    networks:
      - api-network

  api-gateway-configurator:
    image: kong/deck:latest
    container_name: api-gateway-configurator
    depends_on:
      api-gateway:
        condition: service_healthy
      api-gateway-database:
        condition: service_healthy
      api-gateway-migrations:
        condition: service_started
      redis:
        condition: service_started
      auth-backend-service:
        condition: service_started
      users-backend-service:
        condition: service_started
      members-backend-service:
        condition: service_started
      chats-backend-service:
        condition: service_started
    volumes:
      - ./kong-config/kong-declarative-config.yml:/config/kong-declarative-config.yml:ro
    command: gateway sync /config/kong-declarative-config.yml --kong-addr=http://api-gateway:8001
    networks:
      - api-network
    restart: always

  api-gateway-gui-proxy:
    image: nginx:latest
    container_name: api-gateway-gui-proxy
    restart: always
    ports:
      - "8001:8001"
      - "8002:8002"
    volumes:
      - ./nginx/api-gateway-proxy.conf:/etc/nginx/conf.d/default.conf
      - ./nginx/.htpasswd:/etc/nginx/.htpasswd
    depends_on:
      - api-gateway
    networks:
      - api-network

networks:
  api-network:
    driver: bridge

2. Declarative Configuration

Place this file under kong-config/kong-declarative-config.yml. Deck will read it and create/update your Kong services and routes automatically.

_format_version: "3.0"
services:
  - name: auth-backend
    url: http://auth-backend-service:3000
    routes:
      - name: auth-route
        paths:
          - /api/v1/auth
        strip_path: false
        preserve_host: false
      - name: auth-swagger-route
        paths:
          - /api/v1/auth/docs
        strip_path: false
        preserve_host: false

  - name: users-backend
    url: http://users-backend-service:3000
    routes:
      - name: users-route
        paths:
          - /api/v1/users
        strip_path: false
        preserve_host: false
      - name: users-swagger-route
        paths:
          - /api/v1/users/docs
        strip_path: false
        preserve_host: false

  - name: members-backend
    url: http://members-backend-service:3000
    routes:
      - name: members-route
        paths:
          - /api/v1/members
        strip_path: false
        preserve_host: false
      - name: members-swagger-route
        paths:
          - /api/v1/members/docs
        strip_path: false
        preserve_host: false

  - name: chats-backend
    url: http://chats-backend-service:3000
    routes:
      - name: chats-route
        paths:
          - /api/v1/chats
        strip_path: false
        preserve_host: false
      - name: chats-swagger-route
        paths:
          - /api/v1/chats/docs
        strip_path: false
        preserve_host: false

3. How It All Fits Together

Orchestration with Docker Compose:

Declarative API Definitions:

Configuration Sync via Deck:

The api-gateway-configurator container mounts your YAML and runs:

deck gateway sync /config/kong-declarative-config.yml --kong-addr=http://api-gateway:8001

Any changes to kong-declarative-config.yml are applied on restart, giving you declarative, version-controlled API definitions.

Secure Admin & GUI Access:

To run the docker compose use the following commands:

  1. Run the following command where the docker-compose.yml file is, in the terminal:
docker-compose up -d
  1. If there are changes in the microservices code and you want to build the Docker images again, use:
docker-compose up -d --build

Conclusion

By combining Docker Compose and Kong's declarative configuration, you get:

Notes: Kong DB-less Mode for Testing

What is it?

Kong's DB-less (or "off-db") mode allows you to run Kong purely from a declarative config file, no Postgres required. Ideal for quick local tests or CI pipelines.

Key Environment Variables

KONG_DATABASE=off
KONG_DECLARATIVE_CONFIG=/path/to/kong-declarative-config.yml
KONG_PROXY_LISTEN=0.0.0.0:8000
KONG_ADMIN_LISTEN=0.0.0.0:8001

Example Docker Compose Service

kong:
  image: kong:latest
  environment:
    - KONG_DATABASE=off
    - KONG_DECLARATIVE_CONFIG=/config/kong-declarative-config.yml
    - KONG_PROXY_LISTEN=0.0.0.0:8000
    - KONG_ADMIN_LISTEN=0.0.0.0:8001
  volumes:
    - ./kong-config/kong-declarative-config.yml:/config/kong-declarative-config.yml:ro
  ports:
    - "8000:8000"
    - "8001:8001"

Workflow

  1. Edit your declarative YAML.
  2. docker-compose up kong will spin up Kong with routes/services defined in the YAML.
  3. No migrations or database container needed, Kong reads directly from the file.

Bonus Tip: GitHub Workflow to Deploy Kong to Google Cloud Run

name: Production - Deploy Kong Service

on:
  push:
    branches:
      - dev
  workflow_dispatch:

env:
  ARTIFACTORY_PATH: kong-gateway
  IMAGE_NAME: kong-gateway-${{ github.run_number }}
  IMAGE_URL: asia-south1-docker.pkg.dev/kovaad/kong-gateway/kong-gateway-${{ github.run_number }}:latest

jobs:
  build_tag_push_deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1

      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v2
        with:
          credentials_json: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}

      - name: Check Google Cloud authentication
        run: |
          gcloud auth list

      - name: Configure Docker to use the gcloud command-line tool as a credential helper
        run: |
          gcloud auth configure-docker asia-south1-docker.pkg.dev

      - name: Build Docker image
        run: |
          docker build -t ${{ env.IMAGE_NAME }} -f kong/Dockerfile.prod .

      - name: Tag Docker image
        run: |
          docker tag ${{ env.IMAGE_NAME }} ${{ env.IMAGE_URL }}

      - name: Push Docker image
        run: |
          docker push ${{ env.IMAGE_URL }}

      - name: Deploy Kong Production to Google Cloud Run
        run: |
          gcloud run deploy ${{ env.ARTIFACTORY_PATH }} \
          --image ${{ env.IMAGE_URL }} \
          --platform managed \
          --region asia-south1 \
          --allow-unauthenticated \
          --cpu=4 \
          --memory=8Gi \
          --set-env-vars KONG_PROXY_LISTEN=0.0.0.0:8080,KONG_ADMIN_LISTEN=off

This workflow automates your CI/CD pipeline: it builds and pushes a versioned Docker image of Kong, then deploys it to Google Cloud Run with the specified resources and runtime configuration. Feel free to change it according to your environment.

Note: Google Cloud Run only allows one port (8080) to be exposed, hence we set the Kong proxy port to 8080 in the Docker image and workflow.

FROM kong:latest

USER root
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

USER kong

COPY kong/kong-config/prod/kong-declarative-config.yml /config/kong-declarative-config.yml

ENV KONG_DATABASE=off \
    KONG_PROXY_ACCESS_LOG=/dev/stdout \
    KONG_PROXY_ERROR_LOG=/dev/stderr \
    KONG_PROXY_LISTEN=0.0.0.0:8080 \
    KONG_DECLARATIVE_CONFIG=/config/kong-declarative-config.yml

EXPOSE 8080

ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["kong", "docker-start"]

Feel free to adapt container names, port mappings, and route definitions to match your own architecture. Happy proxying!

Continue the Discussion

If you are designing an API gateway layer and want a practical review of routing, auth, observability, and deployment tradeoffs, book a CTO consultation.

You can also connect with me on LinkedIn for a deeper architecture discussion.