Self-HostingDocker Compose

Docker Compose

RAT runs as a set of 7 interconnected services managed by Docker Compose. This page covers the production-ready configuration, resource tuning, networking, and persistence.


Architecture Overview


Services

The compose file defines 7 long-running services plus 1 bootstrap container:

ServiceImageRoleExposed Ports
ratdCustom (Go)API server, scheduler, plugin host8080 (HTTP)
ratqCustom (Python)Interactive DuckDB queries (read-only)Internal only
runnerCustom (Python)Pipeline execution, Iceberg writesInternal only
portalCustom (Next.js)Web IDE3000 (HTTP)
postgrespostgres:16.4-alpinePlatform state database127.0.0.1:5432
miniominio/minioS3-compatible object storage127.0.0.1:9000, 127.0.0.1:9001
nessieghcr.io/projectnessie/nessie:0.79.0Iceberg REST catalog127.0.0.1:19120
minio-initminio/mcBootstrap (bucket creation, exits after)None

The ratq and runner services do not expose ports to the host machine. They communicate with ratd exclusively over the internal Docker network via gRPC (ConnectRPC).


Service Configuration

ratd (Go Platform)

The central API server. All external traffic flows through ratd.

infra/docker-compose.yml (ratd)
ratd:
  build:
    context: ../platform
    dockerfile: Dockerfile
  init: true
  ports:
    - "8080:8080"
  environment:
    DATABASE_URL: postgres://rat:rat@postgres:5432/rat?sslmode=disable
    <<: *s3-credentials
    NESSIE_URL: http://nessie:19120/api/v1
    RATQ_ADDR: http://ratq:50051
    RUNNER_ADDR: http://runner:50052
    EDITION: community
  depends_on:
    postgres: { condition: service_healthy }
    minio: { condition: service_healthy }
    nessie: { condition: service_healthy }
  healthcheck:
    test: ["CMD", "/ratd", "healthcheck"]
    interval: 5s
    timeout: 3s
    retries: 5
    start_period: 5s

runner (Python Pipeline Executor)

The heaviest service — executes pipelines with DuckDB, PyArrow, and PyIceberg.

infra/docker-compose.yml (runner)
runner:
  build:
    context: ../runner
    dockerfile: Dockerfile
  init: true
  environment:
    GRPC_PORT: "50052"
    RATD_CALLBACK_URL: http://ratd:8080
    <<: *s3-credentials
    NESSIE_URL: http://nessie:19120/api/v1
  depends_on:
    minio: { condition: service_healthy }
    nessie: { condition: service_healthy }
  stop_grace_period: 30s

The runner has a stop_grace_period: 30s to allow in-progress pipeline runs to complete gracefully during shutdown. Other services use the default 10-second grace period.

portal (Next.js Web IDE)

The user-facing web application. Connects to ratd for all API calls.

infra/docker-compose.yml (portal)
portal:
  build:
    context: ..
    dockerfile: portal/Dockerfile
  init: true
  ports:
    - "3000:3000"
  environment:
    NEXT_PUBLIC_API_URL: http://localhost:8080
    API_URL: http://ratd:8080
  depends_on:
    ratd: { condition: service_healthy }
⚠️

The portal build context is the monorepo root (..), not the portal/ directory. This is because the Dockerfile needs access to the sdk-typescript/ directory to build the SDK as part of the portal image.


Resource Limits and Tuning

Every service has CPU and memory limits. These are configured in the deploy.resources.limits section.

Default Limits

ServiceMemoryCPUPIDsNotes
ratd512 MB1.0100Lightweight Go binary
ratq1 GB1.0100DuckDB needs memory for query results
runner2 GB2.0100Largest — DuckDB + PyArrow + Iceberg writes
portal512 MB1.0100Next.js standalone server
postgres1 GB1.0100Database engine
minio1 GB1.0100Object storage
nessie512 MB1.0100Quarkus-based catalog
minio-init256 MB0.5100Bootstrap only

Total: ~7.25 GB RAM, 7.5 CPU cores at maximum utilization.

Tuning for Larger Workloads

If you process large datasets, increase the runner’s limits:

infra/docker-compose.override.yml
services:
  runner:
    environment:
      DUCKDB_MEMORY_LIMIT: 4GB
      DUCKDB_THREADS: 8
    deploy:
      resources:
        limits:
          memory: 6G
          cpus: '4.0'
  ratq:
    environment:
      DUCKDB_MEMORY_LIMIT: 4GB
    deploy:
      resources:
        limits:
          memory: 4G

Tuning for Minimal Resources

For development on a laptop with 8 GB RAM, reduce limits:

infra/docker-compose.override.yml
services:
  runner:
    environment:
      DUCKDB_MEMORY_LIMIT: 1GB
      DUCKDB_THREADS: 2
    deploy:
      resources:
        limits:
          memory: 1G
          cpus: '1.0'
  ratq:
    environment:
      DUCKDB_MEMORY_LIMIT: 512MB
    deploy:
      resources:
        limits:
          memory: 512M

