How to Install Uptrace on Ubuntu 24.04 — Self-Hosted OpenTelemetry APM
Application performance monitoring does not have to come with a five-figure invoice. Uptrace is an open-source APM built natively on OpenTelemetry and backed by ClickHouse, the same columnar database that powers observability platforms at Cloudflare and eBay. This tutorial walks through a production-ready install on Ubuntu 24.04: Docker Compose stack, uptrace.yml configuration, Nginx TLS termination, and a worked example of instrumenting a real application with the OpenTelemetry SDK.
Recommended Plan: CloudCore Professional VPS — 6 vCPU, 12 GB RAM, 100 GB NVMe from EUR 19.99/month. Enough headroom for ClickHouse, PostgreSQL, and Nginx in one box.
Table of Contents
uptrace.ymlWhat is Uptrace?
Uptrace is an open-source observability platform that accepts OpenTelemetry Protocol (OTLP) data — traces, metrics, and logs — and stores it in ClickHouse for fast, cheap querying. It is written in Go, distributed as a single binary or container, and uses PostgreSQL for configuration and ClickHouse for telemetry. The web UI provides distributed trace views, service maps, metric dashboards, log search, and alerting.
Unlike traditional APMs that require proprietary agents, Uptrace sits behind the standard OTLP endpoint. Any language that has an OpenTelemetry SDK — Go, Python, Node.js, Java, .NET, Ruby, PHP, Rust — can send data with zero vendor-specific code. Swap Uptrace out for another OTel backend later and your instrumentation does not change.
The architecture has three runtime components:
- Uptrace (Go application) — receives OTLP, serves the UI, runs alerts.
- ClickHouse — columnar database for spans, metrics, and logs. Massive compression (10-20x), subsecond queries over billions of rows.
- PostgreSQL — stores users, projects, dashboards, saved queries, and alert rules.
Why Self-Host Instead of Using Datadog or SigNoz Cloud?
Observability is one of the categories where managed SaaS costs balloon faster than anywhere else. A modest fleet of 20 hosts emitting traces and custom metrics on Datadog APM can run USD 1,500-3,000 per month before logs, real-user monitoring, or synthetic checks are factored in. Per-GB ingestion fees punish teams just as they are scaling their observability practice. SigNoz Cloud and New Relic One are less brutal but still priced per host or per ingested gigabyte.
Self-hosting Uptrace on a single CloudCore Professional VPS at EUR 19.99/month changes the math entirely:
- Flat-rate cost — EUR 19.99/month regardless of how many spans you send. No per-host billing, no ingestion overage.
- Data sovereignty — Traces often contain URLs, request bodies, stack traces, and user identifiers. Keeping them in a VPS under your own DPA simplifies GDPR, HIPAA, and internal compliance audits.
- No sampling anxiety — When storage costs EUR 0 per extra span, you can keep 100% of traces for small services instead of aggressive head-based sampling.
- Open standards — OTLP in, ClickHouse out. If you outgrow Uptrace, point your collectors at any other OTel backend (SigNoz, Grafana Tempo, Honeycomb, Jaeger) without rewriting a line of instrumentation.
- Works offline and in air-gapped networks — Useful for on-premises gateways, edge deployments, or regulated environments.
Cost Comparison: Uptrace vs. Managed APM
| Scenario | Datadog APM | SigNoz Cloud | New Relic | Self-Hosted Uptrace |
|---|---|---|---|---|
| 10 hosts, 50M spans/mo | ~USD 310/mo | ~USD 199/mo | ~USD 99+ingest | EUR 19.99/mo |
| 25 hosts, 200M spans/mo | ~USD 775/mo | ~USD 499/mo | ~USD 300+ingest | EUR 19.99/mo |
| Data residency control | Limited regions | Shared infra | Shared infra | Full (your VPS) |
| Sampling forced? | Often yes | Optional | Often yes | No |
| Custom ClickHouse queries | No | No | No | Yes |
Prerequisites
Before starting, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access. The CloudCore Professional plan (6 vCPU / 12 GB RAM / 100 GB NVMe / EUR 19.99 per month) is the recommended baseline.
- A domain name you control, with an
Arecord pointing to your VPS public IP (e.g.uptrace.yourdomain.com). - SSH access to the server as a user with sudo privileges.
- At least 8 GB of RAM — ClickHouse and PostgreSQL together are memory-hungry. 12 GB is comfortable.
- At least 40 GB of free disk space for 30 days of moderate-volume traces. Scale up if you plan to ingest heavily.
ssh root@your-server-ipStep 1: Prepare Ubuntu 24.04
Update the package index and upgrade installed packages so dependency resolution runs against current metadata:
sudo apt update && sudo apt upgrade -yInstall baseline utilities you will reach for repeatedly:
sudo apt install -y curl ca-certificates gnupg lsb-release ufw git vimConfigure the firewall. We only expose SSH and HTTPS (443) publicly. Uptrace's OTLP ports stay on loopback, reachable via the reverse proxy:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
sudo ufw statusIf the kernel was upgraded, reboot before continuing:
sudo rebootStep 2: Install Docker and the Compose Plugin
Uptrace ships an official Docker Compose bundle. Install the Docker Engine from the Docker repository (the Ubuntu-shipped docker.io package lags behind).
Add the Docker GPG key and repository:
sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \ sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Install the engine, CLI, and Compose plugin:
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginVerify:
docker --version
docker compose versionExpected output:
Docker version 27.3.1, build ce1223035a
Docker Compose version v2.29.7Add your sudo user to the docker group so you do not need sudo for every command:
sudo usermod -aG docker $USER
newgrp dockerStep 3: Fetch the Uptrace Docker Compose Bundle
Create a deployment directory and clone the example bundle that the Uptrace team maintains:
sudo mkdir -p /opt/uptrace
sudo chown $USER:$USER /opt/uptrace
cd /opt/uptrace
git clone https://github.com/uptrace/uptrace.git source
cp -r source/example/docker/* .You now have the following layout under /opt/uptrace:
.
├── docker-compose.yml
├── config/
│ └── uptrace.yml
├── vector.toml
└── ...The docker-compose.yml wires together four services:
clickhouse— official ClickHouse server image, listens on port 9000 internally.postgres— PostgreSQL 16 for Uptrace's metadata.uptrace— the Uptrace backend and web UI, listens on 14317 (OTLP/gRPC), 14318 (OTLP/HTTP), and 14319 (web UI).otelcol(optional) — an OpenTelemetry Collector pre-configured to forward to Uptrace.
uptrace/uptrace:2.2 is recommended over the latest tag for production.Step 4: Configure uptrace.yml
The single source of truth for Uptrace's runtime behaviour is config/uptrace.yml. Open it:
vim /opt/uptrace/config/uptrace.ymlThe file is YAML with several sections. The ones you must edit before first boot:
##
Uptrace configuration
See https://uptrace.dev/get/install.html for the full reference.
##PostgreSQL (metadata)
pg:
addr: postgres:5432
user: uptrace
password: CHANGE_ME_STRONG_PG_PASSWORD
database: uptraceClickHouse (telemetry)
ch:
addr: clickhouse:9000
user: uptrace
password: CHANGE_ME_STRONG_CH_PASSWORD
database: uptraceListen addresses — keep on loopback; Nginx terminates TLS.
listen:
http:
addr: ':14318'
grpc:
addr: ':14317'The external URL users and agents will use.
site:
addr: 'https://uptrace.yourdomain.com'Initial admin user created on first boot.
auth:
users:
- name: Admin
email: [email protected]
password: CHANGE_ME_STRONG_ADMIN_PASSWORD
# Optional: OIDC / SSO block goes here.Projects — each gets a DSN (token) your apps use to send OTLP data.
projects:
- id: 1
name: production
token: CHANGE_ME_RANDOM_PROJECT_TOKEN
pinned_attrs:
- service.name
- host.name
- deployment.environmentData retention and storage engine.
ch_schema:
spans:
ttl_delete: 30 DAY
metrics:
ttl_delete: 90 DAYSecret used to sign session cookies.
secret_key: CHANGE_ME_64_CHAR_RANDOM_STRINGAlerting channels (optional).
alerting:
create_alerts_from_spans:
enabled: true
rules:
- name: High error rate
metrics:
- uptrace.tracing.spans
query:
- group by project.id, service.name, deployment.environment
- where span.status_code = 'error'
- per_min(count($spans)) > 10
for: 5mGenerate strong random values for each CHANGE_ME placeholder:
openssl rand -hex 32 # use for secret_key and project token
openssl rand -base64 24 # use for passwordsReplace the four placeholders in uptrace.yml. Never commit this file to a public repository.
Update the site URL to the domain you will serve Uptrace on. This address is used in email links, alert notifications, and the CORS config — if it does not match what users type in the browser, login will fail.
Step 5: Start the Stack
From /opt/uptrace, launch the full stack in the background:
cd /opt/uptrace
docker compose up -dDocker pulls the images (approximately 1.2 GB total across ClickHouse, Postgres, and Uptrace) and starts the containers. After 30-60 seconds, check status:
docker compose psExpected output:
NAME IMAGE COMMAND STATUS
uptrace-clickhouse-1 clickhouse/clickhouse-server:24.9 "/entrypoint.sh" Up (healthy)
uptrace-postgres-1 postgres:16-alpine "docker-entrypoint..." Up (healthy)
uptrace-uptrace-1 uptrace/uptrace:2.2 "/uptrace serve" UpTail the Uptrace logs on first boot. It runs database migrations against both PostgreSQL and ClickHouse:
docker compose logs -f uptraceYou should see lines like:
INFO migrating database pg...
INFO migrating database ch...
INFO listening on :14318 (http)
INFO listening on :14317 (grpc)
INFO serving UI on :14319Quick health check from the VPS itself:
curl -I http://localhost:14319Expected output:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8The UI is alive but not yet reachable from the internet. That is the next step.
Step 6: Put Nginx in Front with Let's Encrypt TLS
Never expose Uptrace's HTTP port directly. Terminate TLS at Nginx and keep everything else on localhost.
Install Nginx and Certbot:
sudo apt install -y nginx certbot python3-certbot-nginxCreate the site config:
sudo tee /etc/nginx/sites-available/uptrace > /dev/null <<'EOF'Uptrace — OpenTelemetry APM
upstream uptrace_ui { server 127.0.0.1:14319; }upstream uptrace_otlp_http { server 127.0.0.1:14318; }
server { listen 80; server_name uptrace.yourdomain.com; return 301 https://$host$request_uri; }
server { listen 443 ssl http2; server_name uptrace.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/uptrace.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/uptrace.yourdomain.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Content-Type-Options nosniff; add_header X-Frame-Options DENY;
client_max_body_size 50m;
# OTLP/HTTP ingest — /v1/traces, /v1/metrics, /v1/logs location /v1/ { proxy_pass http://uptrace_otlp_http; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 300s; }
# Web UI and API location / { proxy_pass http://uptrace_ui; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;
# Streaming responses and websockets proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_buffering off; proxy_read_timeout 300s; } } EOF
Enable the site and issue a certificate:
sudo ln -s /etc/nginx/sites-available/uptrace /etc/nginx/sites-enabled/ sudo rm -f /etc/nginx/sites-enabled/default sudo nginx -t sudo systemctl reload nginx
sudo certbot --nginx -d uptrace.yourdomain.com \ --non-interactive --agree-tos -m [email protected]
Certbot edits the 443 server block to inject the correct certificate paths and sets up an auto-renewal timer. Verify the renewal hook:
sudo systemctl list-timers | grep certbotOpen https://uptrace.yourdomain.com in your browser. Log in with the admin credentials from uptrace.yml. You should see an empty Projects dashboard waiting for data.
For OTLP/gRPC ingestion (port 14317) from external hosts, expose a second TLS endpoint via an Nginx stream block listening on 4317 with SSL passthrough, or simpler, use OTLP/HTTP which is already proxied on port 443 under /v1/.
Step 7: Instrument Your First Application
Time to send real telemetry. The pattern is identical across languages: install the OpenTelemetry SDK, point the exporter at Uptrace, add a service.name resource attribute.
Node.js example
npm install @opentelemetry/api \
@opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/exporter-metrics-otlp-httpCreate otel.js:
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 { Resource } = require('@opentelemetry/resources');const UPTRACE_DSN = process.env.UPTRACE_DSN; // DSN format: https://<project-token>@uptrace.yourdomain.com/<project-id> const headers = { 'uptrace-dsn': UPTRACE_DSN };
const sdk = new NodeSDK({ resource: new Resource({ 'service.name': 'web-api', 'service.version': '1.0.0', 'deployment.environment': 'production', }), traceExporter: new OTLPTraceExporter({ url: 'https://uptrace.yourdomain.com/v1/traces', headers, }), metricReader: new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter({ url: 'https://uptrace.yourdomain.com/v1/metrics', headers, }), exportIntervalMillis: 15000, }), instrumentations: [getNodeAutoInstrumentations()], });
sdk.start();
Run your app with --require ./otel.js and hit a few endpoints. Within 15-30 seconds, traces appear in the Uptrace UI under the web-api service.
Python example
pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap --action=installLaunch your app with auto-instrumentation:
export OTEL_SERVICE_NAME=billing-worker export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production" export OTEL_EXPORTER_OTLP_ENDPOINT=https://uptrace.yourdomain.com export OTEL_EXPORTER_OTLP_HEADERS="uptrace-dsn=https://[email protected]/1" export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
opentelemetry-instrument python app.py
Go example
import ( "github.com/uptrace/uptrace-go/uptrace" "go.opentelemetry.io/otel" )uptrace.ConfigureOpentelemetry( uptrace.WithDSN("https://[email protected]/1"), uptrace.WithServiceName("checkout"), uptrace.WithServiceVersion("1.2.0"), uptrace.WithDeploymentEnvironment("production"), ) defer uptrace.Shutdown(ctx)
tracer := otel.Tracer("checkout") ctx, span := tracer.Start(ctx, "process-order") defer span.End()
Because everything is standard OTLP, the same pattern applies to Java (OpenTelemetryAgent JAR), .NET (OpenTelemetry.Extensions.Hosting), Ruby, PHP, and Rust.
Step 8: Use the OpenTelemetry Collector as a Gateway
For anything beyond a single application, put an OpenTelemetry Collector in front of Uptrace. The collector can:
- Batch and compress spans before shipping (reduces egress costs).
- Accept Prometheus scrape, Jaeger, Zipkin, Fluent Forward, and syslog formats and convert them to OTLP.
- Tail-sample expensive traces (keep 100% of errors, 1% of healthy traffic).
- Enrich spans with host metadata (Kubernetes labels, AWS instance tags, etc.).
otel-collector-config.yaml):receivers: otlp: protocols: grpc: { endpoint: 0.0.0.0:4317 } http: { endpoint: 0.0.0.0:4318 } prometheus: config: scrape_configs: - job_name: node static_configs: - targets: ['node-exporter:9100']processors: batch: timeout: 5s send_batch_size: 10000 resourcedetection: detectors: [env, system]
exporters: otlphttp/uptrace: endpoint: https://uptrace.yourdomain.com headers: uptrace-dsn: https://[email protected]/1
service: pipelines: traces: receivers: [otlp] processors: [batch, resourcedetection] exporters: [otlphttp/uptrace] metrics: receivers: [otlp, prometheus] processors: [batch, resourcedetection] exporters: [otlphttp/uptrace]
Applications send to the collector on the internal network; only the collector talks to Uptrace over TLS. This is also the recommended way to forward data from existing Prometheus scrape jobs into Uptrace without rewriting exporters.
Step 9: Retention, Backups, and Upgrades
Retention
Data lifetime is controlled by ClickHouse TTLs, set in uptrace.yml under ch_schema. The defaults (30 days for spans, 90 days for metrics) match most teams' needs. If you want 90-day span retention, change:
ch_schema:
spans:
ttl_delete: 90 DAYRestart Uptrace after edits:
docker compose restart uptraceExisting rows keep their original TTL; new rows pick up the new policy. To backfill, run an ALTER TABLE ... MODIFY TTL against ClickHouse manually.
Backups
Two things to back up:
pg_dump inside the container:docker compose exec postgres pg_dump -U uptrace uptrace | gzip > /var/backups/uptrace-pg-$(date +%F).sql.gzBACKUP to an S3-compatible bucket works well:BACKUP DATABASE uptrace TO S3('https://s3.example.com/uptrace-backup', 'KEY', 'SECRET');Schedule both via cron or systemd timers.
Upgrades
cd /opt/uptrace
git -C source pull
docker compose pull
docker compose up -dRead the Uptrace release notes before major version jumps. Minor versions run migrations automatically on startup.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| Login page returns 502 | Uptrace container not ready or wrong upstream port | docker compose ps, check uptrace container is Up, verify Nginx upstream points to 14319 |
| "invalid DSN" error from SDK | DSN format wrong | Must be https://<token>@host/<project-id> where project-id is the numeric id from uptrace.yml |
| No traces appearing, no errors in SDK | Clock skew on VPS (> 5 min) | timedatectl status, enable systemd-timesyncd or chrony |
| ClickHouse container OOM-killed | Default config too aggressive for small VPS | Set CLICKHOUSE_MAX_MEMORY_USAGE=4000000000 env var in compose file |
Nginx client_max_body_size errors on bulk ingest | Large batched OTLP payloads | Raise client_max_body_size to 100m in the Uptrace server block |
| TLS cert renewal fails | Port 80 blocked by firewall | sudo ufw allow 80/tcp, Certbot uses HTTP-01 challenge |
| Query timeouts in UI | ClickHouse CPU-bound on large scans | Add an index on service.name, or move ClickHouse to a dedicated VPS |
| High disk usage after 30 days | TTL not actually deleting | Check ClickHouse system log: docker compose exec clickhouse clickhouse-client --query "SELECT * FROM system.merges" |
Useful debug commands
Stream Uptrace logs:
docker compose logs -f uptraceInspect what is in ClickHouse:
docker compose exec clickhouse clickhouse-clientSELECT count(), project_id, service.name
FROM uptrace.spans_index
WHERE time > now() - INTERVAL 1 HOUR
GROUP BY project_id, service.name;Test OTLP ingest manually:
curl -v https://uptrace.yourdomain.com/v1/traces \
-H "Content-Type: application/json" \
-H "uptrace-dsn: https://[email protected]/1" \
-d '{"resourceSpans":[]}'An empty but well-formed payload should return HTTP 200.
FAQ
What is Uptrace and how does it differ from SigNoz?
Uptrace and SigNoz are both open-source, ClickHouse-backed, OpenTelemetry-native APMs — they solve the same problem but made different engineering choices. Uptrace is a single Go service that bundles ingest, query, and UI; it is simpler to run and has a lighter footprint. SigNoz splits query and frontend into separate services and ships a feature-rich alerting and dashboarding experience. Both accept OTLP, so switching between them is a matter of changing an exporter endpoint.
Why self-host Uptrace instead of using Datadog or SigNoz Cloud?
Managed APM pricing scales punitively with host count and ingestion volume. A fleet emitting 200M spans per month typically runs USD 500-1,500 per month on Datadog or SigNoz Cloud. The same workload on a CloudCore Professional VPS at EUR 19.99/month is a rounding error — and you keep full control of trace data, which often contains PII, request payloads, and internal URLs that some teams cannot send to third-party SaaS under their compliance posture.
What are the minimum hardware requirements?
A 4 vCPU / 8 GB RAM VPS with 100 GB of NVMe storage handles roughly 5-10 million spans per day comfortably. ClickHouse is the main resource consumer — it uses RAM for its marks cache and CPU for compression during inserts. For production workloads above 50M spans/day, scale to 8 vCPU and 32 GB RAM, and mount dedicated storage for the ClickHouse data directory. The CloudCore Professional plan at EUR 19.99/month is a comfortable starting point.
Can I send data from non-Go applications?
Yes. Uptrace accepts OTLP over gRPC (port 14317) and HTTP (port 14318) from any OpenTelemetry SDK — Python, Node.js, Java, .NET, Ruby, PHP, Rust, Swift, and more. You can also front it with the OpenTelemetry Collector to receive Prometheus scrape, Jaeger, Zipkin, Fluent Forward, or syslog and convert them to OTLP before forwarding to Uptrace.
How long is data retained by default?
Uptrace stores data in ClickHouse with a default TTL of 30 days for spans and 90 days for metrics, configurable in uptrace.yml under ch_schema. Because ClickHouse compresses trace data aggressively (typically 10-20x), 30 days of a mid-sized service fits comfortably under 50 GB on disk. Extending to 90 or 180 days is usually a cheap change.
Does Uptrace support alerting?
Yes. Uptrace includes a built-in monitor/alert engine that evaluates metric thresholds, error rates, and span volumes on a schedule. It can send notifications to email, Slack, PagerDuty, Opsgenie, Telegram, and generic webhooks. Alert rules are defined in uptrace.yml under alerting.rules or created through the web UI.
Can I use Grafana on top of Uptrace data?
Yes. Because all telemetry lives in ClickHouse, you can point Grafana at the same ClickHouse instance using the official ClickHouse data source and build custom dashboards alongside Uptrace's built-in UI. This is a common pattern for teams who already standardize on Grafana and want a unified pane of glass across traces, metrics, logs, and business data.
Next Steps
Now that Uptrace is running, build out the rest of your observability stack:
- Ship logs too — Uptrace accepts OTLP logs on the same endpoint. Forward application logs through the OpenTelemetry Collector's
filelogreceiver to correlate logs with traces automatically. - Add a collector gateway — Install the OpenTelemetry Collector on each host to batch, sample, and enrich telemetry before sending to Uptrace.
- Pull Prometheus scrapes in — If you already run Prometheus, configure its remote write or use the collector's
prometheusreceiver to funnel existing scrape jobs into Uptrace without rewriting exporters. - Build custom dashboards in Grafana — Point Grafana at the ClickHouse data source and build team-specific dashboards on top of Uptrace's tables.
- Wire up alerting channels — Connect Uptrace's alert rules to Slack, PagerDuty, or Opsgenie so on-call engineers see incidents where they already work.
- Read the official docs — The Uptrace team maintains detailed installation and configuration notes at uptrace.dev/get/install.html. Bookmark it.
Ready to deploy? Spin up a CloudCore Professional VPS — 6 vCPU, 12 GB RAM, 100 GB NVMe, unmetered bandwidth from EUR 19.99/month. Ubuntu 24.04 LTS images, full root access, and hourly billing. Perfect host for a single-node Uptrace stack.