Back to Home

ClickHouse in Docker: launch analytics in 2 minutes

Guide to running ClickHouse in Docker with four ready-made scenarios: basic docker run, docker-compose for development with healthcheck, cluster with ZooKeeper for replication, full ClickHouse + Kafka + Redis stack for gaming analytics. Environment variables, mounting custom configs, typical errors and their solutions are explained.

ClickHouse in Docker: 4 ready-made scenarios with compose files
Advertisement 728x90

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+.

Google AdInline article slot

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, the developer user is created but cannot create new accounts. We learned this the hard way: in production, we had to dive into the container and edit users.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:

Google AdInline article slot
# 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.

Google AdInline article slot

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:
β†’ Next: ClickHouse Client: How I Befriended the Console and HTTP API in a Gambling Project

β€” Editorial Team

Advertisement 728x90

Read Next