tec/docs/webvm-selfhost.md

374 lines
7.8 KiB
Markdown

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" <<ENV
WEBVM_DOMAIN=$WEBVM_DOMAIN
WEBVM_IMAGE=$WEBVM_IMAGE
WEBVM_ROOT=$ROOT_DIR
LETSENCRYPT_EMAIL=$LETSENCRYPT_EMAIL
ENV
cat > "$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 - <<PY
from pathlib import Path
import re
p = Path("$CONFIG_FILE")
s = p.read_text()
s = re.sub(
r'export const diskImageUrl\\s*=\\s*".*?";',
'export const diskImageUrl = "/custom-disk-images/$IMG_NAME";',
s
)
s = re.sub(
r'export const diskImageType\\s*=\\s*".*?";',
'export const diskImageType = "bytes";',
s
)
p.write_text(s)
print("patched", p)
PY
echo "==> 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.