diff --git a/Makefile b/Makefile index 9a8923aa..ec8e253f 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ # JAN SERVER MAKEFILE # ============================================================================================================ # -# A comprehensive build system for Jan Server - a microservices-based LLM API +# A comprehensive build system for Jan Server - a microservices-based LLM API # with MCP (Model Context Protocol) tool integration. # # ============================================================================================================ @@ -49,14 +49,44 @@ # VARIABLES # ============================================================================================================ -# Docker Compose -COMPOSE = docker compose -COMPOSE_DEV_FULL = docker compose -f docker-compose.yml -f docker-compose.dev-full.yml -MONITOR_COMPOSE = docker compose -f infra/docker/observability.yml +# Container Engine Detection (docker or podman) +# Override with: make CONTAINER_ENGINE=podman +CONTAINER_ENGINE ?= auto + +ifeq ($(CONTAINER_ENGINE),auto) + # Auto-detect: prefer docker, fallback to podman + ifneq ($(shell command -v docker 2>/dev/null),) + DETECTED_ENGINE := docker + else ifneq ($(shell command -v podman 2>/dev/null),) + DETECTED_ENGINE := podman + else + DETECTED_ENGINE := docker + endif +else + DETECTED_ENGINE := $(CONTAINER_ENGINE) +endif + +# Set compose command based on detected engine +ifeq ($(DETECTED_ENGINE),podman) + # Podman: use podman-compose with flattened compose file + # Generate with: python3 scripts/gen-podman-compose.py + PODMAN_COMPOSE_FILE ?= docker-compose.podman.yml + COMPOSE = podman-compose -f $(PODMAN_COMPOSE_FILE) + COMPOSE_DEV_FULL = podman-compose -f $(PODMAN_COMPOSE_FILE) + MONITOR_COMPOSE = podman-compose -f infra/docker/observability.yml + # Podman doesn't use BuildKit + DOCKER_BUILDKIT ?= 0 + COMPOSE_DOCKER_CLI_BUILD ?= 0 +else + # Docker: use docker compose v2 + COMPOSE = docker compose + COMPOSE_DEV_FULL = docker compose -f docker-compose.yml -f docker-compose.dev-full.yml + MONITOR_COMPOSE = docker compose -f infra/docker/observability.yml + # BuildKit is required for additional_contexts in compose builds. + DOCKER_BUILDKIT ?= 1 + COMPOSE_DOCKER_CLI_BUILD ?= 1 +endif -# BuildKit is required for additional_contexts in compose builds. -DOCKER_BUILDKIT ?= 1 -COMPOSE_DOCKER_CLI_BUILD ?= 1 export DOCKER_BUILDKIT COMPOSE_DOCKER_CLI_BUILD MEDIA_SERVICE_KEY ?= changeme-media-key @@ -975,6 +1005,86 @@ else @curl -sf http://localhost:3010 >/dev/null && echo " SandboxFusion: healthy" || echo " SandboxFusion: unhealthy" endif +# ============================================================================================================ +# SECTION 10: PODMAN SUPPORT +# ============================================================================================================ + +.PHONY: engine-info podman-setup podman-network podman-ps podman-clean + +## Show which container engine is being used +engine-info: + @echo "Container Engine Configuration" + @echo "==============================" + @echo "CONTAINER_ENGINE: $(CONTAINER_ENGINE)" + @echo "DETECTED_ENGINE: $(DETECTED_ENGINE)" + @echo "COMPOSE command: $(COMPOSE)" + @echo "" +ifeq ($(DETECTED_ENGINE),podman) + @podman --version + @podman-compose --version + @echo "" + @echo "Podman info:" + @podman info --format 'Rootless: {{.Host.Security.Rootless}}' +else + @docker --version + @docker compose version +endif + +## Setup Podman environment (create networks, generate compose file) +podman-setup: podman-compose-generate podman-network +ifeq ($(DETECTED_ENGINE),podman) + @echo "" + @echo "Podman setup complete!" + @echo "" + @echo "To start services:" + @echo " make up-full" + @echo "" + @echo "Or explicitly use Podman:" + @echo " make CONTAINER_ENGINE=podman up-full" +else + @echo "Docker detected. For Podman, run:" + @echo " make CONTAINER_ENGINE=podman podman-setup" +endif + +## Generate flattened compose file for podman-compose +podman-compose-generate: + @echo "Generating Podman-compatible compose file..." + @python3 scripts/gen-podman-compose.py + @echo "" + +## Create required Podman networks +podman-network: + @echo "Creating container networks..." +ifeq ($(DETECTED_ENGINE),podman) + @podman network exists jan-server_default 2>/dev/null || podman network create jan-server_default + @podman network exists jan-server_mcp-network 2>/dev/null || podman network create jan-server_mcp-network +else + @docker network inspect jan-server_default >/dev/null 2>&1 || docker network create jan-server_default + @docker network inspect jan-server_mcp-network >/dev/null 2>&1 || docker network create jan-server_mcp-network +endif + @echo "Networks ready" + +## List Jan Server containers (works with both Docker and Podman) +podman-ps: +ifeq ($(DETECTED_ENGINE),podman) + @podman ps --filter "label=com.docker.compose.project=server" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" +else + @docker ps --filter "label=com.docker.compose.project=server" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" +endif + +## Clean up container resources +podman-clean: + @echo "Cleaning up Jan Server container resources..." + $(COMPOSE) down -v --remove-orphans 2>/dev/null || true +ifeq ($(DETECTED_ENGINE),podman) + @podman volume prune -f + @podman network prune -f +else + @docker volume prune -f + @docker network prune -f +endif + @echo "Cleanup complete" + # ============================================================================================================ # END OF MAKEFILE # ============================================================================================================ diff --git a/Makefile.podman b/Makefile.podman new file mode 100644 index 00000000..1a2dda72 --- /dev/null +++ b/Makefile.podman @@ -0,0 +1,127 @@ +# ============================================================================================================ +# JAN SERVER - PODMAN OVERRIDE MAKEFILE +# ============================================================================================================ +# +# This file provides Podman-specific overrides for Jan Server. +# Include this in your main Makefile or use it directly. +# +# Usage: +# make -f Makefile.podman up-full # Use Podman directly +# make CONTAINER_ENGINE=podman up-full # Override in main Makefile +# +# Requirements: +# - Podman 4.0+ (you have 5.7.1) +# - podman-compose 1.0.6+ (you have 1.5.0) +# +# ============================================================================================================ + +# Container Engine Detection +# Supports: docker, podman, auto (detect) +CONTAINER_ENGINE ?= podman + +# Podman-specific compose command +# podman-compose is more compatible than 'podman compose' for complex setups +COMPOSE = podman-compose +COMPOSE_DEV_FULL = podman-compose -f docker-compose.yml -f docker-compose.dev-full.yml +MONITOR_COMPOSE = podman-compose -f infra/docker/observability.yml + +# Podman doesn't use BuildKit, but these are harmless +DOCKER_BUILDKIT ?= 0 +COMPOSE_DOCKER_CLI_BUILD ?= 0 + +# Podman rootless networking +# For host.docker.internal equivalent in rootless Podman +PODMAN_HOST_GATEWAY ?= host.containers.internal + +# ============================================================================================================ +# PODMAN-SPECIFIC TARGETS +# ============================================================================================================ + +.PHONY: podman-setup podman-check podman-socket podman-network + +## Setup Podman environment for Jan Server +podman-setup: podman-check podman-network + @echo "Podman environment ready for Jan Server" + @echo "" + @echo "Quick start:" + @echo " make -f Makefile.podman up-full" + @echo "" + @echo "Or set in your shell:" + @echo " export CONTAINER_ENGINE=podman" + @echo " make up-full" + +## Check Podman installation and version +podman-check: + @echo "Checking Podman installation..." + @podman --version || (echo "ERROR: Podman not found" && exit 1) + @podman-compose --version || (echo "ERROR: podman-compose not found" && exit 1) + @echo "" + @echo "Podman info:" + @podman info --format '{{.Host.OS}} / {{.Host.Arch}} / cgroups {{.Host.CgroupsVersion}}' + @echo "" + @echo "Rootless mode: $$(podman info --format '{{.Host.Security.Rootless}}')" + +## Enable Podman socket for Docker API compatibility (optional) +podman-socket: + @echo "Enabling Podman socket..." + systemctl --user enable --now podman.socket + @echo "" + @echo "Socket path: $${XDG_RUNTIME_DIR}/podman/podman.sock" + @echo "" + @echo "To use with Docker CLI:" + @echo " export DOCKER_HOST=unix://$${XDG_RUNTIME_DIR}/podman/podman.sock" + +## Create required networks for Jan Server +podman-network: + @echo "Creating Podman networks..." + @podman network exists jan-server_default 2>/dev/null || podman network create jan-server_default + @podman network exists jan-server_mcp-network 2>/dev/null || podman network create jan-server_mcp-network + @echo "Networks ready" + +## List Jan Server containers +podman-ps: + @podman ps --filter "label=com.docker.compose.project=jan-server" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" + +## Show Podman resource usage +podman-stats: + @podman stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}" + +## Clean up Podman resources +podman-clean: + @echo "Cleaning up Jan Server Podman resources..." + $(COMPOSE) down -v --remove-orphans 2>/dev/null || true + @podman volume prune -f + @podman network prune -f + @echo "Cleanup complete" + +## Fix common Podman issues +podman-fix: + @echo "Applying common Podman fixes..." + @echo "" + @echo "1. Resetting Podman storage (if corrupted)..." + @echo " Run manually if needed: podman system reset" + @echo "" + @echo "2. Checking cgroup configuration..." + @if [ -f /sys/fs/cgroup/cgroup.controllers ]; then \ + echo " cgroups v2 detected (good)"; \ + else \ + echo " cgroups v1 detected - consider upgrading"; \ + fi + @echo "" + @echo "3. Checking user namespaces..." + @cat /proc/sys/user/max_user_namespaces 2>/dev/null || echo " Cannot read max_user_namespaces" + +# ============================================================================================================ +# OVERRIDE MAIN MAKEFILE TARGETS +# ============================================================================================================ + +# Include the main Makefile targets but with Podman compose +include Makefile + +# Override check-deps for Podman +check-deps: + @echo "Checking Podman dependencies..." + @podman --version >/dev/null 2>&1 || echo "Podman not found" + @podman-compose --version >/dev/null 2>&1 || echo "podman-compose not found" + @go version >/dev/null 2>&1 || echo "Go not found (optional)" + @echo "Dependency check complete" diff --git a/docs/guides/podman-setup.md b/docs/guides/podman-setup.md new file mode 100644 index 00000000..ed69ca2c --- /dev/null +++ b/docs/guides/podman-setup.md @@ -0,0 +1,214 @@ +# Podman Setup for Jan Server + +This guide explains how to run Jan Server using Podman instead of Docker. + +## Requirements + +- **Podman** 4.0+ (tested with 5.7.1) +- **podman-compose** 1.0.6+ (tested with 1.5.0) + +### Arch Linux Installation + +```bash +sudo pacman -S podman podman-compose +``` + +### Verify Installation + +```bash +podman --version # Should be 4.0+ +podman-compose --version # Should be 1.0.6+ +``` + +## Quick Start + +The Makefile automatically detects Podman when Docker is not available: + +```bash +# Auto-detect (uses Podman if Docker not found) +make up-full + +# Explicitly use Podman +make CONTAINER_ENGINE=podman up-full + +# Check which engine is being used +make engine-info +``` + +## Setup Steps + +### 1. Initial Setup + +```bash +# Setup Podman networks +make CONTAINER_ENGINE=podman podman-setup + +# Generate .env file +make setup +``` + +### 2. Start Services + +```bash +# Start all services +make CONTAINER_ENGINE=podman up-full + +# Or start specific profiles +make CONTAINER_ENGINE=podman up-infra +make CONTAINER_ENGINE=podman up-api +make CONTAINER_ENGINE=podman up-mcp +``` + +### 3. Verify Services + +```bash +make health-check +``` + +## Shell Configuration (Optional) + +To always use Podman without specifying `CONTAINER_ENGINE`: + +```bash +# Add to ~/.bashrc or ~/.zshrc +export CONTAINER_ENGINE=podman + +# Or create an alias +alias docker=podman +``` + +## Podman-Specific Commands + +```bash +# List Jan Server containers +make podman-ps + +# Clean up all resources +make podman-clean + +# Show engine info +make engine-info +``` + +## Rootless Mode + +Podman runs in rootless mode by default on most Linux distributions. This is the recommended configuration for development. + +To verify rootless mode: + +```bash +podman info --format '{{.Host.Security.Rootless}}' +# Should output: true +``` + +## Networking + +### Container-to-Container Communication + +Containers communicate via Podman networks. The Makefile automatically creates: + +- `jan-server_default` - Main network for services +- `jan-server_mcp-network` - Network for MCP tools + +### Host Access from Containers + +In Podman, use `host.containers.internal` instead of `host.docker.internal`: + +```yaml +# In docker-compose.yml or environment variables +SOME_HOST_URL: http://host.containers.internal:8080 +``` + +## Troubleshooting + +### Permission Denied Errors + +For rootless Podman, ensure your user has subuids/subgids configured: + +```bash +# Check configuration +cat /etc/subuid | grep $USER +cat /etc/subgid | grep $USER + +# If missing, add them +sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER +``` + +### Network Issues + +```bash +# Reset networks +podman network rm jan-server_default jan-server_mcp-network +make podman-network +``` + +### Storage Issues + +```bash +# Reset Podman storage (WARNING: removes all containers/images) +podman system reset +``` + +### Slow Startup + +Podman's first run may be slow as it sets up user namespaces. Subsequent runs should be faster. + +### cgroups v2 Issues + +Modern distributions use cgroups v2. Verify: + +```bash +# Check cgroups version +cat /sys/fs/cgroup/cgroup.controllers + +# If this file exists, you're using cgroups v2 (good) +``` + +## Known Limitations + +1. **BuildKit**: Podman doesn't use Docker's BuildKit. Multi-stage builds still work but some advanced features may differ. + +2. **Docker Socket**: Some tools expect `/var/run/docker.sock`. Enable Podman socket for compatibility: + ```bash + systemctl --user enable --now podman.socket + export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock + ``` + +3. **Compose File Compatibility**: podman-compose 1.5.0 supports most Docker Compose v3 features including: + - Profiles + - Include directives + - Named volumes + - Networks + - Health checks + +## Performance Tips + +1. **Use native overlay storage**: + ```bash + # Check storage driver + podman info --format '{{.Store.GraphDriverName}}' + # Should be 'overlay' for best performance + ``` + +2. **Enable parallel pulls**: + ```bash + # In ~/.config/containers/registries.conf + [engine] + num_locks = 2048 + ``` + +## Comparison: Docker vs Podman + +| Feature | Docker | Podman | +|---------|--------|--------| +| Daemon | Required | Daemonless | +| Root by default | Yes | No (rootless) | +| Compose command | `docker compose` | `podman-compose` | +| Socket | `/var/run/docker.sock` | `$XDG_RUNTIME_DIR/podman/podman.sock` | +| Host access | `host.docker.internal` | `host.containers.internal` | + +## See Also + +- [Development Guide](development.md) +- [Main README](../../README.md) +- [Podman Documentation](https://podman.io/docs) diff --git a/scripts/gen-podman-compose.py b/scripts/gen-podman-compose.py new file mode 100644 index 00000000..52383eb5 --- /dev/null +++ b/scripts/gen-podman-compose.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""Generate a flattened compose file for podman-compose. + +podman-compose doesn't support the 'include' directive with path objects, +so we merge all compose files into a single file. + +Usage: python3 scripts/gen-podman-compose.py [output-file] +""" + +import sys +from pathlib import Path + +import yaml + + +def main(): + project_root = Path(__file__).parent.parent + output_file = ( + sys.argv[1] if len(sys.argv) > 1 else project_root / "docker-compose.podman.yml" + ) + + compose_files = [ + "infra/docker/infrastructure.yml", + "infra/docker/services-api.yml", + "infra/docker/services-mcp.yml", + "infra/docker/services-memory.yml", + "infra/docker/services-realtime.yml", + "infra/docker/apps-platform.yml", + "infra/docker/apps-web.yml", + "infra/docker/inference.yml", + ] + + merged = { + "volumes": {}, + "networks": {}, + "services": {}, + } + + for cf in compose_files: + path = project_root / cf + if not path.exists(): + print(f"Warning: {cf} not found, skipping", file=sys.stderr) + continue + + with open(path) as f: + data = yaml.safe_load(f) or {} + + if "volumes" in data and data["volumes"]: + merged["volumes"].update(data["volumes"]) + + if "networks" in data and data["networks"]: + merged["networks"].update(data["networks"]) + + if "services" in data and data["services"]: + # Fix relative paths - original files are in infra/docker/ + for svc_name, svc_data in data["services"].items(): + if svc_data and "env_file" in svc_data: + fixed_env_files = [] + for ef in svc_data["env_file"]: + # Fix ../../.env -> .env (now relative to project root) + if isinstance(ef, str) and "../../.env" in ef: + fixed_env_files.append(ef.replace("../../.env", ".env")) + else: + fixed_env_files.append(ef) + svc_data["env_file"] = fixed_env_files + + # Fix bind mount paths - adjust relative paths for project root + if svc_data and "volumes" in svc_data: + fixed_volumes = [] + cf_dir = str(Path(cf).parent) # e.g., "infra/docker" + for vol in svc_data["volumes"]: + if isinstance(vol, str) and ":" in vol: + # This is a bind mount like ./path:/container/path:ro + parts = vol.split(":") + host_path = parts[0] + if host_path.startswith("./"): + # Fix relative path: ./init-db -> ./infra/docker/init-db + parts[0] = host_path.replace("./", f"./{cf_dir}/", 1) + fixed_volumes.append(":".join(parts)) + elif host_path.startswith("../../"): + # Fix ../../path -> ./path (relative to project root) + parts[0] = host_path.replace("../../", "./", 1) + fixed_volumes.append(":".join(parts)) + else: + fixed_volumes.append(vol) + else: + # Named volume or other format - keep as is + fixed_volumes.append(vol) + svc_data["volumes"] = fixed_volumes + + # Replace build with pre-built image for services using additional_contexts + # These need to be built separately with: ./scripts/podman-build-services.sh + if svc_data and "build" in svc_data: + build_cfg = svc_data["build"] + if ( + isinstance(build_cfg, dict) + and "additional_contexts" in build_cfg + ): + # Remove build config and use pre-built image instead + del svc_data["build"] + svc_data["image"] = f"localhost/jan-server-{svc_name}:latest" + elif isinstance(build_cfg, dict) and "context" in build_cfg: + ctx = build_cfg["context"] + if ctx.startswith("../"): + # ../../services/x -> services/x + build_cfg["context"] = ctx.replace("../../", "") + + merged["services"].update(data["services"]) + + # Clean up empty sections + merged = {k: v for k, v in merged.items() if v} + + # Fix nested variable interpolation that podman-compose doesn't handle well + # Replace ${VAR:-${NESTED:-default}} patterns with simpler ${VAR:-default} + def fix_nested_vars(obj): + if isinstance(obj, str): + # Replace common nested patterns with direct defaults + replacements = { + "${POSTGRES_USER:-jan_user}": "jan_user", + "${POSTGRES_PASSWORD:-jan_password}": "jan_password", + "${POSTGRES_DB:-jan_llm_api}": "jan_llm_api", + "${POSTGRES_HOST:-api-db}": "api-db", + "${POSTGRES_PORT:-5432}": "5432", + } + for old, new in replacements.items(): + obj = obj.replace(old, new) + return obj + elif isinstance(obj, dict): + return {k: fix_nested_vars(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [fix_nested_vars(i) for i in obj] + return obj + + merged = fix_nested_vars(merged) + + with open(output_file, "w") as f: + f.write("# Auto-generated Podman-compatible compose file\n") + f.write("# Generated by: python3 scripts/gen-podman-compose.py\n") + f.write("# Do not edit manually - regenerate with the script\n\n") + yaml.dump( + merged, f, default_flow_style=False, sort_keys=False, allow_unicode=True + ) + + print(f"Generated: {output_file}") + print(f" Services: {len(merged.get('services', {}))}") + print(f" Volumes: {len(merged.get('volumes', {}))}") + print(f" Networks: {len(merged.get('networks', {}))}") + + +if __name__ == "__main__": + main() diff --git a/scripts/podman-build-services.sh b/scripts/podman-build-services.sh new file mode 100755 index 00000000..f62b2188 --- /dev/null +++ b/scripts/podman-build-services.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# Build Go services for Podman +# Workaround for podman-compose not supporting additional_contexts +# +# Usage: ./scripts/podman-build-services.sh [service...] +# Example: ./scripts/podman-build-services.sh llm-api media-api + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +cd "$PROJECT_ROOT" + +# Services that need building with go-common context +GO_SERVICES=( + "llm-api" + "media-api" + "response-api" + "mcp-tools" + "memory-tools" + "realtime-api" +) + +# If specific services provided, use those +if [ $# -gt 0 ]; then + GO_SERVICES=("$@") +fi + +echo "Building Go services for Podman..." +echo "Project root: $PROJECT_ROOT" +echo "" + +for svc in "${GO_SERVICES[@]}"; do + svc_dir="$PROJECT_ROOT/services/$svc" + + if [ ! -d "$svc_dir" ]; then + echo "Warning: $svc_dir not found, skipping" + continue + fi + + dockerfile="$svc_dir/Dockerfile" + if [ ! -f "$dockerfile" ]; then + echo "Warning: $dockerfile not found, skipping" + continue + fi + + echo "Building $svc..." + + # Create a temporary build context with go-common included + BUILD_CONTEXT=$(mktemp -d) + trap "rm -rf $BUILD_CONTEXT" EXIT + + # Copy service files + cp -r "$svc_dir"/* "$BUILD_CONTEXT/" + + # Copy go-common to the build context + mkdir -p "$BUILD_CONTEXT/packages" + cp -r "$PROJECT_ROOT/packages/go-common" "$BUILD_CONTEXT/packages/" + + # Create modified Dockerfile that doesn't use additional_contexts + sed 's|COPY --from=go-common \. /packages/go-common|COPY packages/go-common /packages/go-common|g' \ + "$dockerfile" > "$BUILD_CONTEXT/Dockerfile.podman" + + # Build with podman + podman build \ + -t "jan-server-$svc:latest" \ + -f "$BUILD_CONTEXT/Dockerfile.podman" \ + "$BUILD_CONTEXT" + + echo "✓ Built jan-server-$svc:latest" + echo "" + + # Clean up temp dir for this iteration + rm -rf "$BUILD_CONTEXT" + trap - EXIT +done + +echo "All services built successfully!" +echo "" +echo "To use in podman-compose, add to docker-compose.podman.yml:" +echo " services:" +echo " llm-api:" +echo " image: jan-server-llm-api:latest"