Telegraf collects the metrics, InfluxDB 3 stores them, and Grafana draws the graphs. Three daemons, two handshakes, and the whole thing runs on a single Ubuntu box. The awkward part is that almost every guide still online wires them together the InfluxDB 2 way, and the pieces have moved.
This walks through an InfluxDB, Telegraf, and Grafana stack on Ubuntu 26.04, with Ubuntu 24.04 covered alongside it because the commands turn out to be identical. We cover the apt repository, the token model that replaced org and bucket auth, the native Telegraf output plugin that shipped recently, the Grafana data source that talks SQL over FlightSQL, and the query window limit in the free edition that will bite you if nobody tells you about it first. If you only need the graphing layer, the standalone Grafana install on Ubuntu 26.04 covers that on its own.
Confirmed working on Ubuntu 26.04 and 24.04 in September 2026.
What the InfluxDB, Telegraf, and Grafana stack needs before you start
Sizing this stack comes down to one number: how many distinct series you are storing, multiplied by how long you keep them. Telegraf’s default input set on a single host produces a few hundred series. A hundred hosts feeding the same database puts you in the tens of thousands, and that is where RAM starts to matter, because InfluxDB 3 holds recent writes in memory before it flushes them to Parquet files on disk.
For disk, budget the compressed Parquet footprint plus room for the write-ahead log, then add headroom. Time series compress well, often better than ten to one, so the usual mistake is over-provisioning disk and under-provisioning RAM. A production single-node deployment collecting from a few dozen hosts typically lands somewhere around 8 to 16 GB of RAM. Grafana itself is cheap and rarely needs more than 1 GB unless you have many concurrent dashboard users.
The box used for this guide ran 4 vCPU and 8 GB of RAM. Treat that as a floor for following along, not a production recommendation.
You also need sudo access and, if you plan to reach Grafana from another machine, a hostname pointing at the server.
Set the values you will reuse
The database name and the auth token appear in most of the commands below. Export them once so you can paste the rest as-is:
export INFLUX_DB="system_metrics"
export GRAFANA_DOMAIN="grafana.example.com"
The token does not exist yet. We create it a few steps down and add it to the same session then. Two files later in the guide cannot read your shell environment and need the values typed in directly: the Telegraf drop-in, which takes its token from a systemd environment file, and the Nginx and Grafana config where the hostname is written literally.
Add the InfluxData repository and install InfluxDB 3
InfluxData publishes one Debian repository that serves every Ubuntu and Debian release, so there is no codename to substitute. Download the signing key and check its fingerprint before you trust it:
curl --silent --location -O https://repos.influxdata.com/influxdata-archive.key
gpg --show-keys --with-fingerprint --with-colons ./influxdata-archive.key
The fingerprint line must read exactly this, otherwise stop and do not add the repository:
fpr:::::::::24C975CBA61A024EE1B631787C3D57159FC2F927:
Convert the key to a keyring and register the repository:
cat influxdata-archive.key | gpg --dearmor | sudo tee /usr/share/keyrings/influxdata-archive.gpg > /dev/null
echo 'deb [signed-by=/usr/share/keyrings/influxdata-archive.gpg] https://repos.influxdata.com/debian stable main' | sudo tee /etc/apt/sources.list.d/influxdata.list
sudo apt-get update
Install the database and the collector together. Telegraf lives in the same repository:
sudo apt-get install influxdb3-core telegraf
Apt pulls in a third package you did not ask for, influxdata-archive-keyring, which takes over key rotation from the manual keyring you just created. That is expected.
The package enables the service but leaves it stopped, which catches people who assume apt started it for them. Before starting it, understand what the default bind address means: InfluxDB 3 listens on 0.0.0.0:8181, and a server with no tokens yet will hand admin rights to whoever asks first. On a cloud VM with a permissive security group, that window is a real exposure. Either close port 8181 at the firewall now, or bind it to loopback, which is the right answer for this single node layout because Telegraf and Grafana both connect over localhost anyway.
Bind it to loopback by adding one line to the config file:
printf '\nhttp-bind="127.0.0.1:8181"\n' | sudo tee -a /etc/influxdb3/influxdb3-core.conf
Now start it and confirm the listener:
sudo systemctl start influxdb3-core
systemctl is-active influxdb3-core
ss -lntp | grep 8181
Port 8181 is the single HTTP port InfluxDB 3 serves everything on, including the SQL interface Grafana uses later. Bound to loopback it reads like this, and if you skipped that step it will say 0.0.0.0:8181 instead:
active
LISTEN 0 128 127.0.0.1:8181 0.0.0.0:*
The Debian package writes its settings to that TOML file rather than passing flags on the command line, which is worth knowing before you go looking for a unit file to edit. Open it with:
sudo vim /etc/influxdb3/influxdb3-core.conf
The defaults it ships with are these, and for a single node they are fine as they are:
node-id="primary-node"
object-store="file"
data-dir="/var/lib/influxdb3/data"
plugin-dir="/var/lib/influxdb3/plugins"
Your http-bind line sits below those. Changing anything here needs a service restart to take effect.
Mint the admin token before anything else reaches the API
InfluxDB 3 drops the organization and bucket model that InfluxDB 2 used. Auth is now a token, and the first one is a special case worth understanding.
A freshly started server has no tokens, and until one exists the API accepts an unauthenticated request to create it. Whoever reaches port 8181 first claims the instance, which is why the bind address matters so much above. Claim it now:
influxdb3 create token --admin
The token prints once and is not recoverable from the database afterwards, so capture it now:
New token created successfully!
Token: apiv3_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
HTTP Requests Header: Authorization: Bearer apiv3_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
IMPORTANT: Store this token securely, as it will not be shown again.
Put it in your shell session so the CLI stops asking for it:
export INFLUXDB3_AUTH_TOKEN="apiv3_your_token_here"
The window is now closed. An unauthenticated query against the same server returns HTTP 401, and a second attempt to create an admin token comes back with a 409 rather than handing out a fresh one.
Create the database Telegraf will write into
Retention is set at creation time and cannot be changed later in the free edition, so decide the number before you run this rather than after:
influxdb3 create database --retention-period 30d "${INFLUX_DB}"
Feed it a unit it does not know and the parser lists everything it does accept, which is not the set the documentation describes:
error: invalid value '5x' for '--retention-period <RETENTION_PERIOD>': unknown time unit "x", supported units: ns, us/µs, ms, sec, min, hours, days, weeks, months, years (and few variations)
The parser and the documentation disagree in both directions here, so it is worth knowing which one to trust. Sub-hour retention is documented as unsupported, yet the current release accepts it and honours it: a database created with 30m comes back from the catalog with retention_period_ns set to 1800000000000, exactly 30 minutes. Going the other way, the mo shorthand that InfluxData’s own retention page documents for months is rejected outright, and so is their example value 1y6mo:
error: invalid value '1mo' for '--retention-period <RETENTION_PERIOD>': unknown time unit "mo", supported units: ns, us/µs, ms, sec, min, hours, days, weeks, months, years (and few variations)
Spell it out as 1month and it works. Compound values like 30d12h are accepted too. Stick to h, d, w and y and you will not trip over any of this, and use none if you want infinite retention.
Listing what exists shows the internal database alongside yours:
influxdb3 show databases
The _internal database holds the server’s own metrics and does not count against your quota:
+----------------+
| iox::database |
+----------------+
| _internal |
| system_metrics |
+----------------+
That quota is five. The free edition refuses the sixth user database with a 422, which is a hard stop rather than a soft warning, so plan your database layout around it. One database per environment works; one per host does not.
Point Telegraf at InfluxDB 3
This is the piece that ties the collector to the database, and it is where most existing tutorials are now wrong. Telegraf 1.38 and later ship a native influxdb_v3 output plugin. Older guides tell you to use influxdb_v2 against a compatibility endpoint, which still works but asks you for an organization and a bucket that InfluxDB 3 no longer has.
Keep the token out of the config file by putting it in the environment file systemd reads for the Telegraf unit:
sudo vim /etc/default/telegraf
Add the token as a single variable:
INFLUX_TOKEN=apiv3_your_token_here
Lock the file down, since it now holds an admin credential:
sudo chmod 600 /etc/default/telegraf
Telegraf reads every file under its drop-in directory, so add the output there instead of editing the main config:
sudo vim /etc/telegraf/telegraf.d/influxdb3.conf
Three settings are all the plugin needs. The database key is the one that replaced the old bucket and organization pair:
[[outputs.influxdb_v3]]
urls = ["http://127.0.0.1:8181"]
token = "${INFLUX_TOKEN}"
database = "system_metrics"
Restart the collector and read the startup lines it logs:
sudo systemctl restart telegraf
sudo journalctl -u telegraf -n 20 --no-pager
Two lines confirm the handshake. The input list is what the stock config collects, and the output line is the one that proves the native plugin loaded rather than the compatibility path:
I! Loaded inputs: cpu disk diskio kernel mem processes swap system
I! Loaded outputs: influxdb_v3
You will also see a warning that skip_processors_after_aggregators changes its default in Telegraf 1.40. It is harmless here and only matters if you later add aggregators.
Confirm the metrics are landing
Give it a collection interval or two, then ask the database what tables exist. Telegraf creates one table per input plugin on first write:
influxdb3 query --database "${INFLUX_DB}" "SELECT table_name FROM information_schema.tables WHERE table_schema='iox'"
Eight tables matching the eight loaded inputs means the write path is working end to end:
+------------+
| table_name |
+------------+
| cpu |
| disk |
| diskio |
| kernel |
| mem |
| processes |
| swap |
| system |
+------------+
If cpu is missing on the first run, wait one more flush and ask again. The CPU plugin needs two samples before it can report a delta, so it lands a beat behind the others.
Now read actual values back:
influxdb3 query --database "${INFLUX_DB}" "SELECT time, host, usage_user, usage_idle FROM cpu WHERE cpu = 'cpu-total' ORDER BY time DESC LIMIT 3"
Real rows with a hostname and float percentages mean the database, the token, and the collector all agree:
+---------------------+---------------+---------------------+-------------------+
| time | host | usage_user | usage_idle |
+---------------------+---------------+---------------------+-------------------+
| 2026-09-05T22:01:10 | cfg-tig-u2604 | 0.47738693467336984 | 99.0954773869332 |
| 2026-09-05T22:01:00 | cfg-tig-u2604 | 0.7031642390758492 | 98.46810647915473 |
| 2026-09-05T22:00:50 | cfg-tig-u2604 | 0.20100502512557344 | 99.54773869346664 |
+---------------------+---------------+---------------------+-------------------+
That is the collection half of the stack finished.

