ClickHouse in Docker: How I Stopped Worrying and Launched Analytics in 2 Minutes
Why Docker β and Everything Else Comes After
I remember the first time I set up ClickHouse on production. It took four hours to configure permissions, limits, manually edit configs, and restart systemd. A month later, a new developer joined, and we tried to replicate the environment on his machine β same old pitfalls.
Docker solved everything. Now I have a single folder with docker-compose.yml that I carry between projects. I spin up an analytics cluster in a minute, and when I need to tear it down β docker-compose down -v and it's clean. No system clutter.
Below are three ready-made scenarios I use in real projects (from a startup side project to betting analytics). All configs are tested on Docker Engine 24+.
Scenario 1. Quick Start: One Command to Test a Hypothesis
For local development and rapid prototyping, a single line suffices. But not just docker run clickhouse/clickhouse-server β let's add what makes ClickHouse useful: persistent storage and port mapping.
docker run -d \
--name clickhouse-dev \
--restart unless-stopped \
-p 8123:8123 \
-p 9000:9000 \
-v clickhouse-data:/var/lib/clickhouse \
-v clickhouse-logs:/var/log/clickhouse-server \
-e CLICKHOUSE_DB=analytics \
-e CLICKHOUSE_USER=developer \
-e CLICKHOUSE_PASSWORD=devpass123 \
-e CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1 \
clickhouse/clickhouse-server:latest
What's important here:
-v clickhouse-dataβ a named volume, not a bind mount. The difference: volumes are managed by Docker, don't get lost on restart, and perform better on macOS (important if you're on a MacBook β bind mounts are slow due to synchronization).CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1β enables access control. Without this variable, thedeveloperuser is created but cannot create new accounts. We learned this the hard way: in production, we had to dive into the container and editusers.xml.- Ports 9000 (native protocol) and 8123 (HTTP) β I always open both because half of the clients (DBeaver, TablePlus) work over HTTP, while applications use the native driver.
Check that it's up:
# HTTP interface β the simplest test
curl "http://localhost:8123/?query=SELECT+version()"
# Output: 24.8.2.3
Scenario 2. Docker Compose for Development (Single Node, Healthcheck)
When the project gets a bit more complex, I immediately switch to docker-compose.yml. This file I use on my laptops and dev servers:
version: '3.8'
services:
clickhouse:
image: clickhouse/clickhouse-server:latest
container_name: clickhouse-dev
hostname: clickhouse
ports:
- "8123:8123"
- "9000:9000"
- "9009:9009"
volumes:
- clickhouse-data:/var/lib/clickhouse
- clickhouse-logs:/var/log/clickhouse-server
- ./config/config.d:/etc/clickhouse-server/config.d
- ./config/users.d:/etc/clickhouse-server/users.d
environment:
CLICKHOUSE_DB: betting_analytics
CLICKHOUSE_USER: analyst
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-analyst123}
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
ulimits:
nofile:
soft: 262144
hard: 262144
nproc:
soft: 32768
hard: 32768
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8123/ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
networks:
- analytics-net
networks:
analytics-net:
driver: bridge
volumes:
clickhouse-data:
clickhouse-logs:
Why I added ulimits: In production, ClickHouse consumes up to 262144 open file descriptors. Without this, under heavy load it will crash with Too many open files. I once wasted three hours on this when the server started failing after 2 million rows.
Healthcheck via /ping: ClickHouse has a built-in /ping endpoint (returns "Ok." if alive). This is better than checking via SELECT 1 because it doesn't require authentication and doesn't write to logs.
Default variable: ${CLICKHOUSE_PASSWORD:-analyst123} β if not set in .env, the password will be analyst123. Don't forget the .env file for real secrets.
Scenario 3. Production-Like Setup: ClickHouse + Zookeeper for Replication
ClickHouse table replication requires ZooKeeper (or ClickHouse Keeper, but I start with the classic). I use this compose for testing fault tolerance:
version: '3.8'
services:
zookeeper:
image: confluentinc/cp-zookeeper:latest
container_name: zookeeper
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
ports:
- "2181:2181"
volumes:
- zookeeper-data:/var/lib/zookeeper
networks:
- ch-cluster
clickhouse-1:
image: clickhouse/clickhouse-server:latest
container_name: clickhouse-1
hostname: clickhouse-1
ports:
- "8123:8123"
- "9000:9000"
volumes:
- ch1-data:/var/lib/clickhouse
- ./config/replicated.xml:/etc/clickhouse-server/config.d/replicated.xml
environment:
CLICKHOUSE_DB: bets
CLICKHOUSE_USER: replicator
CLICKHOUSE_PASSWORD: rep_pass
CLICKHOUSE_SHARD: 1
CLICKHOUSE_REPLICA: 1
depends_on:
- zookeeper
ulimits:
nofile:
soft: 262144
hard: 262144
networks:
- ch-cluster
clickhouse-2:
image: clickhouse/clickhouse-server:latest
container_name: clickhouse-2
hostname: clickhouse-2
ports:
- "8124:8123" # second instance on a different port
- "9001:9000"
volumes:
- ch2-data:/var/lib/clickhouse
- ./config/replicated.xml:/etc/clickhouse-server/config.d/replicated.xml
environment:
CLICKHOUSE_DB: bets
CLICKHOUSE_USER: replicator
CLICKHOUSE_PASSWORD: rep_pass
CLICKHOUSE_SHARD: 1
CLICKHOUSE_REPLICA: 2
depends_on:
- zookeeper
ulimits:
nofile:
soft: 262144
hard: 262144
networks:
- ch-cluster
networks:
ch-cluster:
driver: bridge
volumes:
zookeeper-data:
ch1-data:
ch2-data:
And here's the content of config/replicated.xml (mounted into both containers):
<clickhouse>
<zookeeper>
<node>
<host>zookeeper</host>
<port>2181</port>
</node>
</zookeeper>
<remote_servers>
<replicated_cluster>
<shard>
<replica>
<host>clickhouse-1</host>
<port>9000</port>
</replica>
<replica>
<host>clickhouse-2</host>
<port>9000</port>
</replica>
</shard>
</replicated_cluster>
</remote_servers>
<macros>
<shard>1</shard>
<replica>${CLICKHOUSE_REPLICA}</replica>
</macros>
</clickhouse>
When this is really needed: In a production casino project, we lost data because we kept everything on a single node. After that, I always spin up at least two replicated containers for testing. The price difference is two containers instead of one, but sleeping well is priceless.
Scenario 4. Full Gambling Setup: ClickHouse + Kafka + Redis
For real-time betting analytics, I need streaming (Kafka) and caching (Redis). I use this compose for local pipeline debugging:
version: '3.8'
services:
zookeeper-kafka:
image: confluentinc/cp-zookeeper:latest
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
ports:
- "2181:2181"
kafka:
image: confluentinc/cp-kafka:latest
depends_on:
- zookeeper-kafka
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper-kafka:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
ports:
- "9092:9092"
redis:
image: redis:7-alpine
container_name: redis-cache
ports:
- "6379:6379"
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-cachepass}
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
clickhouse:
image: clickhouse/clickhouse-server:latest
container_name: clickhouse-betting
ports:
- "8123:8123"
- "9000:9000"
volumes:
- clickhouse-betting-data:/var/lib/clickhouse
- ./clickhouse-kafka.xml:/etc/clickhouse-server/config.d/kafka.xml
environment:
CLICKHOUSE_DB: betting
CLICKHOUSE_USER: streamer
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PW:-stream123}
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
ulimits:
nofile:
soft: 262144
hard: 262144
depends_on:
- kafka
- redis
kafka-connector:
image: clickhouse/clickhouse-kafka-connect:latest
container_name: kafka-connector
depends_on:
- kafka
- clickhouse
environment:
CONNECT_BOOTSTRAP_SERVERS: kafka:9092
CONNECT_GROUP_ID: clickhouse-group
CONNECT_CONFIG_STORAGE_TOPIC: connect-configs
CONNECT_OFFSET_STORAGE_TOPIC: connect-offsets
CONNECT_STATUS_STORAGE_TOPIC: connect-status
CONNECT_KEY_CONVERTER: org.apache.kafka.connect.storage.StringConverter
CONNECT_VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter
ports:
- "8083:8083"
volumes:
redis-data:
clickhouse-betting-data:
How to use this in application code:
# Example in Python: read a bet from Redis (cache), write to ClickHouse
import redis
from kafka import KafkaProducer
import json
r = redis.Redis(host='localhost', port=6379, password='cachepass', decode_responses=True)
producer = KafkaProducer(bootstrap_servers='localhost:9092', value_serializer=lambda v: json.dumps(v).encode())
# Duplicate check (fraud detection)
bet_id = "bet_12345"
if r.setnx(bet_id, "processed"):
bet_event = {"user_id": 101, "amount": 500, "odds": 2.1}
producer.send('bets-stream', bet_event)
else:
print(f"Duplicate bet {bet_id} blocked")
How to Mount Your Own config.xml Without Breaking Everything
A mistake I made five times: mounting a full config.xml, only to find that a new ClickHouse version added mandatory sections. The container would crash with Config has no <logger>.
The right approach: place only overrides in config.d/. Here's a structure that works:
docker-clickhouse/
βββ docker-compose.yml
βββ .env
βββ config/
β βββ config.d/
β β βββ memory.xml
β β βββ networks.xml
β β βββ query-log.xml
β βββ users.d/
β βββ profiles.xml
Example config/config.d/memory.xml:
<clickhouse>
<max_server_memory_usage>0.75</max_server_memory_usage>
<max_memory_usage_for_all_queries>0</max_memory_usage_for_all_queries>
<background_pool_size>16</background_pool_size>
</clickhouse>
Example config/users.d/profiles.xml:
<clickhouse>
<profiles>
<default>
<max_memory_usage>10000000000</max_memory_usage>
<timeout_before_checking_execution_speed>0</timeout_before_checking_execution_speed>
</default>
<analyst>
<readonly>1</readonly>
<max_execution_time>300</max_execution_time>
</analyst>
</profiles>
</clickhouse>
Working with clickhouse-client Inside a Container
Jumping into a container for quick queries is normal. But not via docker exec -it bash β do it directly:
# Execute a query
docker exec -it clickhouse-dev clickhouse-client --query "SELECT count() FROM system.tables"
# Interactive mode
docker exec -it clickhouse-dev clickhouse-client
# With password
docker exec -it clickhouse-dev clickhouse-client --password devpass123
My lifehack: Add an alias to ~/.bashrc:
alias ch-cli='docker exec -it clickhouse-dev clickhouse-client'
After that, just type ch-cli and work as if it's a local database.
Environment Variables: What Actually Works
The official image doesn't support all the variables promised on forums. Here are the tested ones:
| Variable | Purpose | Example |
|---|---|---|
CLICKHOUSE_DB |
Default database name | analytics |
CLICKHOUSE_USER |
Admin user | prod_user |
CLICKHOUSE_PASSWORD |
Password | strongpass |
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT |
Enable RBAC (1/0) | 1 |
What DOES NOT WORK: CLICKHOUSE_HTTP_PORT, CLICKHOUSE_TCP_PORT β the entrypoint ignores them. Change ports via ports: in compose or by mounting a config.
Health Check: My Complete Checklist
After starting any compose, I run:
# 1. HTTP ping (should return "Ok.")
curl http://localhost:8123/ping
# 2. Version via HTTP
curl "http://localhost:8123/?query=SELECT+version()"
# 3. Create test table
docker exec -it clickhouse-dev clickhouse-client --query "CREATE TABLE test.t (id UInt64) ENGINE = MergeTree ORDER BY id"
# 4. Insert and select
docker exec -it clickhouse-dev clickhouse-client --query "INSERT INTO test.t SELECT number FROM numbers(1000)"
docker exec -it clickhouse-dev clickhouse-client --query "SELECT count() FROM test.t"
# 5. HTTP with authentication (if password set)
curl -u developer:devpass123 "http://localhost:8123/?query=SELECT+user()"
What to Do If It Doesn't Work β Common Docker Errors
Error: Code: 210. DB::NetException: Connection refused
Solution: The container hasn't started yet. Add depends_on and healthcheck, or do sleep 5 in scripts.
Error: Cannot create directory /var/lib/clickhouse: Permission denied
Solution: On a host with SELinux, add :Z to the volume: -v ./data:/var/lib/clickhouse:Z. Or use named volumes.
Error: Max connections limit reached
Solution: Increase in config: <max_connections>4096</max_connections> and restart.
Container memory eating up the host
Solution: Limit via Docker:
docker update --memory=4g --memory-swap=4g clickhouse-dev
Or in compose:
deploy:
resources:
limits:
memory: 4G
Conclusion: When to Use Docker and When Not
Docker is ideal for ClickHouse in dev, staging, and small production environments. But if you have a cluster of 10+ nodes with 100 TB of data β native packages without extra layers are better.
For now, take my gambling setup compose, change the passwords, and start counting bets in real time.
All configs taken from real projects. Names changed, pitfalls remain.
β Previous: Installing ClickHouse on Ubuntu/Debian: A Step-by-Step Guide from Someone Who Got Burned by Wrong Permissions
β Next: ClickHouse Client: How I Befriended the Console and HTTP API in a Gambling Project
β Editorial Team
No comments yet.