DEV Community

Mohammed Shaheem P
Mohammed Shaheem P

Posted 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! 🚀

Top comments (0)