With metrics arriving, the remaining work is entirely on the Grafana side.
Connect Grafana to InfluxDB 3 over FlightSQL
Grafana comes from its own repository. Add it and install the OSS build:
sudo apt-get install -y apt-transport-https software-properties-common wget gnupg
sudo mkdir -p /etc/apt/keyrings/
sudo wget -q -O /etc/apt/keyrings/grafana.asc https://apt.grafana.com/gpg-full.key
echo "deb [signed-by=/etc/apt/keyrings/grafana.asc] https://apt.grafana.com stable main" | sudo tee /etc/apt/sources.list.d/grafana.list
sudo apt-get update
sudo apt-get install grafana
Enable and start it in one command:
sudo systemctl daemon-reload
sudo systemctl enable --now grafana-server
Grafana 13 spends a while on first-run migrations before it binds. If the port looks dead, give it another half minute before you start debugging:
curl -s http://localhost:3000/api/health
A database status of ok means it is ready for a login:
{
"database": "ok",
"version": "13.2.1",
"commit": "56cd3e9288d8255fecebe5d05b48d191f50674b5"
}
Log in at http://10.0.1.50:3000 with admin and admin, and change the password when prompted. If you would rather set it from the shell, note that grafana-cli is deprecated in Grafana 13. Grafana’s own documentation spells the home path out, though on a package install the wrapper already supplies it, so the flag is belt and braces rather than a requirement:
sudo grafana cli --homepath /usr/share/grafana admin reset-admin-password 'YourStrongPassword'
Run that as root rather than switching to the grafana user, which fails for a reason worth knowing about, covered at the end of this guide. Judge the result by logging in with the new password, not by the success line, because that line prints in cases where nothing changed. On a package install the /usr/sbin/grafana wrapper supplies the config file and data directory for you, so no further flags are needed.
Now the handshake that matters. Go to Connections, then Data sources, add a new InfluxDB source, and set Query language to SQL. That switch changes which fields the form asks for. The URL is the same HTTP endpoint the CLI uses, because InfluxDB 3 serves FlightSQL over gRPC on port 8181 alongside the REST API rather than on a separate port.
Fill in four things and leave the rest alone:
| Field | Value |
|---|---|
| Query language | SQL |
| URL | http://127.0.0.1:8181 |
| Database | system_metrics |
| Token | your admin token |
| Insecure Connection | enabled |
That last toggle is the one people miss. FlightSQL runs over gRPC, and without TLS on the InfluxDB side the gRPC client refuses the connection unless Insecure Connection is on. On a stock Grafana 13 it lives under InfluxDB Details, below the HTTP and Auth sections. If someone has enabled the new data source config layout, the same setting moves to Advanced Database Settings and a Product dropdown appears above it, which lists Enterprise but not Core. Pick the closest entry and the SQL settings behave the same.

