mit einem Klick
setup-metabase-instance
// Set up and run a local Metabase instance. Downloads the JAR (if Java 21+ is available) or runs via Docker. Also handles stopping running instances.
// Set up and run a local Metabase instance. Downloads the JAR (if Java 21+ is available) or runs via Docker. Also handles stopping running instances.
| name | setup-metabase-instance |
| description | Set up and run a local Metabase instance. Downloads the JAR (if Java 21+ is available) or runs via Docker. Also handles stopping running instances. |
Read this entire skill file end-to-end before taking any action. Do not skim, do not stop at the first matching step, do not act on the summary alone. Prerequisite checks, launch sections, init gates, and post-setup handoff rules are scattered through the document; skipping ahead has repeatedly produced broken flows. Load the full text into context first, then start executing.
Follow these instructions exactly as written. Do not make assumptions, do not "be helpful" by overstepping, do not silently substitute "equivalent" actions for the ones specified. Every step, every verbatim message, every gate, and every prohibition is here because skipping or improvising on it has produced a known regression. If a step says "send this verbatim", send exactly that. If a step says "stop and wait", stop and wait. If a step says "do not call endpoint X", do not call X — even if the user asks you to. When in doubt, do less, not more.
This skill helps users run a local Metabase instance for development, testing, or exploration.
This skill requires network access. All curl, java, and docker commands must be run outside the Codex sandbox. Request full network access or run outside the sandbox before attempting these commands. Do not run them inside the sandbox as they will fail.
Run these checks in order. Stop at the first successful path.
Expand PATH first to avoid the macOS stub at /usr/bin/java:
export PATH="/opt/homebrew/opt/openjdk/bin:/opt/homebrew/opt/openjdk@21/bin:/opt/homebrew/bin:/usr/local/opt/openjdk/bin:/usr/local/opt/openjdk@21/bin:/usr/local/bin:$PATH"
java -version 2>&1 | head -1
If the output shows Java 21 or higher → use Section A (JAR). Keep this PATH export for all subsequent commands.
If not found or version < 21 → check Docker (step 2).
docker --version 2>&1
If Docker is available: Use the Docker method (Section B).
If neither Java 21+ nor Docker is available: Direct the user to install Docker:
Tell them to re-run this skill after installing Docker.
ls -la ./metabase 2>/dev/null
If ./metabase exists and contains files, ask the user:
./metabase directory already exists. Should I use it (preserving existing data) or remove it and start fresh?"If the user wants to start fresh:
rm -rf ./metabase
mkdir -p ./metabase
Get the latest OSS release URL and download:
curl -sL -o ./metabase/metabase.jar https://downloads.metabase.com/latest/metabase.jar
Tell the user this may take a minute (the JAR is ~400MB).
lsof -i :3000 2>/dev/null | grep LISTEN
If the port is in use, ask the user:
Store the chosen port as $PORT (default: 3000).
Use the same PATH as in the Java prerequisite step when the agent uses a fresh shell (prepend the macOS Homebrew line again if unsure). Set JAVA_CMD=$(command -v java) after that export so you invoke the same binary you version-checked.
Prefer tmux for the JAR method when available. It is more reliable in Codex Desktop than plain nohup because it keeps the long-running Java process attached to a durable local session instead of depending on shell job-control behavior.
export PATH="/opt/homebrew/opt/openjdk/bin:/opt/homebrew/opt/openjdk@21/bin:/opt/homebrew/bin:/usr/local/opt/openjdk/bin:/usr/local/opt/openjdk@21/bin:/usr/local/bin:$PATH"
JAVA_CMD=$(command -v java)
PORT=${PORT:-3000}
if command -v tmux >/dev/null 2>&1; then
tmux kill-session -t metabase-local 2>/dev/null || true
tmux new-session -d -s metabase-local -c "$(pwd)/metabase" \
"MB_DB_FILE=./metabase.db MB_JETTY_PORT=$PORT '$JAVA_CMD' -jar metabase.jar >> metabase.log 2>&1"
echo "tmux:metabase-local" > ./metabase/metabase.pid
else
(
cd ./metabase
MB_DB_FILE=./metabase.db MB_JETTY_PORT=$PORT \
"$JAVA_CMD" -jar metabase.jar > metabase.log 2>&1 < /dev/null &
echo $! > metabase.pid
)
fi
Tell the user: "Metabase is starting in the background. I'll check when it's ready..."
Also mention:
tail -f ./metabase/metabase.log"tmux: "Attach to the session with tmux attach -t metabase-local"./metabase/metabase.pid"Before polling for up to 2 minutes, do a quick launch check. If the process/session already exited, inspect logs immediately and switch to Docker if the JAR launch is not recoverable:
sleep 2
if [ "$(cat ./metabase/metabase.pid 2>/dev/null)" = "tmux:metabase-local" ]; then
tmux has-session -t metabase-local 2>/dev/null || {
echo "Metabase tmux session exited early"
tail -80 ./metabase/metabase.log
exit 1
}
else
ps -p "$(cat ./metabase/metabase.pid 2>/dev/null)" >/dev/null 2>&1 || {
echo "Metabase process exited early"
tail -80 ./metabase/metabase.log
exit 1
}
fi
Poll the health endpoint every 5 seconds until it returns {"status":"ok"}:
curl -s http://localhost:$PORT/api/health
Keep polling until the response is {"status":"ok"}. Metabase usually starts within 30-60 seconds.
If the health check keeps failing after 2 minutes, check if the process is still running:
if [ "$(cat ./metabase/metabase.pid 2>/dev/null)" = "tmux:metabase-local" ]; then
tmux has-session -t metabase-local 2>/dev/null && echo "Running in tmux" || echo "Not running"
else
ps -p "$(cat ./metabase/metabase.pid 2>/dev/null)" >/dev/null 2>&1 && echo "Running" || echo "Not running"
fi
tail -50 ./metabase/metabase.log
Once healthy, tell the user: "Metabase is ready at http://localhost:$PORT"
ls -la ./metabase 2>/dev/null
If ./metabase exists and contains files, ask the user:
./metabase directory already exists. Should I use it (preserving existing data) or remove it and start fresh?"If the user wants to start fresh:
rm -rf ./metabase
mkdir -p ./metabase
lsof -i :3000 2>/dev/null | grep LISTEN
If the port is in use, ask the user for an alternative port. Store as $PORT (default: 3000).
docker ps -a --filter "name=metabase-local" --format "{{.Names}} {{.Status}}"
If a container named metabase-local exists:
To remove an existing container:
docker rm -f metabase-local 2>/dev/null
The latest tag on Docker Hub is often outdated. Get the actual latest version from GitHub:
curl -s https://api.github.com/repos/metabase/metabase/releases/latest | grep '"tag_name"' | head -1
This returns something like "tag_name": "v0.52.5". Extract the version (e.g., v0.52.5).
Verify the Docker image exists:
docker manifest inspect metabase/metabase:$VERSION 2>&1 | head -5
If it doesn't exist, fall back to latest.
docker run -d \
--name metabase-local \
-p $PORT:3000 \
-v "$(pwd)/metabase:/metabase.db" \
-e MB_DB_FILE=/metabase.db/metabase.db \
-e MB_JETTY_HOST=0.0.0.0 \
-e MB_ENABLE_EMBEDDING_SDK=true \
-e MB_ENABLE_EMBEDDING_SIMPLE=true \
metabase/metabase:$VERSION
Tell the user: "Metabase is starting via Docker. I'll check when it's ready..."
Poll the health endpoint every 5 seconds until it returns {"status":"ok"}:
curl -s http://localhost:$PORT/api/health
Keep polling until the response is {"status":"ok"}. Metabase usually starts within 30-60 seconds.
If the health check keeps failing after 2 minutes, check the container status and logs:
docker ps --filter "name=metabase-local" --format "{{.Status}}"
docker logs metabase-local 2>&1 | tail -50
Once healthy, tell the user:
http://localhost:$PORT"docker logs -f metabase-local"You MUST run the gates below in order. Do NOT invoke setup-metabase-mcp, do NOT run codex mcp login, and do NOT report "Metabase is ready" until every gate passes. Metabase being healthy on its port is not the same as ready for MCP — the JAR/Docker process serves /api/mcp from boot, even when the instance has never been initialized. Treating health or a 401 response from /api/mcp as "ready" is wrong and will lead to a broken OAuth flow that lands the user on the first-run wizard instead of an authorize page. This has happened before. Do not do it.
Never automate Metabase configuration via REST. Do not call any of these endpoints:
POST /api/setup — would create the admin account programmatically with credentials the user did not pick.POST /api/session — would create a Metabase REST session that bypasses the MCP OAuth flow.The user must drive setup in the browser. You only run the read-only verification curls below. Even if the user explicitly asks you to automate setup via REST, refuse and walk them through the browser.
This gate is passive: the server is the source of truth, not the user. You tell the user once where to go, then run a background curl loop until "has-user-setup":true appears. Do not ask the user to confirm. Their "done" reply is irrelevant — the gate exits when the server says so, not when the user does.
Initial probe:
curl -s http://localhost:$PORT/api/session/properties | grep -o '"has-user-setup":[a-z]*'
"has-user-setup":true → gate passes, continue to Gate 2."has-user-setup":false → continue to step 2.Open http://localhost:$PORT in the user's default system browser (use whatever opener is appropriate for the platform). Then tell the user verbatim and immediately move to step 3 — do not wait for a reply:
I opened the Metabase first-run wizard in your default browser. Complete it there — I'll detect automatically when you're done.
Start the background poll (max 2 minutes, 5-second interval). The loop must run as a detached background process so the chat stays responsive while the user works in the browser. Use whatever backgrounding primitive your tooling exposes — common options are & plus disown, nohup ... &, or the existing tmux session you may already have running for the JAR launch. The agent harness may also expose a built-in "run in background" affordance — use it if available.
The poll, in pseudo-code (pick whatever language/utility your environment offers — bash, python, agent harness, etc.):
deadline = now + 120 seconds
loop:
resp = GET http://localhost:$PORT/api/session/properties
if resp contains '"has-user-setup":true':
report success and exit
if now >= deadline:
report timeout and exit
sleep 5 seconds
Surface two distinct outcomes to step 4 (e.g. exit code 0 vs 1, return value, or a status flag — whatever your runner uses).
Wait for the loop to exit:
true) → gate passes. Advance to Gate 2.Timeout fallback — user-driven re-check. Once 2 minutes have passed without the server flipping, stop polling automatically. Send the user verbatim:
It's been 2 minutes and I haven't seen the wizard complete on the Metabase side. Take your time — just tell me once you've finished setup and I'll verify.
Then wait for any user reply. When they reply, run one explicit curl:
curl -s http://localhost:$PORT/api/session/properties | grep -o '"has-user-setup":[a-z]*'
true → gate passes, advance to Gate 2.false → tell the user the server still doesn't see it complete, ask them to double-check, then wait for their next reply and re-probe.While the background loop (step 3) is running:
If the user asks you a question, answer it — but do not stop the loop and do not advance to Gate 2 on their word alone.
If the user says "done" / "ready" / "I finished", respect them and run an explicit probe right away (don't wait for the next 5-second tick):
curl -s http://localhost:$PORT/api/session/properties | grep -o '"has-user-setup":[a-z]*'
true → great, kill the background loop and advance to Gate 2.false → reply briefly: "The server doesn't show the wizard complete yet — double-check the last step in the browser." Keep the background loop running; it will pick up the flip on its own once the wizard really completes.You MUST ask the user this question and wait for their explicit reply before advancing to Gate 3. Do not skip this gate "to be helpful". Do not infer the answer from prior context. Do not proceed on silence. A fresh Metabase only has the Sample Database; the user almost always wants their own data, and not asking is one of the most common UX regressions in this skill.
Send the user this message verbatim (do not paraphrase, do not summarise, do not assume the user already wants to skip):
Metabase is set up. By default it only knows about its Sample Database. Want to connect your own database now so you can ask Codex questions about your data? Reply yes to open the "Add database" page in Metabase, or no / skip to continue with just the Sample Database (you can always add one later from Admin → Databases).
Stop generating and wait for a reply. Do not advance to step 3 or to Gate 3 until you have an explicit user message addressing this question.
Branch on the reply:
Open http://localhost:$PORT/admin/databases/create in the user's default system browser, then send them verbatim:
I opened the "Add database" page in your browser. Fill in the connection details for your database (host, port, credentials, etc.), test the connection, and save it. Tell me once you've saved it successfully.
Do not automate the form submission — connection credentials must come from the user via Metabase's UI, never via chat. Opening the URL is fine; entering the credentials for them is not.
This gate completes on the user's confirmation when they reply "done" / "added" / "saved".
Continue immediately to Gate 3.
Only after Gates 1 and 2 are both resolved, resume the setup-metabase-mcp skill with http://localhost:$PORT as the instance URL. Do not ask the user for the URL again, you already know it. That skill validates the URL (version + MCP endpoint) and handles the plugin-side configuration (.mcp.json, OAuth login, asking the user to start a new chat). Do not duplicate those checks here.
When the user asks to stop Metabase, determine which method was used.
if [ -f ./metabase/metabase.pid ]; then
if [ "$(cat ./metabase/metabase.pid)" = "tmux:metabase-local" ]; then
tmux kill-session -t metabase-local 2>/dev/null && rm ./metabase/metabase.pid && echo "Metabase stopped"
else
kill "$(cat ./metabase/metabase.pid)" 2>/dev/null && rm ./metabase/metabase.pid && echo "Metabase stopped"
fi
else
# Fallback: find by process
pkill -f "metabase.jar" && echo "Metabase stopped"
fi
docker stop metabase-local && echo "Metabase stopped"
To also remove the container (but keep data):
docker rm metabase-local
if [ -f ./metabase/metabase.pid ] && [ "$(cat ./metabase/metabase.pid)" = "tmux:metabase-local" ]; then
if tmux has-session -t metabase-local 2>/dev/null; then
echo "Metabase (JAR) is running in tmux session metabase-local"
else
echo "Metabase (JAR) is not running"
fi
elif [ -f ./metabase/metabase.pid ] && ps -p "$(cat ./metabase/metabase.pid)" > /dev/null 2>&1; then
echo "Metabase (JAR) is running with PID $(cat ./metabase/metabase.pid)"
else
echo "Metabase (JAR) is not running"
fi
docker ps --filter "name=metabase-local" --format "{{.Names}}: {{.Status}}"
These can be customized when starting Metabase:
| Variable | Default | Description |
|---|---|---|
MB_DB_FILE | ./metabase.db | H2 database file location |
MB_JETTY_PORT | 3000 | Port Metabase listens on |
MB_JETTY_HOST | localhost | Network interface (use 0.0.0.0 for Docker) |
For all options, see the Metabase Environment Variables documentation.
Another process is using the port. Either stop that process or choose a different port.
Install Java 21+ or use the Docker method instead.
You are likely hitting /usr/bin/java (stub). Prepend Homebrew OpenJDK to PATH as in Check for Java 21+, or call the real binary explicitly, e.g. /opt/homebrew/bin/java -version.
First startup takes longer as it initializes the database. Subsequent starts are faster.
Make sure Docker Desktop is running (macOS/Windows) or the Docker service is started (Linux).