Skip to content
DuckDB UI: The Built-in Web Interface Explained

Click to use (opens in a new tab)

DuckDB UI: The Built-in Web Interface Explained

August 31, 2026 by Chat2DBChat2DB Team

For most of its life DuckDB was a CLI and a set of language bindings. If you wanted to browse a table, you typed SELECT * FROM t LIMIT 20 and squinted at ASCII box drawing. The DuckDB UI changes that: a local, browser-based notebook that ships with the database itself, launched with a single command and backed by the same in-process engine you already use.

This guide covers launching it, what each panel does, how it handles attached databases and remote files, and where it fits alongside a general-purpose SQL client.

What the DuckDB UI actually is

The UI is a DuckDB extension (ui) that starts a small HTTP server on your machine and serves a single-page app to your browser. Three things follow from that design:

  • It is local. The server binds to localhost. Your data is never uploaded anywhere — queries execute in the same process that serves the page.
  • It is the same engine. There is no separate query layer. Anything you can do in the CLI works identically here, including extensions you have already installed.
  • It is stateful per session. Notebooks, attached databases and query history persist in a local directory between launches.

Launching the UI

The fastest path, from DuckDB 1.2.1 onward:

duckdb -ui

That command installs the extension if needed, starts the server, and opens your default browser. To open a specific database file at the same time:

duckdb -ui analytics.duckdb

You can also start it from inside a session, which is useful when you already have temp tables or attachments set up:

INSTALL ui;
LOAD ui;
CALL start_ui();

start_ui() returns the URL it bound to. If you want the server without the browser popping open — handy in a remote shell or a container — use:

CALL start_ui_server();

To stop it again:

CALL stop_ui_server();

Changing the port

The default port is 4213. When that collides with something else, set it before loading:

SET ui_local_port = 4300;
CALL start_ui();

Or via the environment, which is what you want in a script:

DUCKDB_UI_LOCAL_PORT=4300 duckdb -ui

Running it from a container

Because the server binds to localhost by default, a container needs the bind address widened and the port published:

docker run -it --rm -p 4213:4213 \
  -v "$PWD/data:/data" \
  duckdb/duckdb:latest \
  duckdb -c "SET ui_remote_host='0.0.0.0'; INSTALL ui; LOAD ui; CALL start_ui_server();"

Only do this on a trusted network. The UI has no authentication layer of its own — it assumes it is talking to the person sitting at the machine.

The interface, panel by panel

The explorer sidebar

The left sidebar lists every database currently attached to the session, expanding into schemas, tables and columns with their types. This is a live view of the catalog, not a cached snapshot, so a CREATE TABLE in a cell shows up as soon as it commits.

Attaching more databases makes them appear immediately:

ATTACH 'warehouse.duckdb' AS warehouse;
ATTACH 'postgres://user:pass@localhost/app' AS pg (TYPE postgres);
ATTACH 'sqlite_file.db' AS legacy (TYPE sqlite);

That last pair is the most underrated part of the UI: you get a browser for a live PostgreSQL or SQLite database without installing anything else, and you can join across all three in one query:

SELECT p.customer_id,
       p.email,
       sum(w.amount) AS lifetime_value
FROM   pg.public.customers p
JOIN   warehouse.main.orders w ON w.customer_id = p.customer_id
GROUP  BY 1, 2
ORDER  BY lifetime_value DESC
LIMIT  25;

Notebook cells

The centre pane is a notebook. Each cell holds SQL, runs independently with Cmd/Ctrl+Enter, and keeps its result grid attached beneath it. Cells share one connection, so a temp table created in cell 3 is visible in cell 7:

-- cell 1
CREATE TEMP TABLE recent AS
SELECT * FROM read_parquet('s3://bucket/events/2026/08/*.parquet')
WHERE event_time >= now() - INTERVAL 7 DAY;
 
-- cell 2
SELECT event_type, count(*) AS n
FROM   recent
GROUP  BY 1
ORDER  BY n DESC;

The editor has schema-aware autocomplete driven by the attached catalog, so column names complete correctly across all attached databases.

The result grid

Results render in a virtualised grid that handles large outputs without freezing the tab. Clicking a column header gives you a summary: min, max, null count, approximate distinct count, and a small distribution sketch. Under the hood this is DuckDB's own SUMMARIZE, which you can also call directly:

SUMMARIZE SELECT * FROM read_csv('orders.csv');