Click Save & test. A green result means Grafana can reach the database.
Query the data from Grafana
Open Explore, pick the new data source, and switch the editor from Builder to Code. The builder is fine for browsing a schema, but raw SQL is faster once you know the table names:
SELECT time, host, usage_user, usage_idle FROM cpu ORDER BY time DESC LIMIT 10
Run it and you get a table of the same rows the CLI returned, which confirms the read path independently of the write path.

For dashboard panels, add the Grafana time macro so each panel respects the time picker instead of hardcoding a range:
SELECT time, usage_user, usage_system FROM cpu WHERE cpu = 'cpu-total' AND $__timeFilter(time) ORDER BY time
Grafana expands that macro before it reaches the database, turning it into a plain pair of timestamp comparisons:
time >= '2026-09-05T20:28:55Z' AND time <= '2026-09-05T20:43:55Z'
Check that expansion in the query inspector whenever a panel returns nothing. Nine times out of ten the range is correct and the table name is not.
Build the dashboard
Create a dashboard, add a time series panel, and give it the CPU query above. Set the panel unit to Percent so the axis is labelled properly. Repeat for memory and disk, changing the table and the unit:
SELECT time, used_percent FROM mem WHERE $__timeFilter(time) ORDER BY time
SELECT time, read_bytes, write_bytes FROM diskio WHERE name = 'vda' AND $__timeFilter(time) ORDER BY time
Set the disk panel’s unit to bytes and save. Three panels covering CPU, memory, and disk throughput is enough to prove the stack works, and it is the base most people build on afterwards. The same panel patterns apply if you later point Grafana at other sources, the way our PostgreSQL monitoring setup and Docker container monitoring guides do with Prometheus.

