Unified install/update/destroy script with ephemeral build dir

- Replace deploy.sh and install.sh with tools/nextwks.sh
- Build in /tmp/nextwks-build (fresh clone every time), no more /opt/NextWks
- Script saves itself to ~/nextwks.sh on --install for easy future access
- Add AGENT.md with workflow rules for the new approach
- Secrets persisted in /opt/backup/.env (JWT, SESSION, password hash)
- Update README and CHANGELOG
This commit is contained in:
Claus Lohmar 2026-07-11 00:02:55 +01:00
parent 07288f4e19
commit 81e5505f2a
7 changed files with 396 additions and 301 deletions

100
AGENT.md Normal file
View file

@ -0,0 +1,100 @@
# NextWorkspace — Agent Workflow Instructions
## Versioning
- Format: `MILESTONE.FEATURE.PATCH.BUILD` (e.g., `0.1.0.0032`)
- Bump VERSION file on every change before commit.
- Every commit must be tagged with the version: `git tag v$(cat VERSION)`
## Deployment Model (Unified Script)
The project uses a single unified script at `tools/nextwks.sh` for all operations.
The old `deploy.sh` and `install.sh` are deprecated.
**Key principles:**
- Source of truth is **git remote only**. No local `/opt/NextWks` repo.
- Building happens in `/tmp/nextwks-build/` via `git clone --depth 1` (fresh every time).
- Runtime goes to `/opt/nextworkspace/` (configs, compose, binary, lng).
- Secrets persist in `/opt/backup/.env` and survive `--destroy`.
- Stack runs on a shared `nextwks-net` podman bridge network.
### Script Flags
| Flag | What it does |
|------|-------------|
| `--install` | First-time setup on a bare VM: installs deps, prompts for config, builds binary, generates configs, deploys stack |
| `--update` | Smart update: clones fresh, rebuilds binary, copies to target, bounces containers |
| `--destroy` | Full greenfield redeploy: tears down containers, wipes `/opt/nextworkspace/`, rebuilds from scratch using saved secrets from `/opt/backup/.env` |
### Workflow for Making Changes
1. Edit code in the development clone.
2. Test locally (e.g., `go build && go run .`).
3. Bump `VERSION` (increment BUILD).
4. Update `CHANGELOG.md`.
5. Commit: `git add -A && git commit -m "description"`
6. Tag: `git tag v$(cat VERSION)`
7. Push: `git push origin main --tags`
8. Deploy: `sudo bash ~/nextwks.sh --update`
> **Note:** The deploy script is downloaded by users via `curl` from the repo. For production deployment, users run:
> ```bash
> curl -sL https://git.lohmar.co.uk/lexton-it/NextWks/raw/branch/main/tools/nextwks.sh | sudo bash -s -- --install
> ```
## Build Process
- Static Go binary: `CGO_ENABLED=0 go build -o nextworkspace .`
- Must run on Alpine in the container (no glibc dependency).
- Binary runs as PID 1 in the `launcher` container.
## Architecture
```
Caddy (:80/:443, host ports)
├── auth.{DOMAIN} → Authelia :9091 (internal)
├── app.{DOMAIN} → Launcher :9000 (internal) with forward auth
└── www.{DOMAIN} → static files
All three containers on nextwks-net (podman bridge).
Only Caddy exposes ports to host.
```
## Config Template System
Config files use `{PLACEHOLDER}` syntax. The script substitutes values at deploy time:
| File | Placeholders |
|------|-------------|
| `config/caddy/Caddyfile` | `{DOMAIN}`, `{TLS_EMAIL}` |
| `config/authelia/configuration.yml` | `{DOMAIN}`, `{JWT_SECRET}`, `{SESSION_SECRET}`, `{SMTP_HOST}`, `{SMTP_PORT}`, `{SMTP_USER}`, `{SMTP_PASS}` |
| `config/authelia/users_database.yml` | `{ADMIN_PASSWORD_HASH}`, `{TLS_EMAIL}` |
| `compose/stack.yaml` | `{AUTHELIA_SECRET}` (same as `SESSION_SECRET`) |
## Env Vault (`/opt/backup/.env`)
Persisted secrets across destroys:
```
DOMAIN=nextwks.eu
TLS_EMAIL=admin@nextwks.eu
ADMIN_USERNAME=master
ADMIN_PASSWORD=<generated>
SMTP_HOST=smtp.openxchange.eu
SMTP_PORT=587
SMTP_USER=post@nextwks.eu
SMTP_PASS=<prompted>
JWT_SECRET=<auto-generated>
SESSION_SECRET=<auto-generated>
ADMIN_PASSWORD_HASH=<bcrypt hash>
```
## Health Check
After deploy, the script polls `podman exec launcher wget -qO- http://127.0.0.1:9000/health`
up to 10 times (2s interval). Expected response: `OK`.
## Commit Message Style
- Imperative mood ("Add", "Fix", "Update", "Bump")
- Reference the component if relevant ("launcher: add health endpoint")
- Keep under 72 chars for the first line

