Docker Deployment¶
Running RockLake in Docker provides process isolation, reproducible environments, and seamless integration with container orchestration platforms. Because RockLake is a single stateless binary with no local storage requirements, it is an ideal containerization candidate — the container needs no volumes, no init systems, and no sidecar processes. The official Docker image is minimal (based on distroless/static) and contains only the RockLake binary plus root CA certificates for TLS to object storage.
This page covers everything from a one-line quick start to production-ready Docker Compose stacks, custom image builds, security hardening, and operational patterns.
Official Image¶
The official RockLake container image is published to GitHub Container Registry:
Image characteristics:
- Base:
gcr.io/distroless/static(no shell, no package manager, minimal attack surface) - Size: ~12 MB compressed
- User: Non-root (UID 65534,
nobody) - Entrypoint:
/usr/local/bin/rocklake - Exposed port: 5432
Quick Start¶
The simplest possible Docker deployment — connect RockLake to an S3 bucket:
docker run -d \
--name rocklake \
-p 5432:5432 \
-e AWS_REGION=us-east-1 \
-e AWS_ACCESS_KEY_ID=your-key \
-e AWS_SECRET_ACCESS_KEY=your-secret \
ghcr.io/rocklake/rocklake:latest \
--catalog s3://my-bucket/catalog/ \
--bind 0.0.0.0:5432
Verify it is running:
docker logs rocklake
# INFO RockLake v0.8.0 starting
# INFO Storage: s3://my-bucket/catalog/
# INFO Listening on 0.0.0.0:5432
# Connect with DuckDB
duckdb -c "ATTACH 'ducklake:host=localhost;port=5432' AS lake;"
Docker Compose: Development Stack¶
For local development, use Docker Compose to run RockLake with MinIO (S3-compatible local storage). This gives you a fully functional lakehouse environment without any cloud credentials:
services:
minio:
image: minio/minio:latest
command: server /data --console-address ":9001"
ports:
- "9000:9000" # S3 API
- "9001:9001" # Web console
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
volumes:
- minio-data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 5s
timeout: 3s
retries: 5
minio-init:
image: minio/mc:latest
depends_on:
minio:
condition: service_healthy
entrypoint: >
/bin/sh -c "
mc alias set local http://minio:9000 minioadmin minioadmin;
mc mb local/rocklake-catalog --ignore-existing;
exit 0;
"
rocklake:
image: ghcr.io/rocklake/rocklake:latest
ports:
- "5432:5432"
environment:
AWS_ACCESS_KEY_ID: minioadmin
AWS_SECRET_ACCESS_KEY: minioadmin
AWS_ENDPOINT_URL: http://minio:9000
AWS_REGION: us-east-1
command: ["--storage", "s3://rocklake-catalog/", "--bind", "0.0.0.0:5432"]
depends_on:
minio-init:
condition: service_completed_successfully
healthcheck:
test: ["CMD-SHELL", "pg_isready -h localhost -p 5432 || exit 1"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
volumes:
minio-data:
Start the stack:
docker compose up -d
# Wait for health checks to pass
docker compose ps
# Connect with DuckDB
duckdb -c "ATTACH 'ducklake:host=localhost;port=5432' AS dev_lake;"
The MinIO web console is available at http://localhost:9001 (login: minioadmin/minioadmin) where you can browse the raw object storage contents.
Docker Compose: Production Stack¶
A production-ready Compose file with TLS, authentication, JSON logging, and resource limits:
services:
rocklake:
image: ghcr.io/rocklake/rocklake:0.8.0
ports:
- "5432:5432"
environment:
ROCKLAKE_STORAGE: s3://production-lakehouse/catalog/
ROCKLAKE_BIND: 0.0.0.0:5432
ROCKLAKE_AUTH_USER: ducklake
ROCKLAKE_PASSWORD: ${ROCKLAKE_PASSWORD}
ROCKLAKE_LOG_FORMAT: json
ROCKLAKE_LOG_LEVEL: info
ROCKLAKE_MAX_SESSIONS: 100
ROCKLAKE_TLS_CERT: /etc/rocklake/tls/cert.pem
ROCKLAKE_TLS_KEY: /etc/rocklake/tls/key.pem
AWS_REGION: us-east-1
volumes:
- ./tls:/etc/rocklake/tls:ro
deploy:
resources:
limits:
cpus: "2.0"
memory: 512M
reservations:
cpus: "0.5"
memory: 128M
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD-SHELL", "pg_isready -h localhost -p 5432 || exit 1"]
interval: 10s
timeout: 5s
retries: 3
start_period: 15s
Start with an environment file:
# Create .env with secrets
echo "ROCKLAKE_PASSWORD=$(openssl rand -base64 32)" > .env
docker compose -f docker-compose.prod.yml up -d
Docker Compose: Read Replica Fleet¶
Run one writer and multiple read replicas from the same storage:
services:
writer:
image: ghcr.io/rocklake/rocklake:0.8.0
ports:
- "5432:5432"
environment:
ROCKLAKE_STORAGE: s3://my-bucket/catalog/
ROCKLAKE_BIND: 0.0.0.0:5432
AWS_REGION: us-east-1
restart: unless-stopped
reader-1:
image: ghcr.io/rocklake/rocklake:0.8.0
ports:
- "5433:5432"
environment:
ROCKLAKE_STORAGE: s3://my-bucket/catalog/
ROCKLAKE_BIND: 0.0.0.0:5432
ROCKLAKE_READ_ONLY: "true"
AWS_REGION: us-east-1
restart: unless-stopped
reader-2:
image: ghcr.io/rocklake/rocklake:0.8.0
ports:
- "5434:5432"
environment:
ROCKLAKE_STORAGE: s3://my-bucket/catalog/
ROCKLAKE_BIND: 0.0.0.0:5432
ROCKLAKE_READ_ONLY: "true"
AWS_REGION: us-east-1
restart: unless-stopped
Read replicas see committed changes within seconds (bounded by object storage consistency — typically <1 second on S3).
Building a Custom Image¶
If you need custom CA certificates, additional tooling, or want to build from source:
Minimal Production Image¶
FROM rust:1.80-bookworm AS builder
WORKDIR /src
COPY . .
RUN cargo build --release --bin rocklake
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /src/target/release/rocklake /usr/local/bin/rocklake
EXPOSE 5432
USER nonroot:nonroot
ENTRYPOINT ["rocklake"]
Image with Custom CA Certificates¶
For environments with internal certificate authorities (corporate proxies, private S3-compatible stores):
FROM rust:1.80-bookworm AS builder
WORKDIR /src
COPY . .
RUN cargo build --release --bin rocklake
FROM debian:bookworm-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates && \
rm -rf /var/lib/apt/lists/*
# Add custom CA
COPY internal-ca.pem /usr/local/share/ca-certificates/internal-ca.crt
RUN update-ca-certificates
# Create non-root user
RUN useradd --system --no-create-home rocklake
USER rocklake
COPY --from=builder /src/target/release/rocklake /usr/local/bin/rocklake
EXPOSE 5432
ENTRYPOINT ["rocklake"]
Build and push:
Health Checks¶
RockLake accepts PostgreSQL protocol connections, so standard PostgreSQL health check tools work:
healthcheck:
test: ["CMD-SHELL", "pg_isready -h localhost -p 5432 || exit 1"]
interval: 10s
timeout: 5s
retries: 3
start_period: 15s
If pg_isready is not available in your image (e.g., distroless), use a TCP check:
healthcheck:
test: ["CMD-SHELL", "cat < /dev/tcp/localhost/5432 || exit 1"]
interval: 10s
timeout: 5s
retries: 3
Or install a small binary health checker during the build.
Graceful Shutdown¶
RockLake handles SIGTERM (sent by docker stop) gracefully:
- Stops accepting new connections
- Waits for in-flight transactions to complete (up to 30 seconds)
- Flushes buffered state to object storage
- Exits with code 0
Docker's default stop timeout is 10 seconds. For production, increase it:
This ensures clean shutdown even with long-running transactions.
Security Hardening¶
Run as Non-Root¶
The official image already runs as non-root (nobody). For custom images, always add:
Read-Only Filesystem¶
RockLake does not write to local disk, so you can mount the filesystem read-only:
No Capabilities¶
Drop all Linux capabilities since RockLake needs none:
Secret Management¶
Never embed credentials in the image or Compose file. Use Docker secrets or external secret managers:
services:
rocklake:
secrets:
- rocklake_password
- aws_credentials
secrets:
rocklake_password:
external: true
aws_credentials:
external: true
For AWS credentials in production, prefer IAM roles (ECS task roles, EKS IRSA) over static credentials.
Networking Patterns¶
Host Networking (Lowest Latency)¶
docker run --network host \
ghcr.io/rocklake/rocklake:latest \
--catalog s3://bucket/catalog/ --bind 0.0.0.0:5432
Useful for bare-metal deployments where Docker provides isolation but not networking.
Bridge Networking with DNS¶
Within a Docker Compose network, other containers reach RockLake by service name:
-- From another container in the same Compose stack
ATTACH 'ducklake:host=rocklake;port=5432' AS lake;
Reverse Proxy (Nginx / Traefik)¶
RockLake uses the PostgreSQL wire protocol, which is TCP-based. Configure TCP proxying (not HTTP):
# Traefik TCP router example
services:
traefik:
labels:
- "traefik.tcp.routers.rocklake.rule=HostSNI(`*`)"
- "traefik.tcp.routers.rocklake.entrypoints=postgres"
- "traefik.tcp.services.rocklake.loadbalancer.server.port=5432"
Logging¶
JSON Logging for Production¶
Set ROCKLAKE_LOG_FORMAT=json for structured logs compatible with CloudWatch, Datadog, Loki, and other log aggregators:
{"timestamp":"2024-01-15T10:30:00Z","level":"INFO","target":"rocklake_pgwire","message":"Session connected","session_id":"abc123","remote_addr":"172.18.0.5:41234"}
Log Aggregation¶
For Docker's built-in logging drivers:
Upgrading¶
To upgrade RockLake in Docker:
# Pull new version
docker pull ghcr.io/rocklake/rocklake:0.9.0
# Stop current container (graceful shutdown)
docker stop rocklake
# Start new version (same configuration)
docker run -d --name rocklake-new ... ghcr.io/rocklake/rocklake:0.9.0 ...
With Docker Compose:
Because all state is in object storage, the new container resumes exactly where the old one left off. There is no migration step.
Troubleshooting¶
Container exits immediately¶
Check logs: docker logs rocklake. Common causes:
- Missing
--storageflag - Invalid credentials (container can't reach object storage)
- Port already in use on host
Cannot connect from host¶
Ensure you're binding to 0.0.0.0 inside the container (not 127.0.0.1) and port mapping is correct (-p 5432:5432).
High memory usage¶
Check --max-sessions — each session uses ~1 MB. Set resource limits and let Docker OOM-kill if necessary rather than allowing unbounded growth.
Logging Best Practices¶
Structured Logging¶
In containerized environments, structured (JSON) logging integrates best with log aggregation systems:
docker run -d \
--name rocklake \
-e ROCKLAKE_LOG_FORMAT=json \
-e RUST_LOG=rocklake=info \
ghcr.io/rocklake/rocklake:0.8.0 \
--catalog s3://bucket/catalog/ --bind 0.0.0.0:5432
JSON logs work natively with Docker's logging drivers (fluentd, gelf, awslogs) and can be parsed by any log aggregation system without custom grok patterns.
Log Rotation¶
Docker's default json-file log driver can accumulate unbounded log files. Configure rotation:
Or in docker run:
docker run -d \
--log-opt max-size=10m \
--log-opt max-file=3 \
--name rocklake \
ghcr.io/rocklake/rocklake:0.8.0 \
--catalog s3://bucket/catalog/ --bind 0.0.0.0:5432
Correlating Logs with Queries¶
Enable trace-level logging for the wire protocol to see individual SQL statements:
docker run -d \
--name rocklake \
-e RUST_LOG=rocklake_pgwire=debug,rocklake=info \
ghcr.io/rocklake/rocklake:0.8.0 \
--catalog s3://bucket/catalog/ --bind 0.0.0.0:5432
This produces log lines with session IDs that can be correlated with DuckDB client connections for debugging.
Multi-Container Development Stack¶
For development environments that simulate a production-like setup:
version: "3.8"
services:
rocklake:
image: ghcr.io/rocklake/rocklake:0.8.0
ports:
- "5432:5432"
environment:
- AWS_ACCESS_KEY_ID=minioadmin
- AWS_SECRET_ACCESS_KEY=minioadmin
- AWS_REGION=us-east-1
- RUST_LOG=rocklake=debug
command: >
--catalog s3://lakehouse/catalog/
--bind 0.0.0.0:5432
--s3-endpoint http://minio:9000
--s3-force-path-style
depends_on:
minio:
condition: service_healthy
minio:
image: minio/minio:latest
ports:
- "9000:9000"
- "9001:9001"
environment:
- MINIO_ROOT_USER=minioadmin
- MINIO_ROOT_PASSWORD=minioadmin
command: server /data --console-address ":9001"
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 5s
timeout: 5s
retries: 5
minio-init:
image: minio/mc:latest
depends_on:
minio:
condition: service_healthy
entrypoint: >
/bin/sh -c "
mc alias set local http://minio:9000 minioadmin minioadmin;
mc mb local/lakehouse --ignore-existing;
exit 0;
"
duckdb:
image: datacoves/duckdb:latest
depends_on:
- rocklake
stdin_open: true
tty: true
Start the full stack:
docker compose up -d
docker compose exec duckdb duckdb -c "
ATTACH 'ducklake:host=rocklake;port=5432;user=ducklake' AS lake;
USE lake;
CREATE TABLE hello (msg VARCHAR);
INSERT INTO hello VALUES ('Docker Compose works!');
SELECT * FROM hello;
"
Further Reading¶
- Binary Deployment — For non-containerized environments
- Kubernetes — Orchestrated container deployment
- Configuration — Complete configuration reference
- Networking — Network topology and security