diff --git a/extra/docs/webvm.md b/extra/docs/webvm.md new file mode 100644 index 0000000..afee5e4 --- /dev/null +++ b/extra/docs/webvm.md @@ -0,0 +1,373 @@ +webvm for self host + +--- + +install script: + +- install required packages +- create `/opt/webvm-aio` +- ask for: + - domain + - Let's Encrypt email + - image filename +- write: + - `.env` + - `docker-compose.caddy.yml` + - `docker-compose.webvm.yml` + - `Caddyfile` + - `nginx.conf` + - `bootstrap.sh` + - `deploy.sh` +- create the shared Docker network `web` +- start Caddy +- tell you where to upload your `.ext2` image +- optionally bootstrap immediately if the image is already present + +--- + +### copy-paste + +[debian .ext2](https://github.com/leaningtech/webvm/releases/download/ext2_image/debian_mini_20230519_5022088024.ext2) + +[alpine .ext2](https://github.com/leaningtech/alpine-image/tree/master) + +run as sudo-capable user: + +```bash +bash <(cat <<'EOF' +set -euo pipefail + +ROOT_DIR="/opt/webvm-aio" + +echo "==> WebVM AIO installer" + +if ! command -v sudo >/dev/null 2>&1; then + echo "sudo is required" + exit 1 +fi + +read -rp "Domain (e.g. webvm.example.com): " WEBVM_DOMAIN +read -rp "Let's Encrypt email: " LETSENCRYPT_EMAIL +read -rp "Disk image filename (e.g. debian_mini_20230519_5022088024.ext2): " WEBVM_IMAGE + +if [ -z "${WEBVM_DOMAIN}" ] || [ -z "${LETSENCRYPT_EMAIL}" ] || [ -z "${WEBVM_IMAGE}" ]; then + echo "All fields are required." + exit 1 +fi + +echo "==> Installing packages" +sudo apt update +sudo apt install -y git curl docker.io docker-compose-plugin nodejs npm python3 + +echo "==> Enabling docker" +sudo systemctl enable --now docker + +echo "==> Creating layout at $ROOT_DIR" +sudo mkdir -p "$ROOT_DIR" +sudo chown -R "$USER":"$USER" "$ROOT_DIR" + +mkdir -p "$ROOT_DIR"/{images,index,src} +mkdir -p "$ROOT_DIR/index/custom-disk-images" + +cat > "$ROOT_DIR/.env" < "$ROOT_DIR/docker-compose.caddy.yml" <<'YAML' +version: "3.9" + +services: + caddy: + image: lucaslorentz/caddy-docker-proxy:2.9-alpine + container_name: caddy + restart: unless-stopped + ports: + - "80:80" + - "443:443" + environment: + - CADDY_INGRESS_NETWORKS=web + networks: + - web + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + +networks: + web: + external: true + +volumes: + caddy_data: + caddy_config: +YAML + +cat > "$ROOT_DIR/docker-compose.webvm.yml" <<'YAML' +version: "3.9" + +services: + webvm: + image: nginx:alpine + container_name: webvm + restart: unless-stopped + networks: + - web + volumes: + - ./index:/usr/share/nginx/html:ro + - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + labels: + caddy: ${WEBVM_DOMAIN} + caddy.reverse_proxy: "{{upstreams 80}}" + caddy.encode: zstd gzip + +networks: + web: + external: true +YAML + +cat > "$ROOT_DIR/Caddyfile" <<'CADDY' +{ + email {$LETSENCRYPT_EMAIL} +} +CADDY + +cat > "$ROOT_DIR/nginx.conf" <<'NGINX' +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + types { + application/wasm wasm; + } + + add_header Cross-Origin-Opener-Policy same-origin always; + add_header Cross-Origin-Embedder-Policy require-corp always; + add_header Cross-Origin-Resource-Policy same-origin always; + add_header X-Content-Type-Options nosniff always; + + location / { + try_files $uri $uri/ /index.html; + } + + location /custom-disk-images/ { + autoindex off; + add_header Cache-Control "public, max-age=3600"; + try_files $uri =404; + } + + location ~* \.(wasm|bin|img|ext2)$ { + add_header Cache-Control "public, max-age=3600"; + try_files $uri =404; + } + + location ~* \.(js|mjs|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf)$ { + add_header Cache-Control "public, max-age=31536000, immutable"; + try_files $uri =404; + } + + client_max_body_size 50m; +} +NGINX + +cat > "$ROOT_DIR/bootstrap.sh" <<'BOOT' +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="${WEBVM_ROOT:-/opt/webvm-aio}" +if [ -f "$ROOT_DIR/.env" ]; then + set -a + . "$ROOT_DIR/.env" + set +a +fi + +SRC_DIR="$ROOT_DIR/src/webvm" +PUB_DIR="$ROOT_DIR/index" +IMG_SRC_DIR="$ROOT_DIR/images" +IMG_NAME="${WEBVM_IMAGE:-debian_mini_20230519_5022088024.ext2}" +IMG_SRC="$IMG_SRC_DIR/$IMG_NAME" +IMG_PUB_DIR="$PUB_DIR/custom-disk-images" +IMG_PUB="$IMG_PUB_DIR/$IMG_NAME" + +echo "==> root: $ROOT_DIR" +echo "==> source: $SRC_DIR" +echo "==> publish: $PUB_DIR" +echo "==> image source: $IMG_SRC" + +mkdir -p "$ROOT_DIR" "$ROOT_DIR/src" "$PUB_DIR" "$IMG_SRC_DIR" "$IMG_PUB_DIR" + +if ! command -v docker >/dev/null 2>&1; then + echo "ERROR: docker is not installed" + exit 1 +fi + +if ! docker network inspect web >/dev/null 2>&1; then + echo "==> creating shared docker network: web" + docker network create web +fi + +echo "==> starting caddy" +cd "$ROOT_DIR" +docker compose --env-file .env -f docker-compose.caddy.yml up -d + +if [ ! -f "$IMG_SRC" ]; then + echo + echo "ERROR: missing disk image:" + echo " $IMG_SRC" + echo + echo "Upload your .ext2 image there, then rerun:" + echo " $ROOT_DIR/bootstrap.sh" + echo + exit 1 +fi + +if [ ! -d "$SRC_DIR/.git" ]; then + echo "==> cloning webvm" + git clone https://github.com/leaningtech/webvm.git "$SRC_DIR" +else + echo "==> updating webvm" + git -C "$SRC_DIR" pull --ff-only +fi + +cd "$SRC_DIR" + +echo "==> installing dependencies" +npm install + +CONFIG_FILE="$SRC_DIR/config_public_terminal.js" + +echo "==> patching WebVM config" +python3 - < building webvm" +npm install +npm run build + +echo "==> publishing build" +rm -rf "$PUB_DIR"/* +mkdir -p "$IMG_PUB_DIR" + +if [ -d "$SRC_DIR/dist" ]; then + cp -r "$SRC_DIR/dist/"* "$PUB_DIR"/ +else + echo "ERROR: expected build output in $SRC_DIR/dist" + exit 1 +fi + +cp "$IMG_SRC" "$IMG_PUB" + +echo "==> starting webvm" +cd "$ROOT_DIR" +docker compose --env-file .env -f docker-compose.webvm.yml up -d + +echo +echo "Done." +echo "Site: https://${WEBVM_DOMAIN}" +echo "Image: https://${WEBVM_DOMAIN}/custom-disk-images/$IMG_NAME" +BOOT + +cat > "$ROOT_DIR/deploy.sh" <<'DEPLOY' +#!/usr/bin/env bash +set -euo pipefail +ROOT_DIR="/opt/webvm-aio" +cd "$ROOT_DIR" +./bootstrap.sh +docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" +DEPLOY + +chmod +x "$ROOT_DIR/bootstrap.sh" "$ROOT_DIR/deploy.sh" + +echo "==> Ensuring docker network exists" +docker network inspect web >/dev/null 2>&1 || docker network create web + +echo "==> Starting Caddy" +cd "$ROOT_DIR" +docker compose --env-file .env -f docker-compose.caddy.yml up -d + +echo +echo "==> Install complete" +echo +echo "Next steps:" +echo "1. Point DNS for $WEBVM_DOMAIN to this server." +echo "2. Upload your disk image to:" +echo " $ROOT_DIR/images/$WEBVM_IMAGE" +echo +echo "Example upload:" +echo " scp $WEBVM_IMAGE $USER@$(hostname -I | awk '{print $1}'):$ROOT_DIR/images/" +echo +read -rp "If the image is already present, bootstrap now? [y/N]: " DO_BOOTSTRAP +if [[ "${DO_BOOTSTRAP:-}" =~ ^[Yy]$ ]]; then + "$ROOT_DIR/bootstrap.sh" +else + echo + echo "When ready, run:" + echo " cd $ROOT_DIR && ./bootstrap.sh" +fi +EOF +) +``` + +--- + +## After running it + +### 1. Point DNS +Set `A` record for your domain to your VPS IP. + +### 2. Upload the `.ext2` image +Example: + +```bash +scp debian_mini_20230519_5022088024.ext2 user@your-server:/opt/webvm-aio/images/ +``` + +### 3. Bootstrap +```bash +cd /opt/webvm-aio +./bootstrap.sh +``` + +### 4. Open +```text +https://your-domain +``` + +--- + +## Notes + +### If ports 80/443 are already used +Stop any existing web server before running Caddy: + +```bash +sudo systemctl stop nginx +sudo systemctl disable nginx +``` + +or stop any existing container using 80/443.