View file

@ -1,5 +1,21 @@
# Changelog
## 0.1.0.0032 — 2026-07-11
### Added
- `tools/nextwks.sh` — unified install/update/destroy script
- `AGENT.md` — workflow instructions for agents
### Changed
- Replaced `deploy.sh` and `install.sh` with single `tools/nextwks.sh`
- Build moved from `/opt/NextWks` (persistent git repo) to `/tmp/nextwks-build` (ephemeral clone)
- README.md updated for unified script workflow
- `SESSION_SECRET` persisted in `/opt/backup/.env` for idempotent `--destroy`
### Removed
- `deploy.sh` (replaced by `tools/nextwks.sh --update / --destroy`)
- `install.sh` (replaced by `tools/nextwks.sh --install`)
## 0.1.0.0007 — 2026-07-08
### Added

View file

@ -24,44 +24,63 @@ app.nextwks.eu :443 auth.nextwks.eu :443
- **Authelia**: OIDC provider, 2FA, identity store
- **Binary**: Go launcher + path-based reverse proxy to upstream apps
## Quick Start
## Quick Start (Bare VM)
```bash
sudo ./install.sh
curl -sL https://git.lohmar.co.uk/lexton-it/NextWks/raw/branch/main/tools/nextwks.sh \
| sudo bash -s -- --install
```
Prompts for domain, TLS email, and admin credentials. Installs dependencies, generates configs, deploys Caddy + Authelia + launcher.
Prompts for domain, TLS email, and admin credentials. Installs deps (Go, Podman, git),
clones repo to `/tmp/nextwks-build/`, builds binary, generates configs, deploys stack.
## Directory Layout
```
/opt/nextworkspace/
/opt/nextworkspace/ # Runtime (freshly populated on every deploy)
├── config/
│ ├── caddy/Caddyfile
│ ├── authelia/configuration.yml
│ ├── authelia/users_database.yml
│ └── nextworkspace/{config,apps}.yaml
├── data/
│ ├── caddy/ (certs + runtime data)
│ ├── caddy/ (certs + runtime)
│ └── authelia/ (database)
├── compose/
│ ├── caddy.yaml
│ └── authelia.yaml
├── compose/stack.yaml
├── www/ (landing page)
└── nextworkspace (binary)
├── lng/ (translations)
└── nextworkspace (static Go binary)
/opt/backup/
├── .env (secrets vault)
└── certficates/ (LE cert backup)
/opt/backup/ # Secrets vault (survives --destroy)
├── .env
└── certificates/
/tmp/nextwks-build/ # Ephemeral build dir (git clone --depth 1)
```
## Deployment
## Operations
```bash
sudo ./deploy.sh # Smart update (swap binary, restart)
sudo ./deploy.sh --destroy # Full rebuild (certs backed up)
# First-time install (download + run — saves itself to ~/nextwks.sh)
curl -sL https://git.lohmar.co.uk/lexton-it/NextWks/raw/branch/main/tools/nextwks.sh \
| sudo bash -s -- --install
# Smart update (pull, build, copy, restart)
sudo bash ~/nextwks.sh --update
# Full redeploy (tear down, rebuild from scratch with saved secrets)
sudo bash ~/nextwks.sh --destroy
```
## Workflow (Development)
1. Edit code in your clone.
2. Bump `VERSION`, update `CHANGELOG.md`.
3. `git commit -m "message" && git tag v$(cat VERSION) && git push origin main --tags`
4. On the server: `sudo bash ~/nextwks.sh --update`
The script clones fresh from git every time — no stale repos, no permissions issues.
## Version
Current: 0.1.0.0007 — see [CHANGELOG.md](CHANGELOG.md)
Current: 0.1.0.0032 — see [CHANGELOG.md](CHANGELOG.md)

View file

@ -1 +1 @@
0.1.0.0031
0.1.0.0032

141
deploy.sh
View file

