#!/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 <