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
## 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
+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.
## 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.
+1 -1
View File
@@ -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"
-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:
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:
+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"
[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"
+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"
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,
+9 -2
View File
@@ -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) != "" {
+5 -3
View File
@@ -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 {
-12
View File
@@ -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"
-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" .