kernex-agent 0.4.4

CLI dev assistant powered by Kernex runtime
networks:
  # Caddy-to-kx internal traffic only — no host port binding on kx
  kx_net:
    driver: bridge

volumes:
  kx_data:      # persistent job data and runtime state
  caddy_data:   # TLS certificates (Let's Encrypt)
  caddy_config: # Caddy runtime config

services:

  # ── kx serve ────────────────────────────────────────────────────────────────
  kx:
    build:
      context: ..
      dockerfile: deploy/Dockerfile
    image: kernex-agent:latest
    networks:
      - kx_net
    # No ports exposed to host — only Caddy can reach kx
    env_file: .env
    environment:
      KERNEX_PROVIDER: ${KERNEX_PROVIDER:-claude-code}
      KERNEX_MODEL: ${KERNEX_MODEL:-}
    command:
      - serve
      - --host
      - "0.0.0.0"
      - --port
      - "8080"
      - --workers
      - "${KX_WORKERS:-8}"
    volumes:
      # Persistent data directory
      - kx_data:/home/kx/.kx
      # Skills for headless serve-mode workflows — read-only, mounted at runtime
      - type: bind
        source: ../deploy/skills
        target: /home/kx/.kx/skills
        read_only: true
        bind:
          create_host_path: false
      # Named workflow TOML files — read-only, mounted at runtime
      - type: bind
        source: ../deploy/workflows
        target: /home/kx/.kx/workflows
        read_only: true
        bind:
          create_host_path: false
      # Claude subscription credentials — read-only, never modified by container
      - type: bind
        source: ${CLAUDE_CREDENTIALS_PATH}
        target: /home/kx/.claude/.credentials.json
        read_only: true
        bind:
          create_host_path: false
    restart: unless-stopped

    # ── Container hardening ──────────────────────────────────────────────────
    security_opt:
      - no-new-privileges:true   # prevent privilege escalation via setuid
    cap_drop:
      - ALL                      # drop every Linux capability
    read_only: true              # root filesystem is read-only
    tmpfs:
      - /tmp:noexec,nosuid,size=64m  # writable temp, no executables
    mem_limit: 1g
    memswap_limit: 1g            # disable swap (avoids credential paging to disk)
    cpus: "2.0"
    pids_limit: 512              # prevent fork bombs
    ulimits:
      nofile:
        soft: 65536
        hard: 65536

    logging:
      driver: json-file
      options:
        max-size: "20m"
        max-file: "10"

    healthcheck:
      test: ["CMD", "curl", "-sf", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 15s

  # ── Caddy (TLS termination + reverse proxy) ──────────────────────────────────
  caddy:
    build:
      context: .
      dockerfile: Dockerfile.caddy
    image: kernex-caddy:latest
    networks:
      - kx_net
    ports:
      - "80:80"    # HTTP redirect to HTTPS
      - "443:443"  # HTTPS
      - "443:443/udp"  # HTTP/3
    env_file: .env
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    restart: unless-stopped
    depends_on:
      kx:
        condition: service_healthy

    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE  # required to bind ports 80/443
    read_only: true
    tmpfs:
      - /tmp:noexec,nosuid,size=32m

    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"