DEV Community

Mohammed Shaheem P
Mohammed Shaheem P

Posted on • Edited on

Setting up ClickHouse on macOS and Testing with Node.js

ClickHouse is an open-source columnar database built for high-performance analytical queries. This guide shows how I installed ClickHouse on macOS, ran it in the background using a lightweight nohup setup that stores logs and PID in hidden user folders, and tested it with a minimal Node.js + TypeScript example using @clickhouse/client.


Prerequisites

  • macOS (Intel or Apple Silicon)
  • Homebrew (optional but recommended for installing ClickHouse)
  • pnpm for Node package management (or npm / yarn if you prefer)
  • Basic knowledge of the terminal

1. Install ClickHouse (macOS)

The ClickHouse official macOS install docs: https://clickhouse.com/docs/install/macOS

The simplest approach via Homebrew:

brew install clickhouse 
Enter fullscreen mode Exit fullscreen mode

This installs the clickhouse binary. If you used another install method (downloaded the binary/cask), adjust paths below if necessary.


2. Run ClickHouse in the background (nohup)

This guide uses hidden directories in your home for logs and PID to keep things neat:

  • Logs: ~/.logs/clickhouse/
  • PID: ~/.nohup/clickhouse.pid

Create the folders (one-time)

mkdir -p ~/.logs/clickhouse mkdir -p ~/.nohup touch ~/.logs/clickhouse/server.log ~/.logs/clickhouse/server.err chmod 644 ~/.logs/clickhouse/server.* 
Enter fullscreen mode Exit fullscreen mode

Start ClickHouse in background

Use the clickhouse binary from your PATH to run it in background with nohup

# start backgrounded; this uses nohup and writes to the hidden logs and pid file BINARY="$(which clickhouse)" nohup "$BINARY" server > ~/.logs/clickhouse/server.log 2> ~/.logs/clickhouse/server.err & echo $! > ~/.nohup/clickhouse.pid 
Enter fullscreen mode Exit fullscreen mode

Alternative (handles daemonizing binaries better): use setsid which is more robust if the binary forks:

BINARY="$(which clickhouse)" setsid "$BINARY" server > ~/.logs/clickhouse/server.log 2> ~/.logs/clickhouse/server.err < /dev/null & echo $! > ~/.nohup/clickhouse.pid 
Enter fullscreen mode Exit fullscreen mode

Verify it started & is listening

# check pid/process cat ~/.nohup/clickhouse.pid ps -p "$(cat ~/.nohup/clickhouse.pid)" -o pid,ppid,user,args # see listening ports (8123 HTTP, 9000 native TCP) sudo lsof -iTCP -sTCP:LISTEN | egrep 'clickhouse|8123|9000|9004' || true # quick HTTP test (ClickHouse HTTP endpoint) curl -sS 'http://localhost:8123/' -d 'SELECT version()' || echo "no http response" 
Enter fullscreen mode Exit fullscreen mode

If curl returns the ClickHouse version string, the server is reachable on :8123.

Tail logs

tail -n 200 ~/.logs/clickhouse/server.log tail -n 200 ~/.logs/clickhouse/server.err # follow live tail -f ~/.logs/clickhouse/server.err 
Enter fullscreen mode Exit fullscreen mode

Stop the background server

if [ -f ~/.nohup/clickhouse.pid ]; then kill "$(cat ~/.nohup/clickhouse.pid)" && rm -f ~/.nohup/clickhouse.pid else echo "No PID file at ~/.nohup/clickhouse.pid" fi 
Enter fullscreen mode Exit fullscreen mode

If the process does not stop, find the true daemon PID (sometimes the parent exits and the real daemon has a different PID):

ps aux | grep clickhouse | egrep -v 'grep|egrep' 
Enter fullscreen mode Exit fullscreen mode

3. Node.js + TypeScript test app (pnpm)