Network Architecture

RAT uses two Docker networks to isolate traffic:

frontend (rat_frontend)

Browser-accessible services. Only portal and ratd are on this network.

ServicePortAccessible From
portal3000Browser
ratd8080Browser + portal

backend (infra_default)

Internal service communication. All services are on this network.

ServicePortProtocolAccessible From
ratd8080HTTPportal, runner (callbacks)
ratq50051gRPCratd
runner50052gRPCratd
postgres5432TCPratd, nessie
minio9000HTTPratd, ratq, runner, nessie
nessie19120HTTPratd, ratq, runner

Postgres, MinIO, and Nessie bind to 127.0.0.1 on the host (localhost only). This prevents external access in development. In production, you may want to remove the port bindings entirely or use a reverse proxy.

Network Definition

infra/docker-compose.yml
networks:
  frontend:
    name: rat_frontend
  backend:
    name: infra_default

Volumes and Persistence

RAT uses two named Docker volumes for persistent data:

postgres_data

Stores all PostgreSQL data: pipeline metadata, run history, schedules, triggers, and quality test results.

volumes:
  postgres_data:

Mount point: /var/lib/postgresql/data

minio_data

Stores all S3 objects: pipeline code, landing zone files, Iceberg data files, and Iceberg metadata.

volumes:
  minio_data:

Mount point: /data

⚠️

Removing volumes (docker compose down -v) permanently deletes all data. The minio_data volume contains your actual data files (Parquet/Iceberg) — losing it means losing all pipeline outputs.

External Volumes (Production)

For production, consider using external volumes or bind mounts to a dedicated storage path:

infra/docker-compose.override.yml
volumes:
  postgres_data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /data/rat/postgres
  minio_data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /data/rat/minio

Health Checks

Every service registers a Docker health check. The Compose file uses depends_on with condition: service_healthy to ensure services start in the correct order.

ServiceHealth CheckIntervalRetriesStart Period
ratd/ratd healthcheck (CLI)5s55s
ratqgRPC channel ready check10s515s
runnergRPC channel ready check10s515s
portalwget http://localhost:300010s330s
postgrespg_isready5s55s
miniomc ready local5s55s
nessieHTTP /q/health/ready10s515s

Startup Order

Based on depends_on declarations, services start in this order:

Infrastructure tier

Postgres and MinIO start first (no dependencies).

Catalog tier

Nessie starts after Postgres is healthy (uses Postgres for metadata storage).

Service tier

ratd, ratq, and runner start after their infrastructure dependencies are healthy.

UI tier

Portal starts after ratd is healthy.


Shared Configuration

S3 Credentials Anchor

The compose file uses a YAML anchor to avoid repeating S3 credentials across services:

infra/docker-compose.yml
x-s3-credentials: &s3-credentials
  S3_ENDPOINT: minio:9000
  S3_ACCESS_KEY: ${S3_ACCESS_KEY:-minioadmin}
  S3_SECRET_KEY: ${S3_SECRET_KEY:-minioadmin}
  S3_BUCKET: rat
  S3_USE_SSL: "false"
  S3_REGION: us-east-1

Services reference it with <<: *s3-credentials in their environment block.

Logging

All services use the json-file log driver with rotation:

infra/docker-compose.yml
x-logging: &default-logging
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

This limits each service to 30 MB of logs (3 files x 10 MB each) before rotation.


MinIO Initialization

The minio-init container is a one-shot bootstrap job that:

  1. Sets up the mc (MinIO Client) alias
  2. Creates the rat bucket if it does not exist
  3. Enables bucket versioning
  4. Adds a lifecycle rule to expire non-current versions after 7 days
infra/docker-compose.yml (minio-init)
minio-init:
  image: minio/mc:RELEASE.2024-06-12T14-34-03Z
  depends_on:
    minio: { condition: service_healthy }
  entrypoint: >
    /bin/sh -c "
    mc alias set local http://minio:9000 $$S3_ACCESS_KEY $$S3_SECRET_KEY &&
    mc mb --ignore-existing local/rat &&
    mc version enable local/rat &&
    mc ilm rule add local/rat --noncurrent-expire-days 7 --prefix '' || true
    "
  restart: on-failure

This container exits after initialization. The restart: on-failure policy ensures it retries if MinIO is not yet ready.


Compose Overrides

For local customizations, create a docker-compose.override.yml file in the infra/ directory:

infra/docker-compose.override.yml
services:
  ratd:
    environment:
      RATE_LIMIT: "0"
      CORS_ORIGINS: "http://localhost:3000,http://localhost:3001"
    ports:
      - "8080:8080"
      - "8081:8081"  # expose gRPC port for debugging
 
  runner:
    environment:
      DUCKDB_MEMORY_LIMIT: 4GB
    deploy:
      resources:
        limits:
          memory: 4G

Docker Compose automatically merges docker-compose.yml with docker-compose.override.yml when both exist in the same directory.

The docker-compose.override.yml file is gitignored. Use it for local development customizations that should not be committed.