@ -1,141 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_DIR="/opt/NextWks"
TARGET_DIR="/opt/nextworkspace"
BACKUP_DIR="/opt/backup"
NETWORK_NAME="nextwks-net"
HEALTH_CHECK_RETRIES=10
HEALTH_CHECK_INTERVAL=2
# --- Load .env ---
if [ -f "$BACKUP_DIR/.env" ]; then
set -a; source "$BACKUP_DIR/.env"; set +a
elif [ -f "$TARGET_DIR/.env" ]; then
set -a; source "$TARGET_DIR/.env"; set +a
fi
DOMAIN="${DOMAIN:-nextwks.eu}"
# --- Mode ---
GREENFIELD=false
if [ "${1:-}" = "--destroy" ]; then GREENFIELD=true; echo "[MODE] Greenfield deploy (--destroy)"
elif [ ! -d "$TARGET_DIR" ]; then GREENFIELD=true; echo "[MODE] Greenfield deploy (target missing)"
else echo "[MODE] Smart update (target exists)"; fi
# --- Pull + build ---
cd "$REPO_DIR"
echo "[1/5] Pulling latest code..."
git config --global --add safe.directory /opt/NextWks 2>/dev/null || true
git pull 2>&1 || echo "[WARN] Git pull failed — using existing code"
echo "[2/5] Building binary (static)..."
export PATH=$PATH:/usr/local/go/bin
CGO_ENABLED=0 go build -o nextworkspace .
# --- Greenfield ---
if [ "$GREENFIELD" = true ]; then
echo "[3/5] Full teardown..."
systemctl stop nextworkspace 2>/dev/null || true
systemctl disable nextworkspace 2>/dev/null || true
rm -f /etc/systemd/system/nextworkspace.service
systemctl daemon-reload
# Kill any lingering Caddy processes on port 80/443
pkill -f "caddy" 2>/dev/null || true
# Force clean ALL old pods, containers, and networks
for p in $(podman pod ls -q 2>/dev/null); do podman pod rm -f "$p" 2>/dev/null || true; done
podman rm -f caddy authelia launcher 2>/dev/null || true
podman network rm "$NETWORK_NAME" 2>&1 || true
if [ -d "$TARGET_DIR" ]; then
chattr -R -i "$TARGET_DIR" 2>/dev/null || true
rm -rf "$TARGET_DIR"
fi
echo "[4/5] Building directories..."
mkdir -p "$TARGET_DIR/config/caddy"
mkdir -p "$TARGET_DIR/config/authelia"
mkdir -p "$TARGET_DIR/data/caddy"
mkdir -p "$TARGET_DIR/data/authelia"
mkdir -p "$TARGET_DIR/compose"
mkdir -p "$TARGET_DIR/www"
mkdir -p "$TARGET_DIR/config/nextworkspace"
mkdir -p "$TARGET_DIR/logs"
chown -R master:master "$TARGET_DIR/data/caddy" 2>/dev/null || true
chown -R master:master "$TARGET_DIR/logs" 2>/dev/null || true
if [ -f "$BACKUP_DIR/.env" ]; then
cp "$BACKUP_DIR/.env" "$TARGET_DIR/.env"
chmod 644 "$TARGET_DIR/.env"
fi
# Generate Caddyfile
sed -e "s|{DOMAIN}|$DOMAIN|g" -e "s|{TLS_EMAIL}|${TLS_EMAIL:-admin@$DOMAIN}|g" \
"$SCRIPT_DIR/config/caddy/Caddyfile" > "$TARGET_DIR/config/caddy/Caddyfile"
# Generate Authelia config
JWT_SECRET="${JWT_SECRET:-$(openssl rand -hex 32)}"
SESSION_SECRET="${SESSION_SECRET:-$(openssl rand -hex 32)}"
sed -e "s|{DOMAIN}|$DOMAIN|g" -e "s|{JWT_SECRET}|$JWT_SECRET|g" \
-e "s|{SESSION_SECRET}|$SESSION_SECRET|g" \
-e "s|{SMTP_HOST}|${SMTP_HOST:-smtp.openxchange.eu}|g" \
-e "s|{SMTP_PORT}|${SMTP_PORT:-587}|g" \
-e "s|{SMTP_USER}|${SMTP_USER:-post@nextwks.eu}|g" -e "s|{SMTP_PASS}|$SMTP_PASS|g" \
"$SCRIPT_DIR/config/authelia/configuration.yml" > "$TARGET_DIR/config/authelia/configuration.yml"
ADMIN_PASSWORD_HASH="${ADMIN_PASSWORD_HASH:-}"
if [ -z "$ADMIN_PASSWORD_HASH" ] && [ -n "${ADMIN_PASSWORD:-}" ] && [ -f "$SCRIPT_DIR/tools/hash-password/main.go" ]; then
ADMIN_PASSWORD_HASH=$(cd "$SCRIPT_DIR" && go run ./tools/hash-password/ "$ADMIN_PASSWORD" 2>/dev/null || echo "")
fi
sed -e "s|{ADMIN_PASSWORD_HASH}|$ADMIN_PASSWORD_HASH|g" -e "s|{TLS_EMAIL}|${TLS_EMAIL:-admin@$DOMAIN}|g" \
"$SCRIPT_DIR/config/authelia/users_database.yml" > "$TARGET_DIR/config/authelia/users_database.yml"
cp nextworkspace "$TARGET_DIR/nextworkspace"
if [ -f "$REPO_DIR/VERSION" ]; then cp "$REPO_DIR/VERSION" "$TARGET_DIR/VERSION"; fi
if [ -d "$SCRIPT_DIR/config/www" ]; then cp -r "$SCRIPT_DIR/config/www"/* "$TARGET_DIR/www/"; fi
cp -r "$SCRIPT_DIR/config/nextworkspace"/* "$TARGET_DIR/config/nextworkspace/" 2>/dev/null || true
# Copy language files
if [ -d "$SCRIPT_DIR/lng" ]; then
cp -r "$SCRIPT_DIR/lng" "$TARGET_DIR/"
fi
# Create podman network and deploy Caddy + Authelia
echo "[5/5] Deploying containers on $NETWORK_NAME..."
podman network create "$NETWORK_NAME" 2>&1 || true
sleep 3
# Extract AUTHELIA_SECRET from config
AUTHELIA_SECRET=$(grep -oP 'session_secret: \K.*' "$TARGET_DIR/config/authelia/configuration.yml" 2>/dev/null || echo "")
if [ -n "$AUTHELIA_SECRET" ] && ! grep -q "AUTHELIA_SECRET" "$BACKUP_DIR/.env" 2>/dev/null; then
echo "AUTHELIA_SECRET=$AUTHELIA_SECRET" >> "$BACKUP_DIR/.env"
fi
# Generate compose file with secret, then deploy
sed -e "s|{AUTHELIA_SECRET}|$AUTHELIA_SECRET|g" \
"$SCRIPT_DIR/compose/stack.yaml" > "$TARGET_DIR/compose/stack.yaml"
podman-compose -f "$TARGET_DIR/compose/stack.yaml" down 2>/dev/null || true
podman-compose -f "$TARGET_DIR/compose/stack.yaml" up -d 2>&1 || echo "[WARN] Stack deploy had issues"
# --- Smart update ---
else
echo "[3/5] Redeploying containers..."
podman stop caddy authelia launcher 2>/dev/null || true
podman rm caddy authelia launcher 2>/dev/null || true
podman-compose -f "$TARGET_DIR/compose/stack.yaml" up -d 2>&1 || echo "[WARN] Stack deploy had issues"
fi
# --- Health check ---
echo "[*] Running health check..."
for i in $(seq 1 $HEALTH_CHECK_RETRIES); do
HEALTH=$(podman exec launcher wget -qO- http://127.0.0.1:9000/health 2>/dev/null || echo "")
if [ "$HEALTH" = "OK" ]; then
echo "[OK] NextWorkspace launcher is healthy"
exit 0
fi
echo " Attempt $i/$HEALTH_CHECK_RETRIES — not ready yet..."
sleep $HEALTH_CHECK_INTERVAL
done
echo "[FAIL] Health check failed — launcher did not respond"
exit 1

View file

@ -1,143 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# NextWorkspace Installer — bootstraps a bare Linux VM
# Idempotent: safe to run multiple times.
TARGET_DIR="/opt/nextworkspace"
BACKUP_DIR="/opt/backup"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# --- Create backup vault and runtime directories ---
echo "=== NextWorkspace Setup ==="
mkdir -p "$BACKUP_DIR/certificates"
mkdir -p "$TARGET_DIR/config/nextworkspace"
mkdir -p "$TARGET_DIR/config/caddy"
mkdir -p "$TARGET_DIR/config/authelia"
mkdir -p "$TARGET_DIR/data/caddy"
mkdir -p "$TARGET_DIR/data/authelia"
mkdir -p "$TARGET_DIR/compose"
mkdir -p "$TARGET_DIR/www"
mkdir -p "$TARGET_DIR/logs/caddy"
# Ensure data dirs are owned by the runtime user
chown -R master:master "$TARGET_DIR/data/caddy" 2>/dev/null || true
chown -R master:master "$TARGET_DIR/logs/caddy" 2>/dev/null || true
# --- Interactive prompts (no defaults — user enters everything) ---
read -p "Domain: " DOMAIN
while [ -z "$DOMAIN" ]; do
read -p "Domain (required): " DOMAIN
done
read -p "TLS email (Let's Encrypt): " TLS_EMAIL
while [ -z "$TLS_EMAIL" ]; do
read -p "TLS email (required): " TLS_EMAIL
done
# Basic email validation (must contain @)
while echo "$TLS_EMAIL" | grep -qv '@'; do
read -p "Invalid email — must contain @: " TLS_EMAIL
done
read -p "Admin username: " ADMIN_USERNAME
while [ -z "$ADMIN_USERNAME" ]; do
read -p "Admin username (required): " ADMIN_USERNAME
done
# Generate 12-char alphanumeric password (easy to type)
ADMIN_PASSWORD=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 12 2>/dev/null || date +%s | head -c 12)
# --- SMTP prompts ---
read -p "SMTP host [smtp.openxchange.eu]: " SMTP_HOST
SMTP_HOST="${SMTP_HOST:-smtp.openxchange.eu}"
read -p "SMTP port [587]: " SMTP_PORT
SMTP_PORT="${SMTP_PORT:-587}"
read -p "SMTP user [post@nextwks.eu]: " SMTP_USER
SMTP_USER="${SMTP_USER:-post@nextwks.eu}"
read -sp "SMTP password: " SMTP_PASS
echo ""
if [ -z "$SMTP_PASS" ]; then
echo "[ERROR] SMTP password is required."
exit 1
fi
read -p "IMAP host [imap.openxchange.eu]: " IMAP_HOST
IMAP_HOST="${IMAP_HOST:-imap.openxchange.eu}"
read -p "IMAP port [993]: " IMAP_PORT
IMAP_PORT="${IMAP_PORT:-993}"
echo ""
echo "========================================"
echo " Domain: $DOMAIN"
echo " TLS email: $TLS_EMAIL"
echo " Admin username: $ADMIN_USERNAME"
echo " Admin password: $ADMIN_PASSWORD"
echo " Save this password — it won't be shown again!"
echo "========================================"
echo ""
# Write .env file in backup vault (deploy.sh copies it to production)
ENV_FILE="$BACKUP_DIR/.env"
cat > "$ENV_FILE" <<EOF
# NextWorkspace Configuration
# This file is auto-generated by install.sh — do not edit manually
DOMAIN=$DOMAIN
TLS_EMAIL=$TLS_EMAIL
ADMIN_USERNAME=$ADMIN_USERNAME
ADMIN_PASSWORD=$ADMIN_PASSWORD
SMTP_HOST=$SMTP_HOST
SMTP_PORT=$SMTP_PORT
SMTP_USER=$SMTP_USER
SMTP_PASS=$SMTP_PASS
IMAP_HOST=$IMAP_HOST
IMAP_PORT=$IMAP_PORT
EOF
chmod 600 "$ENV_FILE"
# ---- Go ----
if command -v go &>/dev/null; then
echo "[SKIP] Go already installed: $(go version)"
else
echo "[INSTALL] Installing Go..."
GO_URL="https://go.dev/dl/$(curl -sL https://go.dev/VERSION?m=text | head -1).linux-amd64.tar.gz"
curl -sL "$GO_URL" -o /tmp/go.tar.gz
rm -rf /usr/local/go
tar -C /usr/local -xzf /tmp/go.tar.gz
rm /tmp/go.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' > /etc/profile.d/go.sh
chmod +x /etc/profile.d/go.sh
export PATH=$PATH:/usr/local/go/bin
echo "[OK] Go installed: $(go version)"
fi
# ---- System deps ----
echo "[INSTALL] git, build-essential, podman, podman-compose..."
apt-get update -qq
apt-get install -y -qq git build-essential curl podman podman-compose
# ---- Deploy: use current scripts + tools ---
REPO_DIR="/opt/NextWks"
mkdir -p "$REPO_DIR"
# Clone or update repo, then overlay our current code
echo "[SETUP] Preparing /opt/NextWks..."
if [ -d "$REPO_DIR/.git" ]; then
cd "$REPO_DIR" && git pull
elif command -v git &>/dev/null; then
# Try to clone the remote first, so git history is intact
git clone "https://git.lohmar.co.uk/lexton-it/NextWks.git" "$REPO_DIR.tmp" 2>/dev/null && \
mv "$REPO_DIR.tmp" "$REPO_DIR" || true
fi
# Copy current code on top (ensures latest changes)
mkdir -p "$REPO_DIR"
cp -r "$SCRIPT_DIR"/* "$REPO_DIR/"
cp "$SCRIPT_DIR"/.gitignore "$REPO_DIR/" 2>/dev/null || true
chmod +x "$REPO_DIR/deploy.sh" "$REPO_DIR/install.sh" 2>/dev/null || true
echo "[DONE] Bootstrapping complete. Running first deploy..."
"$REPO_DIR/deploy.sh" --destroy

244
tools/nextwks.sh Executable file
View file

@ -0,0 +1,244 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_URL="https://git.lohmar.co.uk/lexton-it/NextWks.git"
BUILD_DIR="/tmp/nextwks-build"
TARGET_DIR="/opt/nextworkspace"
BACKUP_DIR="/opt/backup"
NETWORK_NAME="nextwks-net"
HEALTH_CHECK_RETRIES=10
HEALTH_CHECK_INTERVAL=2
usage() {
echo "Usage: $0 [--install|--update|--destroy]"
echo " --install First-time setup on a bare VM (prompts for config)"
echo " --update Smart update: pull, build, copy, bounce containers"
echo " --destroy Full greenfield redeploy (uses saved secrets)"
exit 1
}
[ $# -eq 0 ] && usage
MODE="${1#--}"
case "$MODE" in install|update|destroy) ;; *) usage ;; esac
# --- Load existing env (if any) ---
if [ -f "$BACKUP_DIR/.env" ]; then
set -a; source "$BACKUP_DIR/.env"; set +a
elif [ -f "$TARGET_DIR/.env" ]; then
set -a; source "$TARGET_DIR/.env"; set +a
fi
DOMAIN="${DOMAIN:-nextwks.eu}"
echo "=== NextWorkspace ${MODE} ==="
# ============================================================
# 1. INSTALL MODE — first-time setup
# ============================================================
if [ "$MODE" = "install" ]; then
echo "[*] Installing system dependencies..."
apt-get update -qq && apt-get install -y -qq git build-essential curl podman podman-compose
if ! command -v go &>/dev/null; then
echo "[*] Installing Go..."
GO_URL="https://go.dev/dl/$(curl -sL https://go.dev/VERSION?m=text | head -1).linux-amd64.tar.gz"
curl -sL "$GO_URL" -o /tmp/go.tar.gz
rm -rf /usr/local/go
tar -C /usr/local -xzf /tmp/go.tar.gz
rm /tmp/go.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' > /etc/profile.d/go.sh
chmod +x /etc/profile.d/go.sh
export PATH=$PATH:/usr/local/go/bin
fi
echo ""
echo "--- NextWorkspace Configuration ---"
read -p "Domain [nextwks.eu]: " input; DOMAIN="${input:-$DOMAIN}"
read -p "TLS email (Let's Encrypt): " TLS_EMAIL
while [ -z "$TLS_EMAIL" ]; do read -p "TLS email (required): " TLS_EMAIL; done
while echo "$TLS_EMAIL" | grep -qv '@'; do read -p "Invalid email: " TLS_EMAIL; done
read -p "Admin username: " ADMIN_USERNAME
while [ -z "$ADMIN_USERNAME" ]; do read -p "Admin username (required): " ADMIN_USERNAME; done
ADMIN_PASSWORD=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 12)
echo ""
echo "========================================"
echo " Domain: $DOMAIN"
echo " TLS email: $TLS_EMAIL"
echo " Admin username: $ADMIN_USERNAME"
echo " Admin password: $ADMIN_PASSWORD"
echo " Save this password — it won't be shown again!"
echo "========================================"
echo ""
read -p "SMTP host [smtp.openxchange.eu]: " SMTP_HOST; SMTP_HOST="${SMTP_HOST:-smtp.openxchange.eu}"
read -p "SMTP port [587]: " SMTP_PORT; SMTP_PORT="${SMTP_PORT:-587}"
read -p "SMTP user [post@nextwks.eu]: " SMTP_USER; SMTP_USER="${SMTP_USER:-post@nextwks.eu}"
read -sp "SMTP password: " SMTP_PASS; echo ""
[ -z "$SMTP_PASS" ] && echo "ERROR: SMTP password required" && exit 1
# Persist config to backup vault
mkdir -p "$BACKUP_DIR"
cat > "$BACKUP_DIR/.env" <<EOF
# NextWorkspace Configuration — auto-generated by nextwks.sh --install
DOMAIN=$DOMAIN
TLS_EMAIL=$TLS_EMAIL
ADMIN_USERNAME=$ADMIN_USERNAME
ADMIN_PASSWORD=$ADMIN_PASSWORD
SMTP_HOST=$SMTP_HOST
SMTP_PORT=$SMTP_PORT
SMTP_USER=$SMTP_USER
SMTP_PASS=$SMTP_PASS
EOF
chmod 600 "$BACKUP_DIR/.env"
fi
# ============================================================
# 2. CLONE fresh (every mode — ensures latest code)
# ============================================================
echo "[*] Cloning repository..."
rm -rf "$BUILD_DIR"
git clone --depth 1 "$REPO_URL" "$BUILD_DIR"
cd "$BUILD_DIR"
# Save script to user's home for easy future access (--install only)
if [ "$MODE" = "install" ]; then
USER_HOME=$(eval echo "~${SUDO_USER:-}" 2>/dev/null || echo "$HOME")
cp "$BUILD_DIR/tools/nextwks.sh" "$USER_HOME/nextwks.sh"
chmod +x "$USER_HOME/nextwks.sh"
echo "[*] Saved to $USER_HOME/nextwks.sh — use it for future updates"
fi
# ============================================================
# 3. BUILD static binary
# ============================================================
echo "[*] Building static binary..."
export PATH=$PATH:/usr/local/go/bin
CGO_ENABLED=0 go build -o nextworkspace .
# ============================================================
# 4. CREATE target directory structure
# ============================================================
mkdir -p "$TARGET_DIR/config/caddy" "$TARGET_DIR/config/authelia" \
"$TARGET_DIR/data/caddy" "$TARGET_DIR/data/authelia" \
"$TARGET_DIR/compose" "$TARGET_DIR/www" \
"$TARGET_DIR/config/nextworkspace" "$TARGET_DIR/logs"
# ============================================================
# 5. TEARDOWN (destroy mode only)
# ============================================================
if [ "$MODE" = "destroy" ]; then
echo "[*] Full teardown..."
podman stop caddy authelia launcher 2>/dev/null || true
podman rm caddy authelia launcher 2>/dev/null || true
rm -rf "$TARGET_DIR"
mkdir -p "$TARGET_DIR/config/caddy" "$TARGET_DIR/config/authelia" \
"$TARGET_DIR/data/caddy" "$TARGET_DIR/data/authelia" \
"$TARGET_DIR/compose" "$TARGET_DIR/www" \
"$TARGET_DIR/config/nextworkspace" "$TARGET_DIR/logs"
fi
# ============================================================
# 6. COPY artifacts to target
# ============================================================
echo "[*] Copying artifacts..."
cp nextworkspace "$TARGET_DIR/nextworkspace"
cp "$BUILD_DIR/VERSION" "$TARGET_DIR/VERSION"
if [ -d "$BUILD_DIR/config/www" ]; then
cp -r "$BUILD_DIR/config/www"/* "$TARGET_DIR/www/"
fi
if [ -d "$BUILD_DIR/lng" ]; then
rm -rf "$TARGET_DIR/lng"
cp -r "$BUILD_DIR/lng" "$TARGET_DIR/lng"
fi
if [ -d "$BUILD_DIR/config/nextworkspace" ]; then
cp -r "$BUILD_DIR/config/nextworkspace"/* "$TARGET_DIR/config/nextworkspace/"
fi
# Restore .env from backup
if [ -f "$BACKUP_DIR/.env" ]; then
cp "$BACKUP_DIR/.env" "$TARGET_DIR/.env"
chmod 644 "$TARGET_DIR/.env"
fi
# ============================================================
# 7. GENERATE config files with placeholder substitution
# (install + destroy modes; update skips to keep secrets stable)
# ============================================================
if [ "$MODE" != "update" ]; then
echo "[*] Generating config files..."
# Caddyfile
sed -e "s|{DOMAIN}|$DOMAIN|g" -e "s|{TLS_EMAIL}|${TLS_EMAIL:-admin@$DOMAIN}|g" \
"$BUILD_DIR/config/caddy/Caddyfile" > "$TARGET_DIR/config/caddy/Caddyfile"
# Authelia config — preserve existing secrets if present
JWT_SECRET="${JWT_SECRET:-$(openssl rand -hex 32)}"
SESSION_SECRET="${SESSION_SECRET:-$(openssl rand -hex 32)}"
sed -e "s|{DOMAIN}|$DOMAIN|g" -e "s|{JWT_SECRET}|$JWT_SECRET|g" \
-e "s|{SESSION_SECRET}|$SESSION_SECRET|g" \
-e "s|{SMTP_HOST}|${SMTP_HOST:-smtp.openxchange.eu}|g" \
-e "s|{SMTP_PORT}|${SMTP_PORT:-587}|g" \
-e "s|{SMTP_USER}|${SMTP_USER:-post@nextwks.eu}|g" \
-e "s|{SMTP_PASS}|$SMTP_PASS|g" \
"$BUILD_DIR/config/authelia/configuration.yml" > "$TARGET_DIR/config/authelia/configuration.yml"
# Users database
ADMIN_PASSWORD_HASH="${ADMIN_PASSWORD_HASH:-}"
if [ -z "$ADMIN_PASSWORD_HASH" ] && [ -n "${ADMIN_PASSWORD:-}" ]; then
ADMIN_PASSWORD_HASH=$(cd "$BUILD_DIR" && go run ./tools/hash-password/ "$ADMIN_PASSWORD" 2>/dev/null || echo "")
fi
sed -e "s|{ADMIN_PASSWORD_HASH}|$ADMIN_PASSWORD_HASH|g" \
-e "s|{TLS_EMAIL}|${TLS_EMAIL:-admin@$DOMAIN}|g" \
"$BUILD_DIR/config/authelia/users_database.yml" > "$TARGET_DIR/config/authelia/users_database.yml"
# Persist generated secrets so --destroy is idempotent
if [ -f "$BACKUP_DIR/.env" ]; then
# Update existing .env with any new secrets
sed -i "/^JWT_SECRET=/d; /^SESSION_SECRET=/d; /^ADMIN_PASSWORD_HASH=/d" "$BACKUP_DIR/.env" 2>/dev/null || true
fi
echo "JWT_SECRET=$JWT_SECRET" >> "$BACKUP_DIR/.env"
echo "SESSION_SECRET=$SESSION_SECRET" >> "$BACKUP_DIR/.env"
[ -n "$ADMIN_PASSWORD_HASH" ] && echo "ADMIN_PASSWORD_HASH=$ADMIN_PASSWORD_HASH" >> "$BACKUP_DIR/.env"
chmod 600 "$BACKUP_DIR/.env"
fi
# ============================================================
# 8. DEPLOY stack
# ============================================================
echo "[*] Deploying containers on $NETWORK_NAME..."
# Ensure network exists
podman network create "$NETWORK_NAME" 2>/dev/null || true
# AUTHELIA_SECRET is SESSION_SECRET (Authelia session.secret)
AUTHELIA_SECRET="${SESSION_SECRET:-}"
if [ -z "$AUTHELIA_SECRET" ]; then
# Update mode — extract from existing config
AUTHELIA_SECRET=$(sed -n '/^session:/,/^[a-z]/p' "$TARGET_DIR/config/authelia/configuration.yml" \
| grep 'secret:' | awk '{print $2}' 2>/dev/null || echo "")
fi
# Generate compose file with substituted secret
sed -e "s|{AUTHELIA_SECRET}|$AUTHELIA_SECRET|g" \
"$BUILD_DIR/compose/stack.yaml" > "$TARGET_DIR/compose/stack.yaml"
podman-compose -f "$TARGET_DIR/compose/stack.yaml" down 2>/dev/null || true
sleep 1
podman-compose -f "$TARGET_DIR/compose/stack.yaml" up -d 2>&1 || echo "[WARN] Stack deploy had issues"
# ============================================================
# 9. HEALTH CHECK
# ============================================================
echo "[*] Running health check..."
for i in $(seq 1 $HEALTH_CHECK_RETRIES); do
HEALTH=$(podman exec launcher wget -qO- http://127.0.0.1:9000/health 2>/dev/null || echo "")
if [ "$HEALTH" = "OK" ]; then
echo "[OK] NextWorkspace launcher is healthy"
echo "[OK] https://$DOMAIN/"
exit 0
fi
sleep $HEALTH_CHECK_INTERVAL
done
echo "[FAIL] Health check failed — launcher did not respond"
exit 1