Projecto completo: Delonix Temp
Do zero a um serviço na internet pública — build, run, e uma URL real, em 4 comandos. Tudo neste guia foi corrido a sério; o output é real, copiado da execução.
Uma API de tempo real em FastAPI — consulta a
temperatura ACTUAL de qualquer cidade (via Open-Meteo, sem
API key) e serve uma página que actualiza sozinha a cada 30s. O objectivo é percorrer o ciclo
completo do Delonix num só projecto pequeno: build multi-stage → container
run → expor à internet com tunnel. Os ficheiros completos estão em
examples/delonix-temp/
— git clone e segue os passos abaixo tal como estão.
1. A app
Três ficheiros. main.py — duas rotas: /api/weather/{city} (geocodifica
a cidade, pede a temperatura actual, devolve JSON) e / (uma página HTML que chama a
API própria via fetch e refaz a cada 30s):
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse
import httpx
app = FastAPI(title="Delonix Temp")
@app.get("/api/weather/{city}")
async def weather(city: str):
async with httpx.AsyncClient(timeout=8.0) as client:
geo = await client.get("https://geocoding-api.open-meteo.com/v1/search",
params={"name": city, "count": 1})
place = geo.json()["results"][0]
fc = await client.get("https://api.open-meteo.com/v1/forecast", params={
"latitude": place["latitude"], "longitude": place["longitude"],
"current": "temperature_2m,weather_code",
})
current = fc.json()["current"]
return {"city": place["name"], "country": place.get("country"),
"temperature_c": current["temperature_2m"], "observed_at": current["time"]}
@app.get("/", response_class=HTMLResponse)
async def index():
return PAGE # HTML com um <script> que faz fetch("/api/weather/"+city) a cada 30s
(versão completa, com a página HTML e /health, no ficheiro real do repo.)
requirements.txt — fastapi, uvicorn[standard],
httpx, com versões fixas.
Delonixfile — build multi-stage: um estágio instala as
dependências Python, o outro só copia o resultado + o código — a imagem final não carrega o
cache do pip nem ferramentas de build:
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /install /usr/local
COPY main.py .
ENV PYTHONUNBUFFERED=1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
2. Build
cd examples/delonix-temp
delonix build -t delonix-temp:1 .
Collecting fastapi==0.115.0 (from -r requirements.txt (line 1))
...
Successfully installed annotated-types-0.7.0 anyio-4.14.2 ... fastapi-0.115.0 ...
ef708d73f029O ID no fim (ef708d73f029) é a imagem. delonix image ls confirma o
tamanho — o estágio final, sem as ferramentas de build, fica bem mais pequeno que se fosse tudo
num único FROM:
delonix image ls
REPOSITORY:TAG IMAGE ID CREATED SIZE
delonix-temp:1 ef708d73f029 agora mesmo 157.2 MiB
python:3.12-slim 25c5b8011a34 agora mesmo 41.2 MiB3. Correr
delonix container run -d --name delonix-temp -p 8080:80 delonix-temp:1
curl -s http://localhost:8080/api/weather/Luanda
{"city":"Luanda","country":"Angola","temperature_c":20.2,"observed_at":"2026-07-23T19:00"}Temperatura REAL, consultada ao vivo — não é um valor fixo. Os logs do container confirmam os pedidos:
delonix container logs delonix-temp
INFO: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit)
INFO: 10.0.2.2:57786 - "GET /health HTTP/1.1" 200 OK
INFO: 10.0.2.2:57802 - "GET /api/weather/Luanda HTTP/1.1" 200 OK4. Expor à internet
Uma porta local não chega — o objectivo é uma URL que qualquer pessoa, em qualquer rede,
consiga abrir. É aqui que entra o kind: Gateway:
delonix net tunnel expose 8080 --name delonix-temp
tunnel/delonix-temp: running — https://lfdhz-197-148-40-67.free.pinggy.netEssa URL é REAL — foi a que este guião recebeu ao correr o comando. (A tua vai ser diferente de cada vez: o provider grátis atribui uma nova de cada sessão.) Confirmação, de fora, sem tocar em nada local:
curl https://lfdhz-197-148-40-67.free.pinggy.net/api/weather/Luanda
{"city":"Luanda","country":"Angola","temperature_c":20.2,"observed_at":"2026-07-23T19:00"}O mesmo JSON, desta vez a chegar de fora da máquina, por um tunnel SSH até um servidor público (pinggy) e de volta — zero configuração de router, zero IP público próprio, zero conta. Abrir a URL num browser mostra a página Delonix Temp a actualizar-se sozinha.
Ir mais longe: com mais de um serviço, mete o kind:
HTTPRoute à frente (routing por Host/path para vários containers) e aponta
o tunnel para a PORTA DO PROXY em vez de directamente ao container — uma só URL
pública, tantos backends quantos precisares. Ver examples/httproute.yaml +
examples/tunnel.yaml.
Arrumar
delonix delete gateway delonix-temp
delonix container rm -f delonix-temp
O que isto provou
| Comando | O que validou |
|---|---|
delonix build | build multi-stage real (2 estágios, COPY --from), com rede no build |
delonix container run -p | NAT userspace sem root, porta publicada no host |
delonix container logs | observabilidade de um serviço real a correr |
delonix net tunnel expose | tráfego REAL da internet pública a chegar a um container local, sem conta nem IP público |
Full project: Delonix Temp
From zero to a service on the public internet — build, run, and a real URL, in 4 commands. Everything in this guide was actually run; the output is real, copied from the run.
A real-time API in FastAPI — looks up the CURRENT
temperature of any city (via Open-Meteo, no API key) and
serves a page that refreshes itself every 30s. The goal is to walk the full Delonix cycle in one
small project: multi-stage build → container
run → expose to the internet with tunnel. The complete files are in
examples/delonix-temp/
— git clone it and follow the steps below exactly as they are.
1. The app
Three files. main.py — two routes: /api/weather/{city} (geocodes the
city, asks for the current temperature, returns JSON) and / (an HTML page that calls
the API itself via fetch and redoes it every 30s):
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse
import httpx
app = FastAPI(title="Delonix Temp")
@app.get("/api/weather/{city}")
async def weather(city: str):
async with httpx.AsyncClient(timeout=8.0) as client:
geo = await client.get("https://geocoding-api.open-meteo.com/v1/search",
params={"name": city, "count": 1})
place = geo.json()["results"][0]
fc = await client.get("https://api.open-meteo.com/v1/forecast", params={
"latitude": place["latitude"], "longitude": place["longitude"],
"current": "temperature_2m,weather_code",
})
current = fc.json()["current"]
return {"city": place["name"], "country": place.get("country"),
"temperature_c": current["temperature_2m"], "observed_at": current["time"]}
@app.get("/", response_class=HTMLResponse)
async def index():
return PAGE # HTML with a <script> that fetch("/api/weather/"+city) every 30s
(full version, with the HTML page and /health, in the repo's real file.)
requirements.txt — fastapi, uvicorn[standard],
httpx, with pinned versions.
Delonixfile — a multi-stage build: one stage installs the Python
dependencies, the other just copies the result plus the code — the final image carries no pip
cache and no build tools:
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /install /usr/local
COPY main.py .
ENV PYTHONUNBUFFERED=1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
2. Build
cd examples/delonix-temp
delonix build -t delonix-temp:1 .
Collecting fastapi==0.115.0 (from -r requirements.txt (line 1))
...
Successfully installed annotated-types-0.7.0 anyio-4.14.2 ... fastapi-0.115.0 ...
ef708d73f029The ID at the end (ef708d73f029) is the image. delonix image ls
confirms the size — the final stage, with no build tools, ends up much smaller than if everything
were in a single FROM:
delonix image ls
REPOSITORY:TAG IMAGE ID CREATED SIZE
delonix-temp:1 ef708d73f029 just now 157.2 MiB
python:3.12-slim 25c5b8011a34 just now 41.2 MiB3. Run it
delonix container run -d --name delonix-temp -p 8080:80 delonix-temp:1
curl -s http://localhost:8080/api/weather/Luanda
{"city":"Luanda","country":"Angola","temperature_c":20.2,"observed_at":"2026-07-23T19:00"}A REAL temperature, looked up live — not a fixed value. The container's logs confirm the requests:
delonix container logs delonix-temp
INFO: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit)
INFO: 10.0.2.2:57786 - "GET /health HTTP/1.1" 200 OK
INFO: 10.0.2.2:57802 - "GET /api/weather/Luanda HTTP/1.1" 200 OK4. Expose it to the internet
A local port isn't enough — the goal is a URL anyone, on any network, can open. This is where
kind: Gateway comes in:
delonix net tunnel expose 8080 --name delonix-temp
tunnel/delonix-temp: running — https://lfdhz-197-148-40-67.free.pinggy.netThat URL is REAL — it's the one this guide got when the command was actually run. (Yours will be different every time: the free provider assigns a new one each session.) Confirmation, from the outside, without touching anything local:
curl https://lfdhz-197-148-40-67.free.pinggy.net/api/weather/Luanda
{"city":"Luanda","country":"Angola","temperature_c":20.2,"observed_at":"2026-07-23T19:00"}The same JSON, this time arriving from outside the machine, through an SSH tunnel to a public server (pinggy) and back — zero router configuration, zero public IP of your own, zero account. Opening the URL in a browser shows the Delonix Temp page updating itself.
Going further: with more than one service, put kind:
HTTPRoute in front (routing by Host/path to several containers) and point
tunnel at the PROXY'S PORT instead of directly at the container — one public URL, as
many backends as you need. See examples/httproute.yaml +
examples/tunnel.yaml.
Cleanup
delonix delete gateway delonix-temp
delonix container rm -f delonix-temp
What this proved
| Command | What it validated |
|---|---|
delonix build | a real multi-stage build (2 stages, COPY --from), with build networking |
delonix container run -p | userspace NAT with no root, port published on the host |
delonix container logs | observability of a real service running |
delonix net tunnel expose | REAL public internet traffic reaching a local container, no account, no public IP |