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)
- 
pnpmfor Node package management (ornpm/yarnif 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
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.* 
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
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
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"
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
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
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'
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 Memorytable,
- 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
Add the start script in package.json:
"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "start": "ts-node src/main.ts"
}
Create tsconfig.json:
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}
  
  
  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);
});
Run the test
pnpm start
Expected output:
pinging ClickHouse...
✅ ping ok
rows: 2
┌─────────┬────┬─────────┐
│ (index) │ id │ name    │
├─────────┼────┼─────────┤
│ 0       │ 1  │ 'alice' │
│ 1       │ 2  │ 'bob'   │
└─────────┴────┴─────────┘
done
Troubleshooting
- 
No response on port 8123 - Confirm the server is running and the PID from ~/.nohup/clickhouse.pidis 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/.
 
- Confirm the server is running and the PID from 
- 
Log files empty after nohup- Some binaries fork/daemonize after start. If nohuplogs remain empty, the real daemon may be writing to its own log directory (e.g.,/opt/homebrew/var/log/clickhouse/).
- Use setsidor run intmux/screento capture logs more reliably.
- Also check macOS unified logs:
 log show --predicate 'process == "clickhouse" OR process == "clickhouse-server"' --last 1h --info | tail -n 200
- Some binaries fork/daemonize after start. If 
- 
Insert parsing errors - Use client.insert(...)or sendINSERTas a single SQL string viaclient.exec(...). The@clickhouse/clienthelper methods handle the correct transport format.
 
- Use 
- 
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 nohupbackground 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 launchdplist that uses your~/.logs/~/.nohuppaths for auto-start on boot, or
- create a one-file shell helper that manages start/stop/status for you.
Happy benchmarking! 🚀
 

 
    
Top comments (0)