530 lines
12 KiB
Bash
530 lines
12 KiB
Bash
#!/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 <<EOF_DOCKER_REPO
|
|
deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu ${DOCKER_CODENAME} stable
|
|
EOF_DOCKER_REPO
|
|
|
|
apt-get update
|
|
apt-get install -y \
|
|
docker-ce \
|
|
docker-ce-cli \
|
|
containerd.io \
|
|
docker-buildx-plugin \
|
|
docker-compose-plugin
|
|
|
|
systemctl enable docker
|
|
systemctl start docker
|
|
|
|
if id "${APP_USER}" >/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" <<EOF_GITEA
|
|
APP_NAME = ${GITEA_SITE_NAME}
|
|
RUN_USER = git
|
|
WORK_PATH = /data/gitea
|
|
|
|
[database]
|
|
DB_TYPE = sqlite3
|
|
PATH = /data/gitea/gitea.db
|
|
|
|
[repository]
|
|
ROOT = /data/git/repositories
|
|
|
|
[server]
|
|
DOMAIN = ${SERVER_HOST}
|
|
HTTP_PORT = 3000
|
|
ROOT_URL = http://${SERVER_HOST}:${GITEA_HTTP_PORT}/
|
|
DISABLE_SSH = false
|
|
SSH_DOMAIN = ${SERVER_HOST}
|
|
SSH_PORT = ${GITEA_SSH_PORT}
|
|
START_SSH_SERVER = false
|
|
LFS_START_SERVER = true
|
|
LFS_JWT_SECRET = appfactory-lfs-secret-change-me
|
|
OFFLINE_MODE = true
|
|
|
|
[security]
|
|
INSTALL_LOCK = true
|
|
SECRET_KEY = appfactory-secret-key-change-me
|
|
INTERNAL_TOKEN = appfactory-internal-token-change-me
|
|
PASSWORD_HASH_ALGO = pbkdf2
|
|
|
|
[service]
|
|
DISABLE_REGISTRATION = false
|
|
REQUIRE_SIGNIN_VIEW = false
|
|
REGISTER_EMAIL_CONFIRM = false
|
|
ENABLE_NOTIFY_MAIL = false
|
|
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
|
|
ENABLE_CAPTCHA = false
|
|
|
|
[openid]
|
|
ENABLE_OPENID_SIGNIN = false
|
|
ENABLE_OPENID_SIGNUP = false
|
|
|
|
[mailer]
|
|
ENABLED = false
|
|
|
|
[log]
|
|
MODE = console
|
|
LEVEL = info
|
|
ROOT_PATH = /data/gitea/log
|
|
EOF_GITEA
|
|
|
|
log "Create Caddyfile"
|
|
cat > "${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" <<EOF_COMPOSE
|
|
services:
|
|
caddy:
|
|
image: ${CADDY_IMAGE}
|
|
container_name: appfactory-caddy
|
|
restart: unless-stopped
|
|
ports:
|
|
- "${HTTP_PORT}:80"
|
|
- "${HTTPS_PORT}:443"
|
|
volumes:
|
|
- ../gateway/Caddyfile:/etc/caddy/Caddyfile:ro
|
|
- ../data/caddy:/data
|
|
- ../data/caddy-config:/config
|
|
networks:
|
|
- appfactory
|
|
|
|
gitea:
|
|
image: ${GITEA_IMAGE}
|
|
container_name: appfactory-gitea
|
|
restart: unless-stopped
|
|
environment:
|
|
- USER_UID=1000
|
|
- USER_GID=1000
|
|
- GITEA__server__ROOT_URL=http://${SERVER_HOST}:${GITEA_HTTP_PORT}/
|
|
- GITEA__server__SSH_PORT=${GITEA_SSH_PORT}
|
|
- GITEA__security__INSTALL_LOCK=true
|
|
volumes:
|
|
- ../data/gitea:/data
|
|
ports:
|
|
- "${GITEA_HTTP_PORT}:3000"
|
|
- "${GITEA_SSH_PORT}:22"
|
|
networks:
|
|
- appfactory
|
|
|
|
registry:
|
|
image: ${REGISTRY_IMAGE}
|
|
container_name: appfactory-registry
|
|
restart: unless-stopped
|
|
volumes:
|
|
- ../data/registry:/var/lib/registry
|
|
ports:
|
|
- "${REGISTRY_PORT}:5000"
|
|
networks:
|
|
- appfactory
|
|
|
|
networks:
|
|
appfactory:
|
|
name: appfactory
|
|
EOF_COMPOSE
|
|
|
|
log "Create App Catalog"
|
|
cat > "${BASE_DIR}/apps/catalog.yml" <<'EOF_CATALOG'
|
|
apps: []
|
|
EOF_CATALOG
|
|
|
|
log "Create documentation"
|
|
cat > "${BASE_DIR}/docs/README.md" <<EOF_DOCS
|
|
# AppFactory
|
|
|
|
Interní platforma pro Apps/mikroservisy.
|
|
|
|
## URLs
|
|
|
|
- Gitea: http://${SERVER_HOST}:${GITEA_HTTP_PORT}
|
|
- Registry: http://${SERVER_HOST}:${REGISTRY_PORT}/v2/
|
|
- Gateway health: http://${SERVER_HOST}/health
|
|
|
|
## Runtime folders
|
|
|
|
- Platform: ${BASE_DIR}
|
|
- Compose: ${BASE_DIR}/deploy/docker-compose.yml
|
|
- Caddyfile: ${BASE_DIR}/gateway/Caddyfile
|
|
- Gitea data: ${BASE_DIR}/data/gitea
|
|
- Registry data: ${BASE_DIR}/data/registry
|
|
|
|
## Commands
|
|
|
|
Start:
|
|
|
|
\`\`\`bash
|
|
cd ${BASE_DIR}/deploy
|
|
docker compose up -d
|
|
\`\`\`
|
|
|
|
Stop:
|
|
|
|
\`\`\`bash
|
|
cd ${BASE_DIR}/deploy
|
|
docker compose down
|
|
\`\`\`
|
|
|
|
Status:
|
|
|
|
\`\`\`bash
|
|
docker ps
|
|
\`\`\`
|
|
EOF_DOCS
|
|
|
|
log "Create Python FastAPI template folder"
|
|
mkdir -p "${BASE_DIR}/templates/python-fastapi-template/app"
|
|
|
|
cat > "${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 <app-id>"
|
|
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" <<EOF_APP
|
|
id: ${APP_ID}
|
|
name: ${APP_ID}
|
|
language: python
|
|
version: 1.0.0
|
|
base_path: /${APP_ID}
|
|
port: 8000
|
|
status: development
|
|
EOF_APP
|
|
|
|
echo ""
|
|
echo "Created app:"
|
|
echo " $TARGET_DIR"
|
|
echo ""
|
|
echo "Next:"
|
|
echo " cd $TARGET_DIR"
|
|
echo " docker build -t $APP_ID ."
|
|
echo " docker run --rm -p 8001:8000 $APP_ID"
|
|
EOF_NEW_APP
|
|
|
|
chmod +x "${BASE_DIR}/tools/new-app.sh"
|
|
|
|
log "Set permissions"
|
|
if id "${APP_USER}" >/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 <<EOF_DONE
|
|
|
|
DONE
|
|
|
|
Open:
|
|
Gitea: http://${SERVER_HOST}:${GITEA_HTTP_PORT}
|
|
Registry: http://${SERVER_HOST}:${REGISTRY_PORT}/v2/
|
|
Health: http://${SERVER_HOST}/health
|
|
|
|
Gitea admin:
|
|
username: ${GITEA_ADMIN_USER}
|
|
password: ${GITEA_ADMIN_PASSWORD}
|
|
|
|
Useful commands:
|
|
cd ${BASE_DIR}/deploy && docker compose ps
|
|
cd ${BASE_DIR}/deploy && docker compose logs -f
|
|
${BASE_DIR}/tools/new-app.sh gmail-app
|
|
|
|
IMPORTANT:
|
|
If this was first Docker install, log out and log in again so user '${APP_USER}' gets docker permissions.
|
|
|
|
EOF_DONE
|