feat(deploy): self-contained containerd with embedded MCP image

- Add Dockerfile.containerd: multi-stage build that compiles MCP binary,
  assembles rootfs, creates Docker image tar, and bundles it with containerd
- Add containerd-entrypoint.sh: auto-imports MCP image on first start
- Fix MCP image reference: rename busybox_image to image in config,
  use fully-qualified docker.io/library/memoh-mcp:latest everywhere
- Make image ref configurable via config.toml instead of hardcoded
- Simplify deploy.sh: remove manual nerdctl/containerd-install steps
This commit is contained in:
BBQ
2026-02-12 21:25:43 +08:00
parent e63d335c7e
commit c53d35740e
16 changed files with 322 additions and 351 deletions
+26 -15
View File
@@ -1,11 +1,17 @@
# Memoh Deployment Guide # 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 ```bash
git clone https://github.com/memohai/Memoh.git git clone https://github.com/memohai/Memoh.git
cd Memoh cd Memoh
./deploy.sh docker compose up -d
``` ```
Access: Access:
@@ -15,18 +21,22 @@ Access:
Default credentials: `admin` / `admin123` 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 ```bash
cp docker/config/config.docker.toml config.toml cp docker/config/config.docker.toml config.toml
nano config.toml # Change passwords and secrets nano config.toml
nerdctl build -f docker/Dockerfile.mcp -t docker.io/library/memoh-mcp:latest . MEMOH_CONFIG=./config.toml docker compose up -d
docker compose up -d
``` ```
## Required Configuration Recommended changes for production:
Must change in `config.toml`:
- `admin.password` - Admin password - `admin.password` - Admin password
- `auth.jwt_secret` - JWT secret (generate with `openssl rand -base64 32`) - `auth.jwt_secret` - JWT secret (generate with `openssl rand -base64 32`)
- `postgres.password` - Database password - `postgres.password` - Database password
@@ -37,7 +47,8 @@ Must change in `config.toml`:
docker compose up -d # Start docker compose up -d # Start
docker compose down # Stop docker compose down # Stop
docker compose logs -f # View logs 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 ## Production
@@ -52,13 +63,13 @@ nerdctl images # Ensure that memoh-mcp:latest exsits
```bash ```bash
docker compose logs server # View service logs docker compose logs server # View service logs
docker compose logs containerd # View containerd logs
docker compose config # Check configuration 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 ## Security Warnings
⚠️ Main service has host Docker access - only run in trusted environments - Main service has privileged container access - only run in trusted environments
⚠️ Must change all default passwords and secrets - Must change all default passwords and secrets
⚠️ Use HTTPS in production - Use HTTPS in production
+22 -14
View File
@@ -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. 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? ## 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. 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. 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 ### Development
Refer to [CONTRIBUTING.md](CONTRIBUTING.md) for development setup. Refer to [CONTRIBUTING.md](CONTRIBUTING.md) for development setup.
+1 -1
View File
@@ -24,7 +24,7 @@ socket_path = "/run/containerd/containerd.sock"
namespace = "default" namespace = "default"
[mcp] [mcp]
busybox_image = "docker.io/library/memoh-mcp:dev" image = "docker.io/library/memoh-mcp:dev"
snapshotter = "overlayfs" snapshotter = "overlayfs"
data_root = "data" data_root = "data"
data_mount = "/data" data_mount = "/data"
-114
View File
@@ -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"
+6 -16
View File
@@ -6,7 +6,7 @@ services:
environment: environment:
POSTGRES_DB: memoh POSTGRES_DB: memoh
POSTGRES_USER: memoh POSTGRES_USER: memoh
POSTGRES_PASSWORD: memoh123 POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-memoh123}
volumes: volumes:
- postgres_data:/var/lib/postgresql - postgres_data:/var/lib/postgresql
- ./db/migrations:/docker-entrypoint-initdb.d:ro - ./db/migrations:/docker-entrypoint-initdb.d:ro
@@ -39,25 +39,15 @@ services:
- memoh-network - memoh-network
containerd: containerd:
image: docker.io/library/alpine:latest build:
context: .
dockerfile: docker/Dockerfile.containerd
container_name: memoh-containerd container_name: memoh-containerd
entrypoint: ["/bin/sh", "-c"]
command:
- |
apk add --no-cache containerd containerd-ctr
mkdir -p /run/containerd
containerd
privileged: true privileged: true
volumes: volumes:
- containerd_sock:/run/containerd - containerd_sock:/run/containerd
- containerd_data:/var/lib/containerd - containerd_data:/var/lib/containerd
- memoh_data:/opt/memoh/data - 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 restart: unless-stopped
networks: networks:
- memoh-network - memoh-network
@@ -73,7 +63,7 @@ services:
container_name: memoh-server container_name: memoh-server
pid: host pid: host
volumes: volumes:
- ./config.toml:/app/config.toml:ro - ${MEMOH_CONFIG:-./docker/config/config.docker.toml}:/app/config.toml:ro
- containerd_sock:/run/containerd - containerd_sock:/run/containerd
- containerd_data:/var/lib/containerd - containerd_data:/var/lib/containerd
- server_cni_state:/var/lib/cni - server_cni_state:/var/lib/cni
@@ -103,7 +93,7 @@ services:
dockerfile: docker/Dockerfile.agent dockerfile: docker/Dockerfile.agent
container_name: memoh-agent container_name: memoh-agent
volumes: volumes:
- ./config.toml:/config.toml:ro - ${MEMOH_CONFIG:-./docker/config/config.docker.toml}:/config.toml:ro
ports: ports:
- "8081:8081" - "8081:8081"
healthcheck: healthcheck:
+75
View File
@@ -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"]
+1 -1
View File
@@ -23,7 +23,7 @@ socket_path = "/run/containerd/containerd.sock"
namespace = "default" namespace = "default"
[mcp] [mcp]
busybox_image = "docker.io/library/memoh-mcp:latest" image = "docker.io/library/memoh-mcp:latest"
snapshotter = "overlayfs" snapshotter = "overlayfs"
data_root = "/opt/memoh/data" data_root = "/opt/memoh/data"
data_mount = "/data" data_mount = "/data"
+43
View File
@@ -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
+8 -8
View File
@@ -12,7 +12,7 @@ const (
DefaultHTTPAddr = ":8080" DefaultHTTPAddr = ":8080"
DefaultNamespace = "default" DefaultNamespace = "default"
DefaultSocketPath = "/run/containerd/containerd.sock" DefaultSocketPath = "/run/containerd/containerd.sock"
DefaultBusyboxImg = "docker.io/library/busybox:latest" DefaultMCPImage = "docker.io/library/memoh-mcp:latest"
DefaultDataRoot = "data" DefaultDataRoot = "data"
DefaultDataMount = "/data" DefaultDataMount = "/data"
DefaultJWTExpiresIn = "24h" DefaultJWTExpiresIn = "24h"
@@ -63,10 +63,10 @@ type ContainerdConfig struct {
} }
type MCPConfig struct { type MCPConfig struct {
BusyboxImage string `toml:"busybox_image"` Image string `toml:"image"`
Snapshotter string `toml:"snapshotter"` Snapshotter string `toml:"snapshotter"`
DataRoot string `toml:"data_root"` DataRoot string `toml:"data_root"`
DataMount string `toml:"data_mount"` DataMount string `toml:"data_mount"`
} }
type PostgresConfig struct { type PostgresConfig struct {
@@ -124,9 +124,9 @@ func Load(path string) (Config, error) {
Namespace: DefaultNamespace, Namespace: DefaultNamespace,
}, },
MCP: MCPConfig{ MCP: MCPConfig{
BusyboxImage: DefaultBusyboxImg, Image: DefaultMCPImage,
DataRoot: DefaultDataRoot, DataRoot: DefaultDataRoot,
DataMount: DefaultDataMount, DataMount: DefaultDataMount,
}, },
Postgres: PostgresConfig{ Postgres: PostgresConfig{
Host: DefaultPGHost, Host: DefaultPGHost,
+9 -2
View File
@@ -152,7 +152,7 @@ func (h *ContainerdHandler) CreateContainer(c echo.Context) error {
} }
containerID := mcp.ContainerPrefix + botID containerID := mcp.ContainerPrefix + botID
image := mcp.DefaultImageRef image := h.mcpImageRef()
snapshotter := strings.TrimSpace(req.Snapshotter) snapshotter := strings.TrimSpace(req.Snapshotter)
if snapshotter == "" { if snapshotter == "" {
snapshotter = h.cfg.Snapshotter snapshotter = h.cfg.Snapshotter
@@ -622,6 +622,13 @@ func (h *ContainerdHandler) ListSnapshots(c echo.Context) error {
// ---------- auth helpers ---------- // ---------- 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. // requireBotAccess extracts bot_id from path, validates user auth, and authorizes bot access.
func (h *ContainerdHandler) requireBotAccess(c echo.Context) (string, error) { func (h *ContainerdHandler) requireBotAccess(c echo.Context) (string, error) {
channelIdentityID, err := h.requireChannelIdentityID(c) 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 { func (h *ContainerdHandler) SetupBotContainer(ctx context.Context, botID string) error {
containerID := mcp.ContainerPrefix + botID containerID := mcp.ContainerPrefix + botID
image := mcp.DefaultImageRef image := h.mcpImageRef()
snapshotter := strings.TrimSpace(h.cfg.Snapshotter) snapshotter := strings.TrimSpace(h.cfg.Snapshotter)
if strings.TrimSpace(h.namespace) != "" { if strings.TrimSpace(h.namespace) != "" {
+5 -3
View File
@@ -27,7 +27,6 @@ import (
const ( const (
BotLabelKey = "mcp.bot_id" BotLabelKey = "mcp.bot_id"
ContainerPrefix = "mcp-" ContainerPrefix = "mcp-"
DefaultImageRef = "memoh-mcp:dev"
) )
type ExecRequest struct { 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 { func (m *Manager) Init(ctx context.Context) error {
image := DefaultImageRef image := m.imageRef()
_, err := m.service.PullImage(ctx, image, &ctr.PullImageOptions{ _, err := m.service.PullImage(ctx, image, &ctr.PullImageOptions{
Unpack: true, Unpack: true,
@@ -388,7 +387,10 @@ func (m *Manager) dataMount() string {
} }
func (m *Manager) imageRef() string { func (m *Manager) imageRef() string {
return DefaultImageRef if m.cfg.Image != "" {
return m.cfg.Image
}
return config.DefaultMCPImage
} }
func validateBotID(botID string) error { func validateBotID(botID string) error {
-12
View File
@@ -40,10 +40,6 @@ if [ "$(uname -s)" = "Darwin" ]; then
fi fi
""" """
[tasks.containerd-install]
description = "Install containerd or verify in Lima"
run = "scripts/containerd-install.sh"
[tasks.swagger-generate] [tasks.swagger-generate]
description = "Generate Swagger documentation" description = "Generate Swagger documentation"
run = "cd internal/handlers && go generate" 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" 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] [tasks.compile-mcp]
description = "Build MCP binary into /app and signal container" description = "Build MCP binary into /app and signal container"
run = "scripts/compile-mcp.sh" run = "scripts/compile-mcp.sh"
-131
View File
@@ -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
+126
View File
@@ -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}"
-16
View File
@@ -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"
-18
View File
@@ -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" .