Using Traefik proxy on Render.com

Michał DydeckiMichał Dydecki
6 min read

Intro

Render.com is an excellent service for quick and DevOps-less deployment. The basic manuals show how to deploy many types of applications, including the most popular frameworks and dockerized custom apps. However, there is no manual for deploying any apps where we often need to hide our services behind the API gateway. This article demonstrates how to deploy simple applications on Render.com, using Traefik as an API gateway and simple Python FastAPI apps as services. Interested? Let's dive in!

Pre-requisites

  • Basic knowledge of Python and FastAPI

  • Basic knowledge of Docker

  • Render.com account

  • 40 to 60 minutes of your time

Basic assumptions

Let's summarize the basic assumptions and architecture of the application we want to create: We want to deploy two fastapi apps (service1 and service2) hidden behind the Traefik API gateway (both will use private networks). Each service will expose only one GET hello endpoint, with the name of the service and render.com instance ID (that's all we need for testing purposes) service1 and service2 are independent of each other The paths exposed by the Traefik API gateway are:

  • /app1/hello -> GET /hello app1

  • /app2/hello -> GET /hello app2

no extra queue(s)/DBs or any other types of communication at this point is required—we want to keep it as simple as possible.

The described infrastructure should look like this

As a gateway for this particular case, as I mentioned before, we want to use Traefik proxy. In simple terms, Traefik is like a traffic manager for web applications. It acts as an API proxy, meaning it helps route and control the data flow between different parts of a web application (in our case only routing based on the URL prefixed to service1 or service2). More information can be found on the official docs here. To do that, we need a few steps:

  • create the dynamic configuration file (like defining the entry points, routers, services, etc. that our gateway will use)

  • create a custom docker image with our configuration (copy the configuration files into the image)

  • set up service1 and service2 (fastapi-based)

  • add all the configuration to the render.yaml file

  • deploy it to render.com

  • test the correctness of our solution by calling the HTTP get request to both services

Traefik config

To get the working traefik config, we have to define three things:

  • define and configure services

  • add routes to these services

  • add middleware to remove the API prefixes (otherwise our fastapi service will return 404 instead of the correct hello response since the URL address won't be correct)

Just to quickly clarify, by declaring a service in Traefik, we are specifying the target backend that will receive the incoming requests. In this case, we need to configure two services: one for service1 and another for service2. Middleware, on the other hand, is a processing unit that sits between the router and the service and allows us to modify or enhance the request/response flow. Some popular examples of traefik middlewares are:

In our example, we will use only one middleware, the stripPrefix one - we want to change the path from /app1/hello to just /hello seen by the backend service - the first part (/app<id>) is handled by the API gateway and services are not aware of it) Routers connect requests from the given entry points (like web) to services (by declaring the rule, like PathPrefix or Host for example), and define the middleware to be applied to the incoming requests before reaching the services. All of that should be clearer after reading the Traefik documentation and analyzing the example below. At the current state, our configuration file might look something like this:

http:
  routers:
    app1:
      rule: "PathPrefix(`/app1/`)"
      service: app1
      entryPoints:
        - web
      middlewares:
        - app1
    app2:
      rule: "PathPrefix(`/app2/`)"
      service: app2
      entryPoints:
        - web
      middlewares:
        - app2
  middlewares:
    app1:
      stripPrefix:
        prefixes:
          - "/app1"
    app2:
      stripPrefix:
        prefixes:
          - "/app2"
  services:
    app1:
      loadBalancer:
        servers:
          #        This might use http since we are in the internal network
          - url: "http://{{env "APP1_URL"}}:{{env "APP1_PORT"}}"
    app2:
      loadBalancer:
        servers:
          - url: "http://{{env "APP2_URL"}}:{{env "APP2_PORT"}}"

Backend services

As I mentioned, the backend service's main goal is to return the simple response with a unique ID—nothing more, nothing less, that should be used only for testing purposes if our routing works as expected. I put everything into one main.py file for the simplicity purposes of this demo:

from fastapi import FastAPI
from pydantic import BaseModel
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    render_service_name: str = "app1-local"
    render_instance_id: str | None = None


settings = Settings()
app = FastAPI()


class HelloResponse(BaseModel):
    message: str
    instance_id: str | None = None


@app.get('/hello')
def hello() -> HelloResponse:
    return HelloResponse(message=f"hello from {settings.render_service_name}", instance_id=settings.render_instance_id)

RENDER_SERVICE_ID is a unique identifier for each instance. We can use it to distinguish which instance is handling our request.

Traefik Dockerfile

Since the usage of the Docker images on Render.com is limited (e.g., we can't mount the volumes to provide the configuration), we have to create a custom image with the Traefik configuration inside. The Dockerfile might look like this:

FROM traefik:v3.0.4

COPY config/ /etc/traefik/

Under the ./config directory, we should put the config.yaml file with the configuration from the previous step.

Render.com config

To facilitate the deployment of our application, we should use a blueprint—the render.yaml file. This file allows us to specify the necessary resources, deployment type, server instance, and other relevant details. It is a simplified version of IaC (Infrastructure as Code).

services:
  - type: web
    name: traefik-demo-gateway
    runtime: docker
    dockerfilePath: ./docker/traefik/Dockerfile
    dockerContext: ./docker/traefik
    dockerCommand: traefik --entrypoints.web.address=:8091 --providers.file.directory=/etc/traefik/
    plan: starter
    region: frankfurt
    envVars:
      - key: APP1_URL
        fromService:
        type: pserv
        name: traefik-demo-app1
        property: host
      - key: APP1_PORT
        fromService:
        type: pserv
        name: traefik-demo-app1
        property: port
      - key: APP2_URL
        fromService:
        type: pserv
        name: traefik-demo-app2
        property: host
      - key: APP2_PORT
        fromService:
        type: pserv
        name: traefik-demo-app2
        property: port
      - key: PORT
        value: 8091

  - type: pserv
    name: traefik-demo-app1
    runtime: docker
    rootDir: ./app1
    plan: starter
    region: frankfurt
    envVars:
      - fromGroup: traefik-demo-group

  - type: pserv
    name: traefik-demo-app2
    runtime: docker
    rootDir: ./app2
    plan: starter
    numInstances: 1
    region: frankfurt
    envVars:
      - fromGroup: traefik-demo-group

envVarGroups:
  - name: traefik-demo-group
    envVars:
      - key: PORT
        value: 8001
      - key: APP_PORT
        value: 8001

For detailed information about the above config file options and syntax, please go to the doc specification https://docs.render.com/blueprint-spec.

Deployment and testing

As a final step, we'd like to deploy our gateway with two services and check that everything works as expected. In a nutshell, we have to push all the code to the GitHub repository, login to the render.com dashboard, go to the blueprints section, click the new blueprint instance button, and connect our repository - the rest should be done automatically - if not, I strongly advise you to read the render.com documentation and check the examples.

template sync init view

deployed services

The process of testing the changes is as follows: After deploying, you should be able to test it with a simple HTTP client, the only thing you have to know is the URL under which your public service has been deployed:

GitHub Repository

All the code can be found in the GitHub repository

Summary

The above example shows only the most basic approach to setting up this type of app using render.com. Is it simple? Yes. Is it working? Yes. Is it suitable for a production environment? Probably not. But if you want to create a fast PoC with this type of architecture, use the hidden features of Traefik (check the docs, there are a lot of them!), it might be one of the best ways to run it. Cheers!

0
Subscribe to my newsletter

Read articles from Michał Dydecki directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Michał Dydecki
Michał Dydecki