Your endpoints & services
Deploy your app
1
Add a /healthz endpoint
Must return HTTP 200. That's the only platform requirement.
# Node.js
if (req.url === '/healthz') {
  res.writeHead(200); res.end('{"status":"ok"}'); return;
}

# Python / FastAPI
@app.get("/healthz")
def health(): return {"status": "ok"}

# Go
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte(`{"status":"ok"}`))
})
2
Fetch the platform files
Run from your project root. These files wire up CI and tell the platform your app name and port.
mkdir -p .github/workflows
curl -sL https://raw.githubusercontent.com/easydeploytest/cycle02/main/.github/workflows/deploy-dev.yml > .github/workflows/deploy-dev.yml
curl -sL https://raw.githubusercontent.com/easydeploytest/cycle02/main/.github/workflows/promote-prod.yml > .github/workflows/promote-prod.yml
curl -sL https://raw.githubusercontent.com/easydeploytest/cycle02/main/app.yaml > app.yaml
curl -sL https://raw.githubusercontent.com/easydeploytest/cycle02/main/RUNBOOK.md > RUNBOOK.md
Then open app.yaml and set port to your app's port.
3
Set the remote and push
git remote set-url origin https://github.com/easydeploytest/cycle02 2>/dev/null || git remote add origin https://github.com/easydeploytest/cycle02
git add -A
git commit -m "feat: initial deploy"
git push --force origin main
Watch progress at ArgoCD or the portal.
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_NAMEcycle02Auto-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 cycle02 → 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"