Using Traefik proxy on Render.com
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
filedeploy 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:
stripPrefix: Removes a prefix from the URL before routing it to the backend service.
Retry: Retries the request a certain number of times if it fails.
replacePath: Replaces a part of the URL path with a different value. Any many more (the detailed documentation is available here → https://doc.traefik.io/traefik/v3.0/middlewares/overview/ )
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.
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!
Subscribe to my newsletter
Read articles from Michał Dydecki directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by