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 (ornpm
/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
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
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
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.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/
.
- Confirm the server is running and the PID from
-
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 intmux
/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
- Some binaries fork/daemonize after start. If
-
Insert parsing errors
- Use
client.insert(...)
or sendINSERT
as a single SQL string viaclient.exec(...)
. The@clickhouse/client
helper 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
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)