Your endpoints & services
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"]
4
Push to main
That's it. The platform detects the push, builds your image, and deploys to dev. Watch progress at ArgoCD or the portal.
git add -A
git commit -m "feat: replace template with my app"
git push
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
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.

VariableValueDescription
OTEL_SERVICE_NAMEweather-apiAuto-set by Helm chart
OTEL_EXPORTER_OTLP_ENDPOINTset in InfisicalGrafana Cloud OTLP gateway URL
OTEL_EXPORTER_OTLP_HEADERSset in InfisicalAuthorization=Basic <base64(id:token)>
OTEL_EXPORTER_OTLP_PROTOCOLhttp/protobufAuto-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.js
require('./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.ts
import { 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.py
from 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.py
from 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.go
package 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 main
All 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"