We’ll create a minimal TypeScript script that:

  • connects to ClickHouse HTTP (http://localhost:8123),
  • creates a temporary Memory table,
  • inserts rows using client.insert(...),
  • queries and prints the rows,
  • drops the table.

Project setup

mkdir clickhouse-test && cd clickhouse-test pnpm init pnpm add @clickhouse/client pnpm add -D ts-node typescript @types/node 
Enter fullscreen mode Exit fullscreen mode

Add the start script in package.json:

"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "ts-node src/main.ts" } 
Enter fullscreen mode Exit fullscreen mode

Create tsconfig.json:

{ "compilerOptions": { "target": "ES2020", "module": "commonjs", "strict": true, "esModuleInterop": true, "skipLibCheck": true } } 
Enter fullscreen mode Exit fullscreen mode

Example: src/main.ts

import { createClient } from "@clickhouse/client"; async function main() { const client = createClient({ // use url instead of host (host is deprecated) url: "http://localhost:8123", username: "default", database: "default", application: "ts-pnpm-test", }); console.log("pinging ClickHouse..."); if (!(await client.ping())) { console.error( "❌ failed to ping ClickHouse. Is it running on http://localhost:8123 ?", ); process.exit(1); } console.log("✅ ping ok"); // create table (ephemeral Memory engine for testing) await client.exec({ query: ` CREATE TABLE IF NOT EXISTS test_ts_pnpm ( id UInt32, name String ) ENGINE = Memory `, }); // insert rows using the client.insert helper (preferred) await client.insert({ table: "test_ts_pnpm", values: [ { id: 1, name: "alice" }, { id: 2, name: "bob" }, ], // format optional; JSONEachRow is convenient for array-of-objects format: "JSONEachRow", }); // select rows back (explicit JSON output) const result = await client.query({ query: `SELECT id, name FROM test_ts_pnpm ORDER BY id`, format: "JSON", }); const json = (await result.json()) as { meta: unknown[]; data: Array<{ id: number; name: string }>; rows: number; }; console.log("rows:", json.rows); console.table(json.data); // cleanup await client.exec({ query: `DROP TABLE IF EXISTS test_ts_pnpm` }); await client.close(); console.log("done"); } main().catch((err) => { console.error(err); process.exit(1); }); 
Enter fullscreen mode Exit fullscreen mode

Run the test

pnpm start 
Enter fullscreen mode Exit fullscreen mode

Expected output:

pinging ClickHouse... ✅ ping ok rows: 2 ┌─────────┬────┬─────────┐ │ (index) │ id │ name │ ├─────────┼────┼─────────┤ │ 0 │ 1 │ 'alice' │ │ 1 │ 2 │ 'bob' │ └─────────┴────┴─────────┘ done 
Enter fullscreen mode Exit fullscreen mode

Troubleshooting

  • No response on port 8123

    • Confirm the server is running and the PID from ~/.nohup/clickhouse.pid is alive.
    • Try running ClickHouse in the foreground to observe startup errors:
    "$(which clickhouse)" server 2>&1 | tee ~/clickhouse-startup-debug.log tail -n 200 ~/clickhouse-startup-debug.log 
    • Search ClickHouse config for custom ports (http_port, tcp_port) under /opt/homebrew/etc/clickhouse-server/ or /etc/clickhouse-server/.
  • Log files empty after nohup

    • Some binaries fork/daemonize after start. If nohup logs remain empty, the real daemon may be writing to its own log directory (e.g., /opt/homebrew/var/log/clickhouse/).
    • Use setsid or run in tmux/screen to capture logs more reliably.
    • Also check macOS unified logs:
    log show --predicate 'process == "clickhouse" OR process == "clickhouse-server"' --last 1h --info | tail -n 200 
  • Insert parsing errors

    • Use client.insert(...) or send INSERT as a single SQL string via client.exec(...). The @clickhouse/client helper methods handle the correct transport format.
  • Permission or config errors

    • The foreground run will show file permission or config parsing errors. Fix config paths or file ownership accordingly.

Wrap-up

You now have:

  • ClickHouse installed on macOS,
  • a tidy nohup background setup that stores logs in ~/.logs/clickhouse/ and the pid in ~/.nohup/clickhouse.pid,
  • a minimal Node.js + TypeScript test that confirms read/write connectivity.

Note: The nohup setup will not automatically start ClickHouse after a macOS restart. After reboot, you would need to run the nohup command again to start the server.

If you’d like, I can:

  • provide a small launchd plist that uses your ~/.logs / ~/.nohup paths for auto-start on boot, or
  • create a one-file shell helper that manages start/stop/status for you.

Happy benchmarking! 🚀

Buy Me A Coffee

Top comments (0)