Files
appfactory-infrastructure/bootstrap-appfactory-ubuntu.sh
2026-05-20 08:45:57 +00:00

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