diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index ef862655..ff07a7bc 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -1,11 +1,17 @@ # Memoh Deployment Guide -## Quick Deploy +## One-Click Install + +```bash +curl -fsSL https://raw.githubusercontent.com/memohai/Memoh/feat/containerd-in-docker/scripts/install.sh | sh +``` + +Or manually: ```bash git clone https://github.com/memohai/Memoh.git cd Memoh -./deploy.sh +docker compose up -d ``` Access: @@ -15,18 +21,22 @@ Access: Default credentials: `admin` / `admin123` -## Manual Deploy +## Prerequisites + +- Docker (with Docker Compose v2) +- Git + +That's it. No containerd, nerdctl, or buildkit required on the host. + +## Custom Configuration ```bash cp docker/config/config.docker.toml config.toml -nano config.toml # Change passwords and secrets -nerdctl build -f docker/Dockerfile.mcp -t docker.io/library/memoh-mcp:latest . -docker compose up -d +nano config.toml +MEMOH_CONFIG=./config.toml docker compose up -d ``` -## Required Configuration - -Must change in `config.toml`: +Recommended changes for production: - `admin.password` - Admin password - `auth.jwt_secret` - JWT secret (generate with `openssl rand -base64 32`) - `postgres.password` - Database password @@ -37,7 +47,8 @@ Must change in `config.toml`: docker compose up -d # Start docker compose down # Stop docker compose logs -f # View logs -nerdctl images # Ensure that memoh-mcp:latest exsits +docker compose ps # Status +docker compose up -d --build # Rebuild and restart ``` ## Production @@ -52,13 +63,13 @@ nerdctl images # Ensure that memoh-mcp:latest exsits ```bash docker compose logs server # View service logs +docker compose logs containerd # View containerd logs docker compose config # Check configuration -docker compose build --no-cache && docker compose up -d # Rebuild +docker compose build --no-cache && docker compose up -d # Full rebuild ``` ## Security Warnings -⚠️ Main service has host Docker access - only run in trusted environments -⚠️ Must change all default passwords and secrets -⚠️ Use HTTPS in production - +- Main service has privileged container access - only run in trusted environments +- Must change all default passwords and secrets +- Use HTTPS in production diff --git a/README.md b/README.md index dde9d622..af638804 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,28 @@ Memoh is a AI agent system platform. Users can create their own AI bots and chat with them via Telegram, Discord, Lark(Feishu), etc. Every bot has independent container and memory system which allows them to edit files, execute commands and build themselves - Like [OpenClaw](https://openclaw.ai), Memoh provides a more secure, flexible and scalable solution for multi-bot management. +## Quick Start + +One-click install (requires Docker): + +```bash +curl -fsSL https://raw.githubusercontent.com/memohai/Memoh/feat/containerd-in-docker/scripts/install.sh | sh +``` + +Or manually: + +```bash +git clone -b feat/containerd-in-docker --depth 1 https://github.com/memohai/Memoh.git +cd Memoh +docker compose up -d +``` + +Visit http://localhost after startup. Default login: `admin` / `admin123` + +Silent install with all defaults: `curl -fsSL ... | sh -s -- -y` + +See [DEPLOYMENT.md](DEPLOYMENT.md) for custom configuration and production setup. + ## Why Memoh? OpenClaw, Clawdbot, and Moltbot are impressive, but they have notable drawbacks: stability issues, security concerns, cumbersome configuration, and high token costs. If you're looking for a stable, secure Bot SaaS solution, consider our open-source Memoh. @@ -48,20 +70,6 @@ Memoh Bot can distinguish and remember requests from multiple humans and bots, w Please refer to the [Roadmap Version 0.1](https://github.com/memohai/Memoh/issues/2) for more details. -## Quick Start - -### Docker Deployment (Recommended) - -The fastest way to deploy Memoh: - -```bash -git clone https://github.com/memohai/Memoh.git -cd Memoh -./deploy.sh -``` - -Visit http://localhost after deployment. See [Docker Deployment Guide](README_DOCKER.md) for details. - ### Development Refer to [CONTRIBUTING.md](CONTRIBUTING.md) for development setup. diff --git a/config.toml.example b/config.toml.example index 640da6ae..5eaa6adf 100644 --- a/config.toml.example +++ b/config.toml.example @@ -24,7 +24,7 @@ socket_path = "/run/containerd/containerd.sock" namespace = "default" [mcp] -busybox_image = "docker.io/library/memoh-mcp:dev" +image = "docker.io/library/memoh-mcp:dev" snapshotter = "overlayfs" data_root = "data" data_mount = "/data" diff --git a/deploy.sh b/deploy.sh deleted file mode 100755 index 61435ceb..00000000 --- a/deploy.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/bin/bash - -set -e - -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -echo -e "${GREEN}========================================${NC}" -echo -e "${GREEN} Memoh Docker Compose Deployment${NC}" -echo -e "${GREEN}========================================${NC}" -echo "" - -# Check Docker -if ! command -v docker &> /dev/null; then - echo -e "${RED}Error: Docker is not installed${NC}" - echo "Please install Docker first:" - echo " - Linux: curl -fsSL https://get.docker.com | sh" - echo " - macOS: brew install --cask docker" - echo " - Windows: https://docs.docker.com/desktop/install/windows-install/" - echo " - Official guide: https://docs.docker.com/get-docker/" - exit 1 -fi - -# Check Docker Compose -if ! docker compose version &> /dev/null; then - echo -e "${RED}Error: Docker Compose is not installed or version is too old${NC}" - echo "Docker Compose v2.0+ is required (bundled with Docker Desktop)" - echo " - Linux: sudo apt-get install docker-compose-plugin" - echo " - Or follow: https://docs.docker.com/compose/install/" - exit 1 -fi - -echo -e "${GREEN}✓ Docker and Docker Compose are installed${NC}" -echo "" - - -# Check config.toml -if [ ! -f config.toml ]; then - echo -e "${YELLOW}⚠ config.toml does not exist, creating...${NC}" - cp docker/config/config.docker.toml config.toml - echo -e "${GREEN}✓ config.toml created${NC}" - echo "" -fi - -# Prepare data root path for host/containerd compatibility -MEMOH_DATA_ROOT="$(pwd)/.data/memoh" -mkdir -p "${MEMOH_DATA_ROOT}" -export MEMOH_DATA_ROOT - -# Build metadata -export MEMOH_VERSION="${MEMOH_VERSION:-$(git describe --tags --always 2>/dev/null || echo dev)}" -export MEMOH_COMMIT="${MEMOH_COMMIT:-$(git rev-parse --short HEAD 2>/dev/null || echo unknown)}" -export MEMOH_BUILD_TIME="${MEMOH_BUILD_TIME:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}" -echo -e "${GREEN}✓ Version: ${MEMOH_VERSION} (${MEMOH_COMMIT}) built at ${MEMOH_BUILD_TIME}${NC}" -if grep -q '^data_root[[:space:]]*=' config.toml; then - awk -v path="${MEMOH_DATA_ROOT}" ' - $0 ~ /^data_root[[:space:]]*=/ { print "data_root = \"" path "\""; next } - { print } - ' config.toml > config.toml.tmp && mv config.toml.tmp config.toml -fi -echo -e "${GREEN}✓ Data root: ${MEMOH_DATA_ROOT}${NC}" -echo "" - -# Prepare container runtime environment -echo -e "${GREEN}Preparing container runtime environment...${NC}" -if sh scripts/containerd-install.sh > /dev/null 2>&1; then - echo -e "${GREEN}✓ Container runtime environment is ready${NC}" -else - echo -e "${YELLOW}⚠ Failed to prepare container runtime environment, MCP build may be skipped${NC}" -fi -echo "" - -# Build MCP image on host with nerdctl -MCP_IMAGE="docker.io/library/memoh-mcp:latest" -echo -e "${GREEN}Building MCP image on host with nerdctl...${NC}" -if command -v nerdctl &> /dev/null && command -v buildctl &> /dev/null && command -v buildkitd &> /dev/null; then - if nerdctl build -f docker/Dockerfile.mcp -t "${MCP_IMAGE}" . > /dev/null 2>&1; then - echo -e "${GREEN}✓ MCP image built successfully (on host)${NC}" - else - echo -e "${YELLOW}⚠ MCP image build failed on host, will try to pull at runtime${NC}" - fi -else - echo -e "${YELLOW}⚠ nerdctl/buildkit environment not found on host, skipping MCP build${NC}" -fi -echo "" - -# Start services -echo -e "${GREEN}Starting services...${NC}" -docker compose up -d - -echo "" -echo -e "${GREEN}========================================${NC}" -echo -e "${GREEN} Deployment Complete!${NC}" -echo -e "${GREEN}========================================${NC}" -echo "" -echo "Service URLs:" -echo " - Web UI: http://localhost" -echo " - API Service: http://localhost:8080" -echo " - Agent Gateway: http://localhost:8081" -echo "" -echo "View service status:" -echo " docker compose ps" -echo "" -echo "View logs:" -echo " docker compose logs -f" -echo "" -echo "Stop services:" -echo " docker compose down" -echo "" -echo -e "${YELLOW}⚠ First startup may take 1-2 minutes, please be patient${NC}" -echo "" -echo "View detailed documentation: DEPLOYMENT.md" diff --git a/docker-compose.yml b/docker-compose.yml index 3eb88806..66eeadf5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: environment: POSTGRES_DB: memoh POSTGRES_USER: memoh - POSTGRES_PASSWORD: memoh123 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-memoh123} volumes: - postgres_data:/var/lib/postgresql - ./db/migrations:/docker-entrypoint-initdb.d:ro @@ -39,25 +39,15 @@ services: - memoh-network containerd: - image: docker.io/library/alpine:latest + build: + context: . + dockerfile: docker/Dockerfile.containerd container_name: memoh-containerd - entrypoint: ["/bin/sh", "-c"] - command: - - | - apk add --no-cache containerd containerd-ctr - mkdir -p /run/containerd - containerd privileged: true volumes: - containerd_sock:/run/containerd - containerd_data:/var/lib/containerd - memoh_data:/opt/memoh/data - healthcheck: - test: ["CMD-SHELL", "test -S /run/containerd/containerd.sock"] - interval: 5s - timeout: 3s - retries: 10 - start_period: 10s restart: unless-stopped networks: - memoh-network @@ -73,7 +63,7 @@ services: container_name: memoh-server pid: host volumes: - - ./config.toml:/app/config.toml:ro + - ${MEMOH_CONFIG:-./docker/config/config.docker.toml}:/app/config.toml:ro - containerd_sock:/run/containerd - containerd_data:/var/lib/containerd - server_cni_state:/var/lib/cni @@ -103,7 +93,7 @@ services: dockerfile: docker/Dockerfile.agent container_name: memoh-agent volumes: - - ./config.toml:/config.toml:ro + - ${MEMOH_CONFIG:-./docker/config/config.docker.toml}:/config.toml:ro ports: - "8081:8081" healthcheck: diff --git a/docker/Dockerfile.containerd b/docker/Dockerfile.containerd new file mode 100644 index 00000000..6551ee0e --- /dev/null +++ b/docker/Dockerfile.containerd @@ -0,0 +1,75 @@ +# syntax=docker/dockerfile:1 + +# ---- Stage 1: Build MCP binary ---- +FROM golang:1.25-alpine AS mcp-builder + +WORKDIR /src +RUN apk add --no-cache ca-certificates git + +COPY go.mod go.sum ./ +RUN --mount=type=cache,target=/go/pkg/mod \ + go mod download + +COPY . . + +ARG TARGETARCH=amd64 +ARG COMMIT_HASH=unknown +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} \ + go build -trimpath \ + -ldflags "-s -w -X github.com/memohai/memoh/internal/version.CommitHash=${COMMIT_HASH}" \ + -o /out/mcp ./cmd/mcp + +# ---- Stage 2: Assemble MCP image rootfs ---- +FROM alpine:latest AS mcp-rootfs + +RUN apk add --no-cache grep +COPY --from=mcp-builder /out/mcp /opt/mcp +COPY cmd/mcp/template /opt/mcp-template + +RUN printf '#!/bin/sh\n\ +[ -e /app/mcp ] || { mkdir -p /app; [ -f /opt/mcp ] && cp -a /opt/mcp /app/mcp 2>/dev/null || true; }\n\ +if [ -x /app/mcp ]; then exec /app/mcp "$@"; fi\n\ +exec /opt/mcp "$@"\n' > /opt/entrypoint.sh && chmod +x /opt/entrypoint.sh + +# Create rootfs tar excluding pseudo-filesystems +RUN tar -cf /tmp/rootfs.tar \ + --exclude='./proc' --exclude='./sys' --exclude='./dev' \ + --exclude='./tmp' --exclude='./run' \ + -C / . + +# ---- Stage 3: Package rootfs as Docker image tar ---- +FROM alpine:latest AS oci-exporter + +COPY --from=mcp-rootfs /tmp/rootfs.tar /tmp/layer.tar +ARG MCP_IMAGE_TAG=docker.io/library/memoh-mcp:latest + +RUN set -e \ + && LAYER_SHA=$(sha256sum /tmp/layer.tar | awk '{print $1}') \ + && LAYER_SIZE=$(wc -c < /tmp/layer.tar) \ + && mkdir -p "/tmp/image/${LAYER_SHA}" /out \ + && mv /tmp/layer.tar "/tmp/image/${LAYER_SHA}/layer.tar" \ + && printf '{"architecture":"amd64","os":"linux","created":"1970-01-01T00:00:00Z","config":{"Entrypoint":["/opt/entrypoint.sh"],"WorkingDir":"/app","Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]},"rootfs":{"type":"layers","diff_ids":["sha256:%s"]},"history":[{"created":"1970-01-01T00:00:00Z","comment":"memoh-mcp image"}]}' \ + "${LAYER_SHA}" > /tmp/config.json \ + && CONFIG_SHA=$(sha256sum /tmp/config.json | awk '{print $1}') \ + && mv /tmp/config.json "/tmp/image/${CONFIG_SHA}.json" \ + && printf '[{"Config":"%s.json","RepoTags":["%s"],"Layers":["%s/layer.tar"]}]' \ + "${CONFIG_SHA}" "${MCP_IMAGE_TAG}" "${LAYER_SHA}" > /tmp/image/manifest.json \ + && cd /tmp/image && tar -cf /out/memoh-mcp.tar manifest.json "${CONFIG_SHA}.json" "${LAYER_SHA}/" + +# ---- Stage 4: Containerd runtime ---- +FROM alpine:latest + +RUN apk add --no-cache containerd containerd-ctr + +COPY --from=oci-exporter /out/memoh-mcp.tar /opt/images/memoh-mcp.tar +COPY docker/containerd-entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +VOLUME ["/run/containerd", "/var/lib/containerd", "/opt/memoh/data"] + +HEALTHCHECK --interval=5s --timeout=3s --start-period=10s --retries=10 \ + CMD test -S /run/containerd/containerd.sock + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/config/config.docker.toml b/docker/config/config.docker.toml index 376d84d8..46b7d0a0 100644 --- a/docker/config/config.docker.toml +++ b/docker/config/config.docker.toml @@ -23,7 +23,7 @@ socket_path = "/run/containerd/containerd.sock" namespace = "default" [mcp] -busybox_image = "docker.io/library/memoh-mcp:latest" +image = "docker.io/library/memoh-mcp:latest" snapshotter = "overlayfs" data_root = "/opt/memoh/data" data_mount = "/data" diff --git a/docker/containerd-entrypoint.sh b/docker/containerd-entrypoint.sh new file mode 100644 index 00000000..31c811e5 --- /dev/null +++ b/docker/containerd-entrypoint.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +MCP_IMAGE="${MCP_IMAGE:-docker.io/library/memoh-mcp:latest}" + +# Start containerd in background +mkdir -p /run/containerd +containerd & +CONTAINERD_PID=$! + +# Wait for containerd to be fully responsive +echo "Waiting for containerd..." +for i in $(seq 1 30); do + if ctr version >/dev/null 2>&1; then + break + fi + sleep 1 +done + +if ! ctr version >/dev/null 2>&1; then + echo "ERROR: containerd not responsive after 30s" + exit 1 +fi +echo "containerd is running" + +# Import MCP image if not already present +if ! ctr -n default images check "name==${MCP_IMAGE}" 2>/dev/null | grep -q "${MCP_IMAGE}"; then + echo "Importing MCP image into containerd..." + for tar in /opt/images/*.tar; do + if [ -f "$tar" ]; then + ctr -n default images import --all-platforms "$tar" 2>&1 || true + fi + done + if ctr -n default images check "name==${MCP_IMAGE}" 2>/dev/null | grep -q "${MCP_IMAGE}"; then + echo "MCP image ready: ${MCP_IMAGE}" + else + echo "WARNING: MCP image not available after import, will try pull at runtime" + fi +else + echo "MCP image already present: ${MCP_IMAGE}" +fi + +echo "containerd is ready" +wait $CONTAINERD_PID diff --git a/internal/config/config.go b/internal/config/config.go index 3f4d39d6..c4a5ea49 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,7 +12,7 @@ const ( DefaultHTTPAddr = ":8080" DefaultNamespace = "default" DefaultSocketPath = "/run/containerd/containerd.sock" - DefaultBusyboxImg = "docker.io/library/busybox:latest" + DefaultMCPImage = "docker.io/library/memoh-mcp:latest" DefaultDataRoot = "data" DefaultDataMount = "/data" DefaultJWTExpiresIn = "24h" @@ -63,10 +63,10 @@ type ContainerdConfig struct { } type MCPConfig struct { - BusyboxImage string `toml:"busybox_image"` - Snapshotter string `toml:"snapshotter"` - DataRoot string `toml:"data_root"` - DataMount string `toml:"data_mount"` + Image string `toml:"image"` + Snapshotter string `toml:"snapshotter"` + DataRoot string `toml:"data_root"` + DataMount string `toml:"data_mount"` } type PostgresConfig struct { @@ -124,9 +124,9 @@ func Load(path string) (Config, error) { Namespace: DefaultNamespace, }, MCP: MCPConfig{ - BusyboxImage: DefaultBusyboxImg, - DataRoot: DefaultDataRoot, - DataMount: DefaultDataMount, + Image: DefaultMCPImage, + DataRoot: DefaultDataRoot, + DataMount: DefaultDataMount, }, Postgres: PostgresConfig{ Host: DefaultPGHost, diff --git a/internal/handlers/containerd.go b/internal/handlers/containerd.go index 8e0753bc..d5ace680 100644 --- a/internal/handlers/containerd.go +++ b/internal/handlers/containerd.go @@ -152,7 +152,7 @@ func (h *ContainerdHandler) CreateContainer(c echo.Context) error { } containerID := mcp.ContainerPrefix + botID - image := mcp.DefaultImageRef + image := h.mcpImageRef() snapshotter := strings.TrimSpace(req.Snapshotter) if snapshotter == "" { snapshotter = h.cfg.Snapshotter @@ -622,6 +622,13 @@ func (h *ContainerdHandler) ListSnapshots(c echo.Context) error { // ---------- auth helpers ---------- +func (h *ContainerdHandler) mcpImageRef() string { + if h.cfg.Image != "" { + return h.cfg.Image + } + return config.DefaultMCPImage +} + // requireBotAccess extracts bot_id from path, validates user auth, and authorizes bot access. func (h *ContainerdHandler) requireBotAccess(c echo.Context) (string, error) { channelIdentityID, err := h.requireChannelIdentityID(c) @@ -683,7 +690,7 @@ func (h *ContainerdHandler) authorizeBotAccess(ctx context.Context, channelIdent func (h *ContainerdHandler) SetupBotContainer(ctx context.Context, botID string) error { containerID := mcp.ContainerPrefix + botID - image := mcp.DefaultImageRef + image := h.mcpImageRef() snapshotter := strings.TrimSpace(h.cfg.Snapshotter) if strings.TrimSpace(h.namespace) != "" { diff --git a/internal/mcp/manager.go b/internal/mcp/manager.go index 9ad442b8..48c75e01 100644 --- a/internal/mcp/manager.go +++ b/internal/mcp/manager.go @@ -27,7 +27,6 @@ import ( const ( BotLabelKey = "mcp.bot_id" ContainerPrefix = "mcp-" - DefaultImageRef = "memoh-mcp:dev" ) type ExecRequest struct { @@ -78,7 +77,7 @@ func NewManager(log *slog.Logger, service ctr.Service, cfg config.MCPConfig, nam } func (m *Manager) Init(ctx context.Context) error { - image := DefaultImageRef + image := m.imageRef() _, err := m.service.PullImage(ctx, image, &ctr.PullImageOptions{ Unpack: true, @@ -388,7 +387,10 @@ func (m *Manager) dataMount() string { } func (m *Manager) imageRef() string { - return DefaultImageRef + if m.cfg.Image != "" { + return m.cfg.Image + } + return config.DefaultMCPImage } func validateBotID(botID string) error { diff --git a/mise.toml b/mise.toml index 544089b7..36ec3393 100644 --- a/mise.toml +++ b/mise.toml @@ -40,10 +40,6 @@ if [ "$(uname -s)" = "Darwin" ]; then fi """ -[tasks.containerd-install] -description = "Install containerd or verify in Lima" -run = "scripts/containerd-install.sh" - [tasks.swagger-generate] description = "Generate Swagger documentation" run = "cd internal/handlers && go generate" @@ -79,14 +75,6 @@ chmod +x ~/.local/bin/memoh-cli echo "✓ CLI binary installed to ~/.local/bin/memoh-cli" """ -[tasks.mcp-image-up] -description = "Build MCP container image" -run = "scripts/mcp-image-up.sh" - -[tasks.mcp-image-down] -description = "Remove MCP container image" -run = "scripts/mcp-image-down.sh" - [tasks.compile-mcp] description = "Build MCP binary into /app and signal container" run = "scripts/compile-mcp.sh" diff --git a/scripts/containerd-install.sh b/scripts/containerd-install.sh deleted file mode 100755 index 085c4f09..00000000 --- a/scripts/containerd-install.sh +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env sh -set -e - -detect_distro() { - DISTRO_ID="unknown" - DISTRO_LIKE="" - - # shellcheck disable=SC1091 - if [ -r /etc/os-release ]; then - . /etc/os-release - if [ -n "${ID:-}" ]; then - DISTRO_ID="$ID" - fi - if [ -n "${ID_LIKE:-}" ]; then - DISTRO_LIKE="$ID_LIKE" - fi - fi -} - -detect_distro - -if [ "$(uname -s)" = "Darwin" ]; then - limactl start default - limactl shell default -- sudo containerd --version - exit $? -fi - -if command -v containerd >/dev/null 2>&1 \ - && command -v nerdctl >/dev/null 2>&1 \ - && command -v buildctl >/dev/null 2>&1 \ - && command -v buildkitd >/dev/null 2>&1; then - containerd --version - nerdctl --version - buildctl --version - exit 0 -fi - -if ! command -v containerd >/dev/null 2>&1; then - echo "Detected distro: ${DISTRO_ID}${DISTRO_LIKE:+ (like: $DISTRO_LIKE)}" - - if command -v apt-get >/dev/null 2>&1; then - sudo apt-get update - # Debian/Ubuntu usually provide "containerd"; some setups use "containerd.io". - sudo apt-get install -y containerd || sudo apt-get install -y containerd.io - elif command -v dnf >/dev/null 2>&1; then - sudo dnf install -y containerd || sudo dnf install -y containerd.io - elif command -v yum >/dev/null 2>&1; then - sudo yum install -y containerd || sudo yum install -y containerd.io - elif command -v apk >/dev/null 2>&1; then - sudo apk add --no-cache containerd - elif command -v zypper >/dev/null 2>&1; then - sudo zypper --non-interactive install -y containerd - elif command -v pacman >/dev/null 2>&1; then - sudo pacman -Sy --noconfirm containerd - else - echo "No supported package manager found. Install containerd manually." - exit 1 - fi -fi - -if ! command -v nerdctl >/dev/null 2>&1 || ! command -v buildctl >/dev/null 2>&1 || ! command -v buildkitd >/dev/null 2>&1; then - OS="$(uname -s | tr '[:upper:]' '[:lower:]')" - ARCH="$(uname -m)" - NERDCTL_VERSION="${NERDCTL_VERSION:-}" - - if [ "$OS" != "linux" ]; then - echo "Automatic nerdctl installation from release is only supported on Linux." - exit 1 - fi - - case "$ARCH" in - x86_64|amd64) - ARCH="amd64" - ;; - aarch64|arm64) - ARCH="arm64" - ;; - *) - echo "Unsupported architecture for nerdctl release: $ARCH" - exit 1 - ;; - esac - - if [ -z "$NERDCTL_VERSION" ]; then - RELEASES_API_URL="https://api.github.com/repos/containerd/nerdctl/releases/latest" - if command -v curl >/dev/null 2>&1; then - NERDCTL_VERSION="$(curl -fsSL "$RELEASES_API_URL" | sed -n 's/.*"tag_name":[[:space:]]*"v\{0,1\}\([^"]*\)".*/\1/p' | head -n1)" - elif command -v wget >/dev/null 2>&1; then - NERDCTL_VERSION="$(wget -qO- "$RELEASES_API_URL" | sed -n 's/.*"tag_name":[[:space:]]*"v\{0,1\}\([^"]*\)".*/\1/p' | head -n1)" - fi - fi - - if [ -z "$NERDCTL_VERSION" ]; then - echo "Failed to detect latest nerdctl version. Set NERDCTL_VERSION manually." - exit 1 - fi - - NERDCTL_TARBALL="nerdctl-full-${NERDCTL_VERSION}-linux-${ARCH}.tar.gz" - NERDCTL_URL="https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/${NERDCTL_TARBALL}" - TMP_DIR="$(mktemp -d)" - TMP_TARBALL="${TMP_DIR}/${NERDCTL_TARBALL}" - - cleanup() { - rm -rf "$TMP_DIR" - } - trap cleanup EXIT INT TERM - - if command -v curl >/dev/null 2>&1; then - curl -fsSL "$NERDCTL_URL" -o "$TMP_TARBALL" - elif command -v wget >/dev/null 2>&1; then - wget -qO "$TMP_TARBALL" "$NERDCTL_URL" - else - echo "curl or wget is required to download nerdctl." - exit 1 - fi - - tar -xzf "$TMP_TARBALL" -C "$TMP_DIR" - sudo install -m 0755 "$TMP_DIR/bin/nerdctl" /usr/local/bin/nerdctl - sudo install -m 0755 "$TMP_DIR/bin/buildctl" /usr/local/bin/buildctl - sudo install -m 0755 "$TMP_DIR/bin/buildkitd" /usr/local/bin/buildkitd - - if command -v systemctl >/dev/null 2>&1 && [ -f "$TMP_DIR/lib/systemd/system/buildkit.service" ]; then - sudo install -m 0644 "$TMP_DIR/lib/systemd/system/buildkit.service" /etc/systemd/system/buildkit.service - sudo systemctl daemon-reload - sudo systemctl enable --now buildkit.service || true - fi -fi - -containerd --version -nerdctl --version -buildctl --version diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 00000000..fc3954a7 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,126 @@ +#!/bin/sh +set -e + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +REPO="https://github.com/memohai/Memoh.git" +BRANCH="feat/containerd-in-docker" +DIR="Memoh" +SILENT=false + +# Parse flags +for arg in "$@"; do + case "$arg" in + -y|--yes) SILENT=true ;; + esac +done + +# Auto-silent if no TTY available +if [ "$SILENT" = false ] && ! [ -e /dev/tty ]; then + SILENT=true +fi + +echo "${GREEN}========================================${NC}" +echo "${GREEN} Memoh One-Click Install${NC}" +echo "${GREEN}========================================${NC}" +echo "" + +# Check Docker +if ! command -v docker >/dev/null 2>&1; then + echo "${RED}Error: Docker is not installed${NC}" + echo "Install Docker first: https://docs.docker.com/get-docker/" + exit 1 +fi +if ! docker compose version >/dev/null 2>&1; then + echo "${RED}Error: Docker Compose v2 is required${NC}" + echo "Install: https://docs.docker.com/compose/install/" + exit 1 +fi +echo "${GREEN}✓ Docker and Docker Compose detected${NC}" +echo "" + +# Generate random JWT secret +gen_secret() { + if command -v openssl >/dev/null 2>&1; then + openssl rand -base64 32 + else + head -c 32 /dev/urandom | base64 | tr -d '\n' + fi +} + +# Configuration defaults +ADMIN_USER="admin" +ADMIN_PASS="admin123" +JWT_SECRET="$(gen_secret)" +PG_PASS="memoh123" + +if [ "$SILENT" = false ]; then + echo "Configure Memoh (press Enter to use defaults):" > /dev/tty + echo "" > /dev/tty + + printf " Admin username [%s]: " "$ADMIN_USER" > /dev/tty + read -r input < /dev/tty || true + [ -n "$input" ] && ADMIN_USER="$input" + + printf " Admin password [%s]: " "$ADMIN_PASS" > /dev/tty + read -r input < /dev/tty || true + [ -n "$input" ] && ADMIN_PASS="$input" + + printf " JWT secret [auto-generated]: " > /dev/tty + read -r input < /dev/tty || true + [ -n "$input" ] && JWT_SECRET="$input" + + printf " Postgres password [%s]: " "$PG_PASS" > /dev/tty + read -r input < /dev/tty || true + [ -n "$input" ] && PG_PASS="$input" + + echo "" > /dev/tty +fi + +# Clone or update +if [ -d "$DIR" ]; then + echo "Updating existing installation..." + cd "$DIR" + git pull --ff-only 2>/dev/null || true +else + echo "Cloning Memoh..." + git clone --depth 1 -b "$BRANCH" "$REPO" "$DIR" + cd "$DIR" +fi + +# Generate config.toml from template +cp docker/config/config.docker.toml config.toml +sed -i.bak "s|username = \"admin\"|username = \"${ADMIN_USER}\"|" config.toml +sed -i.bak "s|password = \"admin123\"|password = \"${ADMIN_PASS}\"|" config.toml +sed -i.bak "s|jwt_secret = \".*\"|jwt_secret = \"${JWT_SECRET}\"|" config.toml +sed -i.bak "s|password = \"memoh123\"|password = \"${PG_PASS}\"|" config.toml +export POSTGRES_PASSWORD="${PG_PASS}" +rm -f config.toml.bak + +# Use generated config +export MEMOH_CONFIG=./config.toml + +echo "" +echo "${GREEN}Starting services (first build may take a few minutes)...${NC}" +docker compose up -d --build + +echo "" +echo "${GREEN}========================================${NC}" +echo "${GREEN} Memoh is running!${NC}" +echo "${GREEN}========================================${NC}" +echo "" +echo " Web UI: http://localhost" +echo " API: http://localhost:8080" +echo " Agent Gateway: http://localhost:8081" +echo "" +echo " Admin login: ${ADMIN_USER} / ${ADMIN_PASS}" +echo "" +echo "Commands:" +echo " cd ${DIR} && docker compose ps # Status" +echo " cd ${DIR} && docker compose logs -f # Logs" +echo " cd ${DIR} && docker compose down # Stop" +echo "" +echo "${YELLOW}First startup may take 1-2 minutes, please be patient.${NC}" diff --git a/scripts/mcp-image-down.sh b/scripts/mcp-image-down.sh deleted file mode 100755 index 51743113..00000000 --- a/scripts/mcp-image-down.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env sh -set -e - -IMAGE="memoh-mcp:dev" - -if [ "$(uname -s)" = "Darwin" ]; then - limactl shell default -- nerdctl rmi -f "$IMAGE" - exit $? -fi - -if ! command -v nerdctl >/dev/null 2>&1; then - echo "nerdctl not found. Install nerdctl to remove images." - exit 1 -fi - -nerdctl rmi -f "$IMAGE" diff --git a/scripts/mcp-image-up.sh b/scripts/mcp-image-up.sh deleted file mode 100755 index f849552d..00000000 --- a/scripts/mcp-image-up.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env sh -set -e - -IMAGE="memoh-mcp:dev" - -if [ "$(uname -s)" = "Darwin" ]; then - limactl shell default -- nerdctl build -f docker/Dockerfile.mcp -t "$IMAGE" . - # Import into rootful containerd so the Go agent can find the image - limactl shell default -- sh -c "nerdctl save $IMAGE | sudo nerdctl load" - exit $? -fi - -if ! command -v nerdctl >/dev/null 2>&1; then - echo "nerdctl not found. Install nerdctl to build images." - exit 1 -fi - -nerdctl build -f docker/Dockerfile.mcp -t "$IMAGE" .