🚀 cycle01 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
cycle01-dev.easy-deploy.135.181.177.246.nip.io
Prod URL
cycle01.easy-deploy.135.181.177.246.nip.io
ArgoCD
Deployment status & history
Grafana dashboard
Metrics, logs, traces
Infisical — project: cycle01
Environment variables & secrets
GitHub repo
https://github.com/easydeploytest/cycle01
Deploy a new app
Create another app from the portal
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/cycle01/main/.github/workflows/deploy-dev.yml > .github/workflows/deploy-dev.yml curl -sL https://raw.githubusercontent.com/easydeploytest/cycle01/main/.github/workflows/promote-prod.yml > .github/workflows/promote-prod.yml curl -sL https://raw.githubusercontent.com/easydeploytest/cycle01/main/app.yaml > app.yaml curl -sL https://raw.githubusercontent.com/easydeploytest/cycle01/main/RUNBOOK.md > RUNBOOK.mdThen open
app.yaml and set port to your app's port.
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 cycle01
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 | cycle01 | 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 cycle01 → 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