Installation
otel-magnify ships as a single binary that embeds the frontend. Three deployment paths are supported: Docker Compose, Kubernetes with Helm, and a native binary.
Prerequisites
- Go version compatible with
go.modwhen building from source. - Node.js/npm only when rebuilding the frontend locally. The production binary embeds pre-built frontend assets.
- PostgreSQL 18.x.
DB_DSNis required by the binary. - A strong
JWT_SECRET. Startup fails when it is missing, too short, or still set to the production placeholder.
PostgreSQL 18 support boundary
PostgreSQL 18.x is the only supported server version. An existing v0.7.1 database on PostgreSQL 16 must be dumped or migrated into a distinct PostgreSQL 18 instance and validated with the same v0.7.1 artifact before the application is upgraded. Never reuse a PostgreSQL 16 physical data directory with PostgreSQL 18. Follow the PostgreSQL lifecycle runbook for backup, upgrade, and rollback.
Docker Compose
The supported cold-start path from a source checkout is Docker Compose. It includes an opt-in activation workload that can acknowledge remote config:
export JWT_SECRET="$(openssl rand -hex 32)"
export POSTGRES_PASSWORD="$(openssl rand -hex 24)"
export SEED_ADMIN_EMAIL="admin@example.invalid"
read -r -s -p "Initial admin password (minimum 12 characters): " SEED_ADMIN_PASSWORD
echo
export SEED_ADMIN_PASSWORD
docker compose --profile activation up --detach --build
The API and embedded frontend are served at http://localhost:8080. The OpAMP endpoint listens on :4320 and is available to containers on the Compose network at ws://otel-magnify:4320/v1/opamp.
Sign in, open the otelcol-activation-demo workload, edit and validate the
config, generate its safety plan, request approval, approve it, and push it.
The local activation agent reports the final OpAMP status as applied. It is a
protocol simulator, not a Collector runtime.
After the first successful login, recreate the application without its bootstrap credential:
unset SEED_ADMIN_EMAIL SEED_ADMIN_PASSWORD
docker compose --profile activation up --detach --force-recreate otel-magnify
To verify the whole path automatically on a clean PostgreSQL volume:
The smoke test generates ephemeral credentials, requires the Community
approval and policy-preview features, and fails unless /readyz returns
exactly ready, the database reports PostgreSQL major 18, and first-admin
login, workload discovery, approval, push, and the applied acknowledgement
complete within 15 minutes. It removes its isolated containers, network,
volume, and credential files on exit.
Compose defaults:
DB_DSN=postgres://magnify:***@postgres:5432/magnify?sslmode=disableCORS_ORIGINS=http://localhost:8080- PostgreSQL data persisted in a fresh
pg18-dataDocker volume, mounted at/var/lib/postgresqlwithPGDATA=/var/lib/postgresql/18/docker OPAMP_SHARED_SECRETempty unless you set it in the shell environment
Do not use sample password values from docs in a shared environment. Do not
run docker compose down --volumes against an older project until its database
has been migrated, validated on PostgreSQL 18, and backed up.
Published GHCR image
Once a release containing this activation path is published and the package is public, the same smoke test can verify an anonymous image pull before starting:
read -r -p "Released image version (without v prefix): " released_version
OTEL_MAGNIFY_IMAGE="ghcr.io/magnify-labs/otel-magnify:${released_version}" \
./scripts/activation-smoke.sh
The script uses an empty temporary Docker configuration for the pull, so a developer's cached GitHub credentials cannot produce a false positive.
If GHCR returns denied, an organization owner must perform this external
GitHub action; the repository cannot change package visibility from code:
- Open Magnify-Labs → Settings → Packages.
- Under Package Creation, enable Public. If this setting is locked, an organization or Enterprise owner must change the higher-level policy.
- Open Magnify-Labs → Packages → otel-magnify → Package settings.
- In Danger Zone, select Change visibility, choose Public, and type the package name to confirm.
- Optionally disable public package creation again at the organization level;
the existing
otel-magnifypackage remains public.
GitHub documents this under Configuring a package's access control and
visibility.
Changing a private package to public cannot be undone. Verify the result with
an empty Docker configuration and a released tag, not with latest:
read -r -p "Released image version (without v prefix): " released_version
anonymous_config="$(mktemp -d)"
DOCKER_CONFIG="${anonymous_config}" \
docker pull "ghcr.io/magnify-labs/otel-magnify:${released_version}"
rm -rf "${anonymous_config}"
Kubernetes (Helm)
Create three operator-managed Secrets: one for the PostgreSQL DSN, one for the durable JWT signing key, and a separate Secret for the removable first-admin bootstrap credential. Prefer External Secrets or your platform secret manager in a shared cluster. The following local example avoids putting secret values in Helm values, shell history, or process arguments:
set -euo pipefail
namespace="otel-magnify"
secret_directory="$(mktemp -d)"
chmod 700 "${secret_directory}"
trap 'rm -rf "${secret_directory}"' EXIT
kubectl create namespace "${namespace}" --dry-run=client -o yaml | kubectl apply -f -
read -r -s -p "PostgreSQL DSN: " database_dsn
echo
read -r -s -p "Initial admin password (minimum 12 characters): " seed_admin_password
echo
read -r -p "Released image version (without v prefix): " released_version
printf '%s' "${database_dsn}" >"${secret_directory}/db-dsn"
printf '%s' "$(openssl rand -hex 32)" >"${secret_directory}/jwt-secret"
printf '%s' 'admin@example.invalid' >"${secret_directory}/seed-admin-email"
printf '%s' "${seed_admin_password}" >"${secret_directory}/seed-admin-password"
unset database_dsn seed_admin_password
kubectl --namespace "${namespace}" create secret generic magnify-postgres \
--from-file="${secret_directory}/db-dsn" \
--dry-run=client -o yaml | kubectl apply -f -
kubectl --namespace "${namespace}" create secret generic magnify-auth \
--from-file="${secret_directory}/jwt-secret" \
--dry-run=client -o yaml | kubectl apply -f -
kubectl --namespace "${namespace}" create secret generic magnify-bootstrap \
--from-file="${secret_directory}/seed-admin-email" \
--from-file="${secret_directory}/seed-admin-password" \
--dry-run=client -o yaml | kubectl apply -f -
helm install magnify helm/otel-magnify/ \
--namespace "${namespace}" \
--set image.tag="${released_version:?pin a released image version}" \
--set database.existingSecret=magnify-postgres \
--set auth.existingSecret=magnify-auth \
--set auth.seedAdmin.enabled=true \
--set auth.seedAdmin.existingSecret=magnify-bootstrap
After the first successful login, disable the seed environment references and
delete only the bootstrap Secret. Keep magnify-auth: changing or losing the
JWT signing key invalidates every active session.
helm upgrade magnify helm/otel-magnify/ \
--namespace otel-magnify \
--reuse-values \
--set auth.seedAdmin.enabled=false
kubectl --namespace otel-magnify delete secret magnify-bootstrap
The chart creates:
- one
Deployment - one
Serviceexposing named portsapiandopamp - an optional release
Secretonly when a legacy inline value is supplied - no release
db-dsncredential whendatabase.existingSecretis set; theDeploymentreads the operator-managed Secret anddatabase.existingSecretKey - no release JWT credential when
auth.existingSecretis set; theDeploymentreadsauth.jwtSecretKey - optional seed-admin references from a separate Secret only while
auth.seedAdmin.enabled=true - an optional
Ingressfor the API/frontend only
Important values:
| Value | Default | Notes |
|---|---|---|
replicaCount |
1 |
Must remain 1 while the OpAMP registry and live connections are process-local. |
deployment.secretRevision |
empty | Change after rotating an operator-managed Secret to restart the pod. |
image.repository |
ghcr.io/magnify-labs/otel-magnify |
Container image repository. |
image.tag |
chart app version | Override to pin a release/image digest. |
service.type |
ClusterIP |
Exposes both API and OpAMP service ports inside the cluster. |
service.apiPort |
8080 |
Service port for API, frontend, health, and browser WebSocket hub. |
service.opampPort |
4320 |
Service port for OpAMP clients. |
ingress.enabled |
false |
Ingress routes to the API port only. Expose OpAMP separately if agents connect from outside the cluster. |
database.existingSecret |
empty | Operator-managed Secret containing the PostgreSQL DSN; preferred when a secret manager creates it. |
database.existingSecretKey |
db-dsn |
Key holding the PostgreSQL DSN in database.existingSecret. |
database.dsn |
empty | Explicit DSN used to create the release Secret when database.existingSecret is empty. |
database.maxOpenConns |
40 |
Maximum PostgreSQL connections held open. |
database.maxIdleConns |
10 |
Maximum idle PostgreSQL connections retained. |
database.connMaxIdleTimeSeconds |
300 |
Maximum idle time for a pooled connection. |
database.connMaxLifetimeSeconds |
1800 |
Maximum lifetime for a pooled connection. |
config.corsOrigins |
empty | Passed to CORS_ORIGINS. Set this to your external UI origin when using ingress. |
auth.existingSecret |
empty | Operator-managed Secret containing the durable JWT signing key; required for the supported install path. |
auth.jwtSecretKey |
jwt-secret |
Key holding the JWT signing key in auth.existingSecret. |
auth.seedAdmin.enabled |
false |
Inject the bootstrap email and password from a separate Secret. Disable after first login. |
auth.seedAdmin.existingSecret |
empty | Secret containing the seed email and password keys; required when bootstrap is enabled. |
jwtSecret |
empty | Legacy inline fallback that renders a release Secret; avoid for new installations. |
opampSharedSecret |
empty | Stored in the generated Kubernetes Secret as opamp-shared-secret; leave empty only for trusted local/internal OpAMP networks. |
automountServiceAccountToken |
false |
The binary does not call the Kubernetes API. |
podSecurityContext / containerSecurityContext |
hardened non-root defaults | Keep these defaults unless your runtime requires a documented exception. |
Deployment availability and probes
The chart deliberately supports one replica only while the OpAMP registry and
live collector connections are process-local. It rejects any replicaCount
other than 1. The Deployment uses the Recreate strategy, so an upgrade has
brief downtime and connected collectors must reconnect after the replacement
pod starts.
The startup and liveness probes call /healthz, which checks that the process
is alive. The readiness probe calls /readyz, which depends on PostgreSQL and
keeps the pod out of Service endpoints while the database is unavailable.
For an operator-managed external Secret, first apply the updated Secret and
then change deployment.secretRevision in the Helm release. This explicit pod
template change triggers a restart; the chart does not read external Secret
contents during rendering. For example:
kubectl --namespace otel-magnify apply -f magnify-secrets.yaml
helm upgrade magnify helm/otel-magnify/ \
--namespace otel-magnify \
--reuse-values \
--set-string deployment.secretRevision="rev-2"
helm upgrade --atomic can roll back Kubernetes release objects after a failed
upgrade, but it never restores PostgreSQL schemas or data. Back up PostgreSQL
and verify recovery independently before an application upgrade.
Helm security caveats
- Passing secrets with
--setcan expose them in shell history and Helm release state. Use pre-created Secrets for the database, JWT key, and first-admin credential. - When
database.dsnis set withoutdatabase.existingSecret, Helm creates the release Secret with thedb-dsncredential. Whendatabase.existingSecretis set, Helm does not render a releasedb-dsn; protect the operator-managed Secret and namespace read access accordingly. - Store the JWT signing key separately from the seed-admin credential. The former is durable; the latter should be deleted immediately after bootstrap.
- The default ingress exposes only the API/frontend. OpAMP is a separate service port and should be exposed deliberately, with network policy, an internal load balancer, and
OPAMP_SHARED_SECRETwhen possible. readOnlyRootFilesystemis enabled. The application uses/tmponly for temporary files; database state belongs to PostgreSQL.automountServiceAccountToken=falseshould stay disabled unless an extension binary actually needs Kubernetes API access.
Helm secret references
Reference Secrets created by your secret manager without copying their content into a Helm values file:
database:
existingSecret: magnify-postgres
existingSecretKey: db-dsn
auth:
existingSecret: magnify-auth
jwtSecretKey: jwt-secret
seedAdmin:
enabled: true
existingSecret: magnify-bootstrap
emailKey: seed-admin-email
passwordKey: seed-admin-password
Native binary
Build from source:
go build -o otel-magnify ./cmd/server/
DB_DSN="${DB_DSN:?set DB_DSN through your secret workflow}" \
JWT_SECRET="$(openssl rand -hex 32)" ./otel-magnify
For local development with an initial admin:
export JWT_SECRET="$(openssl rand -hex 32)"
export SEED_ADMIN_EMAIL="admin@example.invalid"
read -r -s -p "Initial admin password (minimum 12 characters): " SEED_ADMIN_PASSWORD
echo
export SEED_ADMIN_PASSWORD
DB_DSN="${DB_DSN:?set DB_DSN through your secret workflow}" ./otel-magnify
Seed an admin user on first start
Both SEED_ADMIN_EMAIL and SEED_ADMIN_PASSWORD must be set, and the password
must contain at least 12 characters. Bootstrap succeeds only when the users
table is empty. A restart with the same already-administrator email is
idempotent and does not reset its password. Any other existing user or a
same-email non-admin makes startup fail closed.
Use this as an initial bootstrap mechanism only. After first login, rotate the password through the application or your operational process and remove the seed variables from the runtime environment.
Post-install smoke checks
curl -fsS http://localhost:8080/healthz
curl -fsS http://localhost:8080/readyz
curl -fsS http://localhost:8080/api/v1/capabilities
curl -fsS http://localhost:8080/api/auth/methods
Expected unauthenticated responses:
/healthzreturnsok./readyzreturnsreadywhen PostgreSQL is reachable./api/v1/capabilitiesis the canonical public capability-discovery endpoint and returns only the Communityconfig_safety.approvalsandconfig_safety.policy_previewcapabilities in this release./api/auth/methodslists the password login method by default.
GET /api/features remains a legacy boolean compatibility endpoint; it is not the smoke-check path for new integrations. Capability discovery is not authorization: protected APIs still enforce authentication, RBAC, and server-side gates. For edition binary maintainers, WithCapabilities is preferred for typed declarations; WithFeatures remains supported for legacy edition overlays.
Production checklist
Before exposing otel-magnify beyond a developer machine:
- Generate a strong
JWT_SECRET; do not reuse docs/examples. - Use PostgreSQL 18.x with
sslmode=verify-full, a trusted root certificate, and hostname verification. - Configure and rehearse PostgreSQL backups and recovery, and budget connection limits for the expected workload. Community makes no PITR, RPO, or RTO promise; follow the lifecycle runbook.
- Set
CORS_ORIGINSto the exact browser origin(s) that should access the API. - Serve the API/frontend and WebSocket hub over TLS.
- Treat legacy WebSocket URLs containing
?token=as sensitive; browser clients should normally use theom_sessionHttpOnly cookie on/ws. - Restrict OpAMP exposure to trusted agents/networks and configure
OPAMP_SHARED_SECRETwhen OpAMP crosses a shared or exposed boundary. - Review any Collector YAML before sharing it publicly; exporter configs often contain credentials.