diff --git a/bootstrap-appfactory-ubuntu.sh b/bootstrap-appfactory-ubuntu.sh new file mode 100644 index 0000000..9921d96 --- /dev/null +++ b/bootstrap-appfactory-ubuntu.sh @@ -0,0 +1,529 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# ============================================================================== +# AppFactory Bootstrap for Ubuntu Server +# ============================================================================== +# What this script does: +# - Installs base packages +# - Installs Docker Engine + Docker Compose plugin +# - Creates /opt/appfactory structure +# - Creates Docker Compose stack: +# - Caddy reverse proxy +# - Gitea Git server +# - Docker Registry +# - Creates initial Gitea app.ini so web setup is skipped +# - Starts the stack +# - Optionally creates Gitea admin user +# +# Run: +# chmod +x bootstrap-appfactory-ubuntu.sh +# sudo ./bootstrap-appfactory-ubuntu.sh +# +# Recommended: +# edit variables below before running on a real server +# ============================================================================== + +# ------------------------------------------------------------------------------ +# Editable variables +# ------------------------------------------------------------------------------ + +APP_USER="${APP_USER:-${SUDO_USER:-jiri}}" +BASE_DIR="${BASE_DIR:-/opt/appfactory}" + +# Server IP/domain used in URLs. +# If empty, script tries to detect first IPv4 address. +SERVER_HOST="${SERVER_HOST:-}" + +GITEA_SITE_NAME="${GITEA_SITE_NAME:-AppFactory Git}" +GITEA_ADMIN_USER="${GITEA_ADMIN_USER:-jiri}" +GITEA_ADMIN_EMAIL="${GITEA_ADMIN_EMAIL:-jiri.uhlir59@gmail.com}" + +# CHANGE THIS before production use. +GITEA_ADMIN_PASSWORD="${GITEA_ADMIN_PASSWORD:-ChangeMe123!}" + +# Ports +HTTP_PORT="${HTTP_PORT:-80}" +HTTPS_PORT="${HTTPS_PORT:-443}" +GITEA_HTTP_PORT="${GITEA_HTTP_PORT:-3000}" +GITEA_SSH_PORT="${GITEA_SSH_PORT:-2222}" +REGISTRY_PORT="${REGISTRY_PORT:-5000}" + +# Docker images +CADDY_IMAGE="${CADDY_IMAGE:-caddy:2}" +GITEA_IMAGE="${GITEA_IMAGE:-gitea/gitea:latest}" +REGISTRY_IMAGE="${REGISTRY_IMAGE:-registry:2}" + +# ------------------------------------------------------------------------------ +# Helpers +# ------------------------------------------------------------------------------ + +log() { + echo "" + echo "== $* ==" +} + +fail() { + echo "ERROR: $*" >&2 + exit 1 +} + +require_root() { + if [ "${EUID}" -ne 0 ]; then + fail "Run as root: sudo ./bootstrap-appfactory-ubuntu.sh" + fi +} + +detect_server_host() { + if [ -z "${SERVER_HOST}" ]; then + SERVER_HOST="$(hostname -I | awk '{print $1}')" + fi + + if [ -z "${SERVER_HOST}" ]; then + fail "Could not detect SERVER_HOST. Run with SERVER_HOST=your-ip-or-domain" + fi +} + +ubuntu_codename_for_docker() { + local codename + codename="$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")" + + # Docker may not immediately support future Ubuntu releases. + # Fallback to noble for unsupported future/testing versions. + case "$codename" in + jammy|noble) + echo "$codename" + ;; + *) + echo "noble" + ;; + esac +} + +wait_for_gitea() { + log "Waiting for Gitea" + for i in $(seq 1 60); do + if curl -fsS "http://127.0.0.1:${GITEA_HTTP_PORT}/api/healthz" >/dev/null 2>&1; then + echo "Gitea is responding." + return 0 + fi + sleep 2 + done + + echo "Gitea did not respond on /api/healthz yet. Continuing anyway." +} + +# ------------------------------------------------------------------------------ +# Main +# ------------------------------------------------------------------------------ + +require_root +detect_server_host + +log "AppFactory bootstrap" +echo "APP_USER: ${APP_USER}" +echo "BASE_DIR: ${BASE_DIR}" +echo "SERVER_HOST: ${SERVER_HOST}" +echo "Gitea URL: http://${SERVER_HOST}:${GITEA_HTTP_PORT}/" +echo "Registry URL: http://${SERVER_HOST}:${REGISTRY_PORT}/v2/" +echo "Gateway health URL: http://${SERVER_HOST}/health" + +log "Install base packages" +apt-get update +apt-get install -y \ + ca-certificates \ + curl \ + gnupg \ + git \ + nano \ + unzip \ + tree \ + ufw \ + jq + +log "Install Docker Engine" +install -m 0755 -d /etc/apt/keyrings + +if [ ! -f /etc/apt/keyrings/docker.asc ]; then + curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc + chmod a+r /etc/apt/keyrings/docker.asc +fi + +DOCKER_CODENAME="$(ubuntu_codename_for_docker)" + +cat > /etc/apt/sources.list.d/docker.list </dev/null 2>&1; then + usermod -aG docker "${APP_USER}" || true +fi + +log "Create AppFactory folders" +mkdir -p \ + "${BASE_DIR}/apps" \ + "${BASE_DIR}/services" \ + "${BASE_DIR}/templates" \ + "${BASE_DIR}/deploy" \ + "${BASE_DIR}/deploy/env" \ + "${BASE_DIR}/gateway" \ + "${BASE_DIR}/docs" \ + "${BASE_DIR}/tools" \ + "${BASE_DIR}/data/gitea/gitea/conf" \ + "${BASE_DIR}/data/gitea/git" \ + "${BASE_DIR}/data/registry" \ + "${BASE_DIR}/data/caddy" \ + "${BASE_DIR}/data/caddy-config" + +log "Create Gitea config" +cat > "${BASE_DIR}/data/gitea/gitea/conf/app.ini" < "${BASE_DIR}/gateway/Caddyfile" <<'EOF_CADDY' +:80 { + respond /health "AppFactory OK" 200 + + handle_path /registry/* { + reverse_proxy registry:5000 + } + + handle { + respond "AppFactory gateway is running" 200 + } +} +EOF_CADDY + +log "Create docker-compose.yml" +cat > "${BASE_DIR}/deploy/docker-compose.yml" < "${BASE_DIR}/apps/catalog.yml" <<'EOF_CATALOG' +apps: [] +EOF_CATALOG + +log "Create documentation" +cat > "${BASE_DIR}/docs/README.md" < "${BASE_DIR}/templates/python-fastapi-template/app/main.py" <<'EOF_FASTAPI' +from fastapi import FastAPI + +APP_NAME = "Python FastAPI Template" +APP_VERSION = "1.0.0" + +app = FastAPI( + title=APP_NAME, + version=APP_VERSION +) + +@app.get("/health") +def health(): + return {"status": "ok"} + +@app.get("/version") +def version(): + return { + "app": APP_NAME, + "version": APP_VERSION, + "language": "python" + } +EOF_FASTAPI + +cat > "${BASE_DIR}/templates/python-fastapi-template/requirements.txt" <<'EOF_REQ' +fastapi +uvicorn[standard] +EOF_REQ + +cat > "${BASE_DIR}/templates/python-fastapi-template/Dockerfile" <<'EOF_DOCKERFILE' +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app ./app + +EXPOSE 8000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +EOF_DOCKERFILE + +cat > "${BASE_DIR}/templates/python-fastapi-template/README.md" <<'EOF_TEMPLATE_README' +# Python FastAPI Template + +Default template for AppFactory Python Apps. + +## Local run + +```bash +docker build -t python-fastapi-template . +docker run --rm -p 8000:8000 python-fastapi-template +``` + +Open: + +```text +http://SERVER_IP:8000/docs +``` +EOF_TEMPLATE_README + +log "Create new-app tool" +cat > "${BASE_DIR}/tools/new-app.sh" <<'EOF_NEW_APP' +#!/usr/bin/env bash +set -euo pipefail + +APP_ID="${1:-}" + +if [ -z "$APP_ID" ]; then + echo "Usage:" + echo " ./new-app.sh " + echo "" + echo "Example:" + echo " ./new-app.sh gmail-app" + exit 1 +fi + +BASE_DIR="${BASE_DIR:-/opt/appfactory}" +SOURCE_TEMPLATE="${BASE_DIR}/templates/python-fastapi-template" +TARGET_DIR="${BASE_DIR}/services/${APP_ID}" + +if [ -d "$TARGET_DIR" ]; then + echo "App already exists: $TARGET_DIR" + exit 1 +fi + +cp -R "$SOURCE_TEMPLATE" "$TARGET_DIR" + +sed -i "s/Python FastAPI Template/${APP_ID}/g" "$TARGET_DIR/app/main.py" + +cat > "$TARGET_DIR/app.yml" </dev/null 2>&1; then + chown -R "${APP_USER}:${APP_USER}" "${BASE_DIR}" +fi + +log "Firewall" +ufw allow OpenSSH || true +ufw allow "${HTTP_PORT}/tcp" || true +ufw allow "${HTTPS_PORT}/tcp" || true +ufw allow "${GITEA_HTTP_PORT}/tcp" || true +ufw allow "${GITEA_SSH_PORT}/tcp" || true +ufw allow "${REGISTRY_PORT}/tcp" || true +ufw --force enable || true + +log "Start AppFactory stack" +cd "${BASE_DIR}/deploy" +docker compose up -d + +wait_for_gitea + +log "Create Gitea admin user if missing" +if docker exec appfactory-gitea gitea admin user list --config /data/gitea/conf/app.ini 2>/dev/null | grep -q "${GITEA_ADMIN_USER}"; then + echo "Gitea admin user already exists: ${GITEA_ADMIN_USER}" +else + docker exec appfactory-gitea gitea admin user create \ + --config /data/gitea/conf/app.ini \ + --admin \ + --username "${GITEA_ADMIN_USER}" \ + --password "${GITEA_ADMIN_PASSWORD}" \ + --email "${GITEA_ADMIN_EMAIL}" \ + --must-change-password=false || true +fi + +log "Final status" +docker ps + +cat <