That is the stack working. What is left is stopping it from being served over plain HTTP.
Put Grafana behind Nginx and a certificate
Grafana on port 3000 over plain HTTP is fine on a laptop and wrong on anything reachable. Put Nginx in front of it:
sudo apt-get install nginx certbot python3-certbot-nginx
Create a virtual host for the name you will use:
sudo vim /etc/nginx/sites-available/grafana.conf
The second location block is the one people forget. Time-picker auto refresh is plain HTTP polling and survives without it, but Grafana Live runs over a WebSocket, so leaving it out costs you streaming panels and alert-state push, and fills the browser console with failed upgrade attempts:
server {
listen 80;
server_name grafana.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
location /api/live/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
Enable the site and reload. The second line removes the stock default vhost, so check that the box is not already serving something else from it before you run it:
sudo ln -sf /etc/nginx/sites-available/grafana.conf /etc/nginx/sites-enabled/grafana.conf
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
With an A record pointing at the server and port 80 reachable, certbot handles the certificate and rewrites the vhost for TLS in one pass. Put your own address in the contact flag, because Let’s Encrypt rejects registrations that use documentation domains and the run will fail outright under --non-interactive:
sudo certbot --nginx -d "${GRAFANA_DOMAIN}" --non-interactive --agree-tos --redirect -m [email protected]
One Grafana setting needs to match, otherwise password reset links and OAuth redirects point back at port 3000. Open the config:
sudo vim /etc/grafana/grafana.ini
Set the public URL under the server section, and bind Grafana to loopback while you are there. Without that, port 3000 stays open on every interface and anyone can bypass the certificate you just installed by connecting to it directly. Restart Grafana afterwards:
[server]
http_addr = 127.0.0.1
domain = grafana.example.com
root_url = https://grafana.example.com/
None of that takes effect until the service restarts, and until it does port 3000 is still listening on every interface:
sudo systemctl restart grafana-server
ss -lntp | grep 3000
Grafana takes a few seconds to rebind. When it comes back it should be on loopback only, matching what InfluxDB did earlier:
LISTEN 0 4096 127.0.0.1:3000 0.0.0.0:*
If your server sits on a private LAN with no public port 80, certbot’s DNS challenge is the way through instead. Our Grafana with Nginx and SSL guide walks that path in more detail.
The 72-hour query window you need to plan around
This is the part that surprises people a month in, when a dashboard set to “last 7 days” starts erroring instead of drawing.
InfluxDB 3 Core does not compact its Parquet files. It writes them in fixed time slices and caps how many any single query may open, and a query that needs more files than the cap returns an error rather than an empty result. Two defaults produce that cap: files are arranged in 10 minute slices (--gen1-duration), and a query may touch 432 of them (--query-file-limit). Multiply those out and you get 4,320 minutes, which is 72 hours. Treat that as a ceiling rather than a guarantee, because a slice holding data ingested across several periods can cost you more than one file, putting the real reach below 72 hours. Older data is still on disk and still counted by your retention period. It is simply outside what a single query can reach.
Both knobs are visible on the server itself, and both are listed in the InfluxDB 3 Core configuration reference:
influxdb3 serve --help-all | grep -A2 'gen1-duration'
The slice length is the smaller lever, because it only accepts 1m, 5m or 10m, and 10 minutes is already the widest:
--gen1-duration <DURATION> Duration for Parquet file arrangement [default: 10m]
[env: INFLUXDB3_GEN1_DURATION=]
Raising the file limit widens the window, at the cost of every query opening more files, and without compaction that cost grows with time rather than levelling off. It buys you room, not a fix.
There are three honest ways to live with this. Keep dashboards inside a three day window, which is genuinely enough for alerting and live troubleshooting. Downsample into a second database on a schedule, so long range panels read pre-aggregated rows instead of raw ones. Or move to InfluxDB 3 Enterprise, which adds the compaction that makes long range queries work.
Enterprise is worth a look for home labs specifically, because its Home license costs nothing, never expires, and is limited to two CPU cores on a single node for non-commercial use. Two caveats before you try it. The Enterprise package conflicts with the Core one, so installing it removes Core, takes its systemd unit with it, and stops your running database, though your data directory survives. And Enterprise refuses to start without a license, which it tries to obtain interactively, so under systemd it fails with a TTY error. Supplying the email alone is not enough, because the server then asks which license type you want and fails again. Both --license-email and --license-type home have to be set, in the config file or as environment variables.
If you are coming from the previous generation, note that InfluxDB 1 and 2 are both in maintenance now, and from 15 September 2026 the influxdb:latest Docker tag points at InfluxDB 3 Core. Anything pulling latest unpinned will land on a different database than it did last month. Our older InfluxDB v2 install guide still applies if you need to stay put for now, and if you already have data in a v2 bucket, moving that bucket across has a type trap in the export that will bite you.
Errors you will hit wiring this up
invalid TOML syntax on line 3 of the Telegraf config
Telegraf refuses to start and the unit reports a failed control process. The cause is nearly always a shell escaping accident while writing the drop-in, which leaves a stray backslash in front of the token variable. Print the file and check that the token line reads exactly token = "${INFLUX_TOKEN}" with no backslashes:
cat /etc/telegraf/telegraf.d/influxdb3.conf
Telegraf validates every file in the drop-in directory, so one bad file stops the whole agent rather than just that output.
Error during planning: table ‘public.iox.cpu’ not found
The database exists but the table does not, which means Telegraf has not written that input yet. Either the collector is failing to write at all, or you queried within the first collection interval. Check that the output plugin actually loaded before assuming the query is wrong.
No SQL statements were provided in the query string
This one appears when provisioning dashboards as JSON rather than through the UI. The InfluxDB data source in SQL mode reads the panel’s SQL from a rawSql field. Provisioning files copied from InfluxQL or Flux examples use query instead, which the SQL path ignores, so the request reaches the database with nothing in it. Rename the field and the panel works.
Adding a new database would exceed limit of 5 databases
An HTTP 422 on database creation, and it is a real ceiling in Core rather than a quota you can raise. The _internal database is excluded from the count, so you get five of your own. Consolidate by using tags to separate hosts or environments inside one database.
token name already exists, _admin
A 409 rather than a 401, returned when a second admin token creation is attempted. It means the instance was already claimed. If that was not you, treat the server as compromised, because whoever created the first token holds admin access to it.
panic: error getting work directory: stat .: permission denied
This comes from the Grafana CLI, and it is the reason the password reset above runs as root. Some guides suggest running it as the grafana user instead. Ubuntu creates home directories at mode 750 (HOME_MODE 0750 in /etc/login.defs), so no other account can traverse yours, and sudo -u grafana keeps your current directory. The CLI calls os.Getwd() from a package initialiser, which runs before main(), so it panics before it ever opens the database:
panic: error getting work directory: stat .: permission denied
goroutine 1 [running]:
github.com/grafana/grafana/pkg/api/static.init.0()
github.com/grafana/grafana/pkg/api/static/static.go:38 +0x85
The password is left untouched, so a reader who only reads the last line of output believes it worked. Either run the reset as root, or change into a directory the grafana user can read first. This is also why the acceptance test for a password reset is a successful login rather than a success message.
License management error: No interactive TTY detected
Only relevant if you try Enterprise. It starts, finds no license file in the object store, and tries to prompt for an email address, which fails under systemd. Setting only the email moves the failure one step along, to the license type prompt. Set both in /etc/influxdb3/influxdb3-enterprise.conf:
license-email="[email protected]"
license-type="home"
The equivalent environment variables are INFLUXDB3_LICENSE_EMAIL and INFLUXDB3_LICENSE_TYPE. Enterprise also needs a --cluster-id, which Core does not.