That is often the first thing worth running against an unfamiliar file — it tells you the inferred types and where the nulls are hiding before you write any real logic.

Charts

Any result set with a sensible shape can be flipped to a chart from the result panel — line, bar, area or scatter — by picking the x and y columns. These are exploratory visuals, not a BI layer: they live in the notebook and are meant for a quick look at a trend before you go back to SQL.

Working with remote data

The UI inherits every reader the engine has, which is what makes it more than a table browser. Querying object storage needs a secret first:

CREATE SECRET aws_creds (
  TYPE s3,
  KEY_ID 'AKIA...',
  SECRET 'xxx',
  REGION 'us-east-1'
);
 
SELECT count(*) FROM 's3://my-bucket/events/*.parquet';

CREATE PERSISTENT SECRET instead stores it in your local secret store so it survives a restart — worth doing so you are not pasting keys into cells that end up in a saved notebook.

Reading straight from HTTPS or a local glob works with no setup at all:

SELECT * FROM 'https://example.com/data/prices.parquet' LIMIT 10;
SELECT * FROM read_csv('~/exports/*.csv', union_by_name = true);

Where the UI fits — and where it doesn't

The DuckDB UI is excellent at what it targets: exploring files and local analytical databases with zero setup, in a notebook that keeps your working context together. It is deliberately narrow, though.

It is a single-user, local tool. There is no shared connection registry, no team-level saved queries, no permissions model, and no support for the operational databases most teams also run — MySQL, SQL Server, Oracle, Redis, MongoDB. If your day involves moving between a DuckDB analysis and the production databases feeding it, you will end up wanting one client that speaks to all of them. That is the gap Chat2DB (opens in a new tab) fills: a single client across DuckDB, PostgreSQL, MySQL and the rest, with AI-assisted SQL generation and explanation on top; there is a browser version at app.chat2db.ai (opens in a new tab) if you would rather not install anything.

Use the DuckDB UI for the analysis loop. Use a general client for everything that crosses into the rest of your stack.

Troubleshooting

The browser opens a blank page. Almost always a port collision. Check what holds 4213 with lsof -i :4213, then relaunch on a different port with SET ui_local_port.

Extension "ui" not found. Your build predates the extension, or it cannot reach the extension repository. Confirm with SELECT version(); — you want 1.2.1 or newer — then INSTALL ui FROM community; if the core repo is unreachable.

Attached PostgreSQL tables do not appear. The postgres extension loads separately from ui. Run INSTALL postgres; LOAD postgres; before the ATTACH, and confirm with SELECT * FROM duckdb_databases();.

Notebooks vanished after an upgrade. They live in a local state directory keyed to the DuckDB version in some builds. Check ~/.duckdb/ before assuming the data is gone.

Saving and sharing work

Notebooks persist locally between sessions, keyed to the database you opened them against. That makes the UI a reasonable place to keep a running investigation, but it is worth being clear about what "sharing" means here: there is no server-side storage and no collaboration layer. If a colleague needs the analysis, you send them the SQL, or you materialise the result somewhere both of you can read.

The pragmatic pattern is to treat notebook cells as scratch and promote anything durable into a view or a table inside the database file itself:

CREATE OR REPLACE VIEW weekly_revenue AS
SELECT date_trunc('week', created_at) AS week,
       count(*)    AS orders,
       sum(amount) AS revenue
FROM   events
WHERE  event_type = 'purchase'
GROUP  BY 1;

Now the definition travels with the .duckdb file rather than living in one person's browser state. Exporting a result for someone else is a single statement:

COPY (SELECT * FROM weekly_revenue) TO 'weekly_revenue.parquet' (FORMAT parquet);
COPY (SELECT * FROM weekly_revenue) TO 'weekly_revenue.csv' (HEADER, DELIMITER ',');

You can also export the entire database to a directory of Parquet files plus a schema script, which is the closest thing to a portable snapshot:

EXPORT DATABASE 'snapshot_dir' (FORMAT parquet);
-- and to reload it elsewhere
IMPORT DATABASE 'snapshot_dir';

Wrapping up

duckdb -ui turns a CLI-first embedded database into something you can actually browse: a live catalog of every attached database, notebook cells sharing one session, column summaries a click away, and every remote reader DuckDB supports available from the same editor. It costs one command to try, and for local analytical work it removes most of the friction that used to push people toward a heavier tool.