🚀 weather-api is live
This is the EasyDeploy template app. Replace it with your own code — everything else is already wired up.
Your endpoints & services
Dev URL
weather-api-dev.easy-deploy.135.181.177.246.nip.io
Prod URL
weather-api.easy-deploy.135.181.177.246.nip.io
ArgoCD
Deployment status & history
Grafana dashboard
Metrics, logs, traces
Infisical — project: weather-api
Environment variables & secrets
GitHub repo
github.com/easydeploytest/weather-api
Deploy a new app
Create another app from the portal
Replace this template with your app
This page is served by the template app. Follow these steps to replace it with your own code. Push to
main when done — the platform rebuilds and deploys automatically.
1
Edit
app.yaml
Set
name (already set to weather-api), team, and port to match your app's HTTP port.
name: weather-api team: easy-deploy port: 3000 # the port your app listens on
2
Write your app in
src/
Delete the current template files in
src/ and add your own. The platform doesn't care about the language or framework — it just builds whatever your Dockerfile produces and exposes the port from app.yaml.
3
Update the
Dockerfile
Replace the template Dockerfile with one that builds your app. The only requirements are: expose the correct port and have a
/healthz endpoint that returns HTTP 200. Examples:
# Node.js FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --production COPY src/ ./src/ EXPOSE 3000 CMD ["node", "src/index.js"] # Bun + Elysia FROM oven/bun:1-alpine WORKDIR /app COPY package.json bun.lockb ./ RUN bun install --frozen-lockfile COPY src/ ./src/ EXPOSE 3000 CMD ["bun", "src/index.ts"] # Python (FastAPI) FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . EXPOSE 3000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "3000"] # Go FROM golang:1.22-alpine AS build WORKDIR /app COPY go.* ./ RUN go mod download COPY . . RUN go build -o server . FROM alpine:3.19 COPY --from=build /app/server /server EXPOSE 3000 CMD ["/server"]
Environment variables & secrets
Secrets are managed in Infisical, not in code or CI. They are injected into your pods as environment variables automatically — no redeploy needed when you change them (updated within ~5 minutes).
1
Open Infisical
Go to https://infisical.easy-deploy.135.181.177.246.nip.io → project weather-api
2
Add secrets per environment
Use the dev environment for dev deployments and prod for production. Secrets in each environment are scoped —
prod secrets are never visible in dev pods.3
Use them in your app
Read them as normal environment variables:
process.env.MY_SECRET / os.environ["MY_SECRET"] / os.Getenv("MY_SECRET")Observability — OpenTelemetry setup
Auto-instrumentation is NOT available for custom runtimes.
The platform does not inject an OTel agent automatically (this requires a language-specific operator that supports your runtime). You must add instrumentation to your app code. Without it, the Grafana dashboard will have no data.
The following env vars are pre-set by the platform in every pod. Your OTel SDK reads them automatically — no config needed in code beyond initialising the SDK.
| Variable | Value | Description |
|---|---|---|
| OTEL_SERVICE_NAME | weather-api | Auto-set by Helm chart |
| OTEL_EXPORTER_OTLP_ENDPOINT | set in Infisical | Grafana Cloud OTLP gateway URL |
| OTEL_EXPORTER_OTLP_HEADERS | set in Infisical | Authorization=Basic <base64(id:token)> |
| OTEL_EXPORTER_OTLP_PROTOCOL | http/protobuf | Auto-set by Helm chart |
Ask your platform team for
OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS, then set them in Infisical → project weather-api → environments dev and prod.
1. Install packages
npm install @opentelemetry/sdk-node \ @opentelemetry/auto-instrumentations-node \ @opentelemetry/exporter-trace-otlp-http \ @opentelemetry/exporter-metrics-otlp-http \ @opentelemetry/sdk-metrics
2. Create
src/instrumentation.js// Must be imported BEFORE anything else
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-http');
const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter(), // reads OTEL_EXPORTER_OTLP_ENDPOINT
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter(),
exportIntervalMillis: 15_000,
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
process.on('SIGTERM', () => sdk.shutdown());
3. Import at top of
src/index.jsrequire('./instrumentation'); // must be first line
const http = require('http');
// ... rest of your app
4. Structured logs (stdout → Loki)
const log = (level, message, extra = {}) =>
console.log(JSON.stringify({ level, message, app: process.env.OTEL_SERVICE_NAME, ...extra }))
log('info', 'server started', { port: 3000 })
log('error', 'something failed', { error: err.message })
1. Install packages
bun add @elysiajs/opentelemetry @opentelemetry/sdk-node \ @opentelemetry/exporter-trace-otlp-http \ @opentelemetry/exporter-metrics-otlp-http \ @opentelemetry/sdk-metrics \ @opentelemetry/auto-instrumentations-node
2. Create
src/instrumentation.tsimport { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter(),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter(),
exportIntervalMillis: 15_000,
}),
});
sdk.start();
process.on('SIGTERM', () => sdk.shutdown());
3. Wire into Elysia (
src/index.ts)import './instrumentation'; // must be first import
import { Elysia } from 'elysia';
import { opentelemetry } from '@elysiajs/opentelemetry'; // HTTP auto-instrument
new Elysia()
.use(opentelemetry())
.get('/healthz', () => ({ status: 'ok' }))
.listen(3000);
4. Structured logs
const log = (level: string, message: string, extra = {}) =>
console.log(JSON.stringify({ level, message, service: process.env.OTEL_SERVICE_NAME, ...extra }));
log('info', 'server started', { port: 3000 });
1. Install packages
pip install opentelemetry-sdk \ opentelemetry-exporter-otlp \ opentelemetry-instrumentation-fastapi \ # or flask, django, etc. opentelemetry-instrumentation-httpx
2. Create
instrumentation.pyfrom opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
def setup_telemetry(app=None):
# Reads OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS automatically
tracer_provider = TracerProvider()
tracer_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(tracer_provider)
reader = PeriodicExportingMetricReader(OTLPMetricExporter(), export_interval_millis=15000)
metrics.set_meter_provider(MeterProvider(metric_readers=[reader]))
if app:
FastAPIInstrumentor.instrument_app(app)
3. Call in
main.pyfrom fastapi import FastAPI
from instrumentation import setup_telemetry
import logging, json
app = FastAPI()
setup_telemetry(app)
logging.basicConfig()
logger = logging.getLogger(__name__)
@app.get('/healthz')
def health(): return {'status': 'ok'}
1. Add dependencies
go get go.opentelemetry.io/otel \ go.opentelemetry.io/otel/sdk/trace \ go.opentelemetry.io/otel/sdk/metric \ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp \ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp \ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
2. Create
telemetry.gopackage main
import (
"context"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/trace"
"time"
)
func setupTelemetry(ctx context.Context) func() {
// Reads OTEL_EXPORTER_OTLP_ENDPOINT + OTEL_EXPORTER_OTLP_HEADERS automatically
traceExp, _ := otlptracehttp.New(ctx)
tp := trace.NewTracerProvider(trace.WithBatcher(traceExp))
otel.SetTracerProvider(tp)
metricExp, _ := otlpmetrichttp.New(ctx)
mp := metric.NewMeterProvider(metric.WithReader(
metric.NewPeriodicReader(metricExp, metric.WithInterval(15*time.Second)),
))
otel.SetMeterProvider(mp)
return func() { tp.Shutdown(ctx); mp.Shutdown(ctx) }
}
3. Wrap HTTP handler
import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
func main() {
shutdown := setupTelemetry(context.Background())
defer shutdown()
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"status":"ok"}`))
})
http.ListenAndServe(":3000", otelhttp.NewHandler(mux, "server"))
}
Deploy to production
Prod deploys are triggered by GitHub Releases, not pushes. This prevents accidental production deployments.
1
Merge to
mainAll prod deploys start from a commit that is already running on dev. Verify the dev URL before promoting.
2
Create a GitHub Release
Tag:
v1.0.0 (semver). The platform re-tags the dev image with this version and ArgoCD syncs the prod namespace.
gh release create v1.0.0 --title "v1.0.0" --notes "First production release"
3
Prod is live