Initial commit
This commit is contained in:
@@ -0,0 +1,192 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# devlab — DevLab Management CLI
|
||||||
|
# Usage: devlab <command> [args]
|
||||||
|
#
|
||||||
|
# Install to PATH: sudo cp devlab.sh /usr/local/bin/devlab && chmod +x /usr/local/bin/devlab
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CONFIG_FILE="/opt/lab/.devlab-config"
|
||||||
|
[[ -f "$CONFIG_FILE" ]] && source "$CONFIG_FILE"
|
||||||
|
LAB_ROOT="${LAB_ROOT:-/opt/lab}"
|
||||||
|
|
||||||
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
|
||||||
|
|
||||||
|
SERVICES=(caddy code-server gitea gitea-runner ollama open-webui portainer watchtower)
|
||||||
|
|
||||||
|
cmd_help() {
|
||||||
|
cat <<EOF
|
||||||
|
${BOLD}devlab${RESET} — DevLab Management CLI
|
||||||
|
|
||||||
|
${BOLD}COMMANDS${RESET}
|
||||||
|
status Show status of all services
|
||||||
|
start Start all services
|
||||||
|
stop Stop all services
|
||||||
|
restart [name] Restart all or a specific service
|
||||||
|
logs <name> Tail logs for a service
|
||||||
|
update Pull latest images and recreate containers
|
||||||
|
backup Run a manual backup now
|
||||||
|
models List downloaded Ollama models
|
||||||
|
pull <model> Pull an Ollama model (e.g. codellama:34b)
|
||||||
|
remove <model> Remove an Ollama model
|
||||||
|
urls Print all service URLs
|
||||||
|
compose up <file> Start a test environment from a compose file
|
||||||
|
compose down <file> Stop a test environment
|
||||||
|
compose ls List running compose stacks
|
||||||
|
|
||||||
|
${BOLD}SERVICES${RESET}
|
||||||
|
${SERVICES[*]}
|
||||||
|
|
||||||
|
${BOLD}EXAMPLES${RESET}
|
||||||
|
devlab status
|
||||||
|
devlab logs gitea
|
||||||
|
devlab pull mistral:latest
|
||||||
|
devlab compose up /opt/lab/compose/examples/lamp-stack.yml
|
||||||
|
devlab restart code-server
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_status() {
|
||||||
|
echo -e "\n${BOLD}DevLab Service Status${RESET}\n"
|
||||||
|
printf "%-20s %-12s %s\n" "CONTAINER" "STATUS" "IMAGE"
|
||||||
|
printf '%.0s─' {1..60}; echo
|
||||||
|
for svc in "${SERVICES[@]}"; do
|
||||||
|
local info
|
||||||
|
info=$(docker inspect "$svc" --format '{{.State.Status}} {{.Config.Image}}' 2>/dev/null) || {
|
||||||
|
printf "%-20s ${RED}%-12s${RESET} %s\n" "$svc" "not found" "-"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
local status="${info%% *}"
|
||||||
|
local image="${info#* }"
|
||||||
|
local color="$RED"
|
||||||
|
[[ "$status" == "running" ]] && color="$GREEN"
|
||||||
|
printf "%-20s ${color}%-12s${RESET} %s\n" "$svc" "$status" "$image"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_start() {
|
||||||
|
for svc in "${SERVICES[@]}"; do
|
||||||
|
docker start "$svc" 2>/dev/null && \
|
||||||
|
echo -e " ${GREEN}✓${RESET} ${svc}" || \
|
||||||
|
echo -e " ${RED}✗${RESET} ${svc} (not found)"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_stop() {
|
||||||
|
read -rp "Stop all DevLab services? [y/N]: " confirm
|
||||||
|
[[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; return; }
|
||||||
|
for svc in "${SERVICES[@]}"; do
|
||||||
|
docker stop "$svc" 2>/dev/null && \
|
||||||
|
echo -e " ${YELLOW}■${RESET} ${svc} stopped" || true
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_restart() {
|
||||||
|
local target="${1:-all}"
|
||||||
|
if [[ "$target" == "all" ]]; then
|
||||||
|
for svc in "${SERVICES[@]}"; do
|
||||||
|
docker restart "$svc" 2>/dev/null && \
|
||||||
|
echo -e " ${GREEN}↺${RESET} ${svc}" || true
|
||||||
|
done
|
||||||
|
else
|
||||||
|
docker restart "$target" && echo -e " ${GREEN}↺${RESET} ${target} restarted"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_logs() {
|
||||||
|
local svc="${1:-}"
|
||||||
|
[[ -z "$svc" ]] && { echo "Usage: devlab logs <service>"; exit 1; }
|
||||||
|
docker logs -f --tail=100 "$svc"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_update() {
|
||||||
|
echo -e "${CYAN}Pulling latest images...${RESET}"
|
||||||
|
for svc in "${SERVICES[@]}"; do
|
||||||
|
local image
|
||||||
|
image=$(docker inspect "$svc" --format '{{.Config.Image}}' 2>/dev/null) || continue
|
||||||
|
echo -e " Updating ${image}..."
|
||||||
|
docker pull "$image" || true
|
||||||
|
docker restart "$svc" || true
|
||||||
|
done
|
||||||
|
echo -e "${GREEN}Update complete.${RESET}"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_backup() {
|
||||||
|
[[ -x "${LAB_ROOT}/backup.sh" ]] || { echo "Backup script not found."; exit 1; }
|
||||||
|
"${LAB_ROOT}/backup.sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_models() {
|
||||||
|
docker exec ollama ollama list
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_pull() {
|
||||||
|
local model="${1:-}"
|
||||||
|
[[ -z "$model" ]] && { echo "Usage: devlab pull <model>"; exit 1; }
|
||||||
|
docker exec ollama ollama pull "$model"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_remove_model() {
|
||||||
|
local model="${1:-}"
|
||||||
|
[[ -z "$model" ]] && { echo "Usage: devlab remove <model>"; exit 1; }
|
||||||
|
docker exec ollama ollama rm "$model"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_urls() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}DevLab Service URLs${RESET}"
|
||||||
|
echo -e " Web IDE → ${GREEN}https://${CODE_HOST:-code.yourdomain.com}${RESET}"
|
||||||
|
echo -e " Git → ${GREEN}https://${GIT_HOST:-git.yourdomain.com}${RESET}"
|
||||||
|
echo -e " AI Chat → ${GREEN}https://${AI_HOST:-ai.yourdomain.com}${RESET}"
|
||||||
|
echo -e " Portainer → ${GREEN}https://${PM_HOST:-portainer.yourdomain.com}${RESET}"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_compose() {
|
||||||
|
local action="${1:-}"
|
||||||
|
shift
|
||||||
|
case "$action" in
|
||||||
|
up)
|
||||||
|
local file="${1:-}"
|
||||||
|
[[ -z "$file" ]] && { echo "Usage: devlab compose up <file.yml>"; exit 1; }
|
||||||
|
docker compose -f "$file" up -d
|
||||||
|
echo -e "${GREEN}Stack started.${RESET}"
|
||||||
|
;;
|
||||||
|
down)
|
||||||
|
local file="${1:-}"
|
||||||
|
[[ -z "$file" ]] && { echo "Usage: devlab compose down <file.yml>"; exit 1; }
|
||||||
|
docker compose -f "$file" down
|
||||||
|
;;
|
||||||
|
ls)
|
||||||
|
docker compose ls
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: devlab compose {up|down|ls} [file]"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Router ──────────────────────────────────────────────────────────────────
|
||||||
|
COMMAND="${1:-help}"
|
||||||
|
shift 2>/dev/null || true
|
||||||
|
|
||||||
|
case "$COMMAND" in
|
||||||
|
status) cmd_status ;;
|
||||||
|
start) cmd_start ;;
|
||||||
|
stop) cmd_stop ;;
|
||||||
|
restart) cmd_restart "$@" ;;
|
||||||
|
logs) cmd_logs "$@" ;;
|
||||||
|
update) cmd_update ;;
|
||||||
|
backup) cmd_backup ;;
|
||||||
|
models) cmd_models ;;
|
||||||
|
pull) cmd_pull "$@" ;;
|
||||||
|
remove) cmd_remove_model "$@" ;;
|
||||||
|
urls) cmd_urls ;;
|
||||||
|
compose) cmd_compose "$@" ;;
|
||||||
|
help|--help|-h) cmd_help ;;
|
||||||
|
*) echo "Unknown command: ${COMMAND}. Run: devlab help" && exit 1 ;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,672 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# DevLab Installer — Rocky Linux 10 / Hetzner Dedicated
|
||||||
|
# Installs: Docker, Caddy, code-server, Gitea, Gitea Runner,
|
||||||
|
# Ollama, Open WebUI, Portainer, Watchtower
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# chmod +x install-devlab.sh
|
||||||
|
# sudo ./install-devlab.sh
|
||||||
|
#
|
||||||
|
# Environment variables (override defaults):
|
||||||
|
# DOMAIN Base domain (e.g. example.com)
|
||||||
|
# EMAIL Let's Encrypt email
|
||||||
|
# LAB_ROOT Install root (default /opt/lab)
|
||||||
|
# CODE_PASSWORD code-server password
|
||||||
|
# GITEA_ADMIN Gitea admin username
|
||||||
|
# GITEA_PASS Gitea admin password
|
||||||
|
# GITEA_EMAIL Gitea admin email
|
||||||
|
# PULL_MODELS Space-separated Ollama model list
|
||||||
|
# e.g. "codellama:34b mistral:latest"
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ─── Colour helpers ──────────────────────────────────────────────────────────
|
||||||
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
|
||||||
|
info() { echo -e "${CYAN}[INFO]${RESET} $*"; }
|
||||||
|
success() { echo -e "${GREEN}[OK]${RESET} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
|
||||||
|
error() { echo -e "${RED}[ERROR]${RESET} $*"; exit 1; }
|
||||||
|
header() { echo -e "\n${BOLD}${CYAN}══════════════════════════════════════════════${RESET}"; \
|
||||||
|
echo -e "${BOLD}${CYAN} $*${RESET}"; \
|
||||||
|
echo -e "${BOLD}${CYAN}══════════════════════════════════════════════${RESET}"; }
|
||||||
|
|
||||||
|
# ─── Root check ──────────────────────────────────────────────────────────────
|
||||||
|
[[ $EUID -ne 0 ]] && error "Run this script as root: sudo ./install-devlab.sh"
|
||||||
|
|
||||||
|
# ─── Interactive configuration ───────────────────────────────────────────────
|
||||||
|
header "DevLab Setup — Interactive Configuration"
|
||||||
|
|
||||||
|
prompt_val() {
|
||||||
|
local var="$1" prompt="$2" default="$3" secret="${4:-}"
|
||||||
|
if [[ -n "${!var:-}" ]]; then
|
||||||
|
info "$var already set (${!var:0:6}…)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ "$secret" == "secret" ]]; then
|
||||||
|
read -rsp "${prompt} [default: ${default}]: " val; echo
|
||||||
|
else
|
||||||
|
read -rp "${prompt} [default: ${default}]: " val
|
||||||
|
fi
|
||||||
|
printf -v "$var" '%s' "${val:-$default}"
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt_val DOMAIN "Base domain (e.g. yourdomain.com)" "lab.example.com"
|
||||||
|
prompt_val EMAIL "Let's Encrypt / admin email" "admin@${DOMAIN}"
|
||||||
|
prompt_val LAB_ROOT "Install root directory" "/opt/lab"
|
||||||
|
prompt_val CODE_PASSWORD "code-server password" "ChangeMe123!" secret
|
||||||
|
prompt_val GITEA_ADMIN "Gitea admin username" "labadmin"
|
||||||
|
prompt_val GITEA_PASS "Gitea admin password" "ChangeMe456!" secret
|
||||||
|
prompt_val GITEA_EMAIL "Gitea admin email" "${EMAIL}"
|
||||||
|
prompt_val PULL_MODELS "Ollama models to pull (space-separated, leave blank to skip)" \
|
||||||
|
"codellama:13b"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}Configuration summary:${RESET}"
|
||||||
|
echo -e " Domain : ${CYAN}${DOMAIN}${RESET}"
|
||||||
|
echo -e " Email : ${CYAN}${EMAIL}${RESET}"
|
||||||
|
echo -e " Lab root : ${CYAN}${LAB_ROOT}${RESET}"
|
||||||
|
echo -e " Gitea admin: ${CYAN}${GITEA_ADMIN}${RESET}"
|
||||||
|
echo -e " Models : ${CYAN}${PULL_MODELS:-none}${RESET}"
|
||||||
|
echo ""
|
||||||
|
read -rp "Proceed with installation? [y/N]: " confirm
|
||||||
|
[[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; }
|
||||||
|
|
||||||
|
# ─── Subdomains ──────────────────────────────────────────────────────────────
|
||||||
|
CODE_HOST="code.${DOMAIN}"
|
||||||
|
GIT_HOST="git.${DOMAIN}"
|
||||||
|
AI_HOST="ai.${DOMAIN}"
|
||||||
|
PM_HOST="portainer.${DOMAIN}"
|
||||||
|
RUNNER_TOKEN="" # filled later
|
||||||
|
|
||||||
|
# ─── Directory layout ────────────────────────────────────────────────────────
|
||||||
|
header "Creating directory structure"
|
||||||
|
DIRS=(
|
||||||
|
"${LAB_ROOT}/caddy/config"
|
||||||
|
"${LAB_ROOT}/caddy/data"
|
||||||
|
"${LAB_ROOT}/workspace"
|
||||||
|
"${LAB_ROOT}/gitea"
|
||||||
|
"${LAB_ROOT}/gitea-runner"
|
||||||
|
"${LAB_ROOT}/ollama"
|
||||||
|
"${LAB_ROOT}/portainer"
|
||||||
|
"${LAB_ROOT}/open-webui"
|
||||||
|
"${LAB_ROOT}/compose"
|
||||||
|
"${LAB_ROOT}/backups"
|
||||||
|
)
|
||||||
|
for d in "${DIRS[@]}"; do mkdir -p "$d"; done
|
||||||
|
success "Directories created under ${LAB_ROOT}"
|
||||||
|
|
||||||
|
# ─── System packages ─────────────────────────────────────────────────────────
|
||||||
|
header "Installing system packages"
|
||||||
|
dnf -y update
|
||||||
|
|
||||||
|
# Enable EPEL — required for htop, btop, and other tools not in Rocky 10 base repos
|
||||||
|
info "Enabling EPEL repository..."
|
||||||
|
dnf -y install epel-release 2>/dev/null || \
|
||||||
|
dnf -y install "https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm" 2>/dev/null || \
|
||||||
|
warn "EPEL install failed — htop/btop may not be available"
|
||||||
|
dnf -y makecache 2>/dev/null || true
|
||||||
|
|
||||||
|
# Core packages — all available in base repos (must succeed)
|
||||||
|
dnf -y install \
|
||||||
|
curl wget git vim unzip tar \
|
||||||
|
firewalld bash-completion \
|
||||||
|
ca-certificates gnupg2 \
|
||||||
|
python3 python3-pip \
|
||||||
|
net-tools lsof
|
||||||
|
|
||||||
|
# Optional monitoring tools — from EPEL, non-fatal if unavailable
|
||||||
|
dnf -y install htop 2>/dev/null && success "htop installed" || \
|
||||||
|
dnf -y install btop 2>/dev/null && success "btop installed (htop unavailable)" || \
|
||||||
|
warn "htop/btop not available — skipping (optional)"
|
||||||
|
|
||||||
|
success "System packages installed"
|
||||||
|
|
||||||
|
# ─── Firewall ─────────────────────────────────────────────────────────────────
|
||||||
|
header "Configuring firewall"
|
||||||
|
systemctl enable --now firewalld
|
||||||
|
firewall-cmd --permanent --add-service=http
|
||||||
|
firewall-cmd --permanent --add-service=https
|
||||||
|
firewall-cmd --permanent --add-service=ssh
|
||||||
|
# Remove default open ports that Hetzner sometimes pre-opens
|
||||||
|
firewall-cmd --permanent --remove-service=cockpit 2>/dev/null || true
|
||||||
|
firewall-cmd --reload
|
||||||
|
success "Firewall configured (HTTP, HTTPS, SSH)"
|
||||||
|
|
||||||
|
# ─── Docker ──────────────────────────────────────────────────────────────────
|
||||||
|
header "Installing Docker CE"
|
||||||
|
if command -v docker &>/dev/null; then
|
||||||
|
warn "Docker already installed — skipping"
|
||||||
|
else
|
||||||
|
dnf -y install dnf-plugins-core
|
||||||
|
dnf config-manager --add-repo \
|
||||||
|
https://download.docker.com/linux/rhel/docker-ce.repo
|
||||||
|
dnf -y install docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||||
|
systemctl enable --now docker
|
||||||
|
success "Docker CE installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure docker group exists and add current SUDO_USER if set
|
||||||
|
if [[ -n "${SUDO_USER:-}" ]]; then
|
||||||
|
usermod -aG docker "$SUDO_USER"
|
||||||
|
info "Added ${SUDO_USER} to docker group"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── Docker network ───────────────────────────────────────────────────────────
|
||||||
|
docker network create devlab 2>/dev/null || true
|
||||||
|
success "Docker network 'devlab' ready"
|
||||||
|
|
||||||
|
# ─── Caddy (reverse proxy + auto-SSL) ────────────────────────────────────────
|
||||||
|
header "Deploying Caddy (reverse proxy + Let's Encrypt)"
|
||||||
|
|
||||||
|
cat > "${LAB_ROOT}/caddy/config/Caddyfile" <<EOF
|
||||||
|
{
|
||||||
|
email ${EMAIL}
|
||||||
|
}
|
||||||
|
|
||||||
|
${CODE_HOST} {
|
||||||
|
reverse_proxy code-server:8080
|
||||||
|
}
|
||||||
|
|
||||||
|
${GIT_HOST} {
|
||||||
|
reverse_proxy gitea:3000
|
||||||
|
}
|
||||||
|
|
||||||
|
${AI_HOST} {
|
||||||
|
reverse_proxy open-webui:8080
|
||||||
|
}
|
||||||
|
|
||||||
|
${PM_HOST} {
|
||||||
|
reverse_proxy portainer:9000
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
docker run -d \
|
||||||
|
--name caddy \
|
||||||
|
--restart unless-stopped \
|
||||||
|
--network devlab \
|
||||||
|
-p 80:80 \
|
||||||
|
-p 443:443 \
|
||||||
|
-v "${LAB_ROOT}/caddy/config/Caddyfile:/etc/caddy/Caddyfile:ro" \
|
||||||
|
-v "${LAB_ROOT}/caddy/data:/data" \
|
||||||
|
-v "${LAB_ROOT}/caddy/config:/config" \
|
||||||
|
caddy:latest
|
||||||
|
|
||||||
|
success "Caddy running"
|
||||||
|
|
||||||
|
# ─── code-server ─────────────────────────────────────────────────────────────
|
||||||
|
header "Deploying code-server (web IDE)"
|
||||||
|
|
||||||
|
# Build custom image with multi-language runtimes
|
||||||
|
cat > /tmp/code-server.Dockerfile <<'DOCKERFILE'
|
||||||
|
FROM codercom/code-server:latest
|
||||||
|
USER root
|
||||||
|
|
||||||
|
# Node.js (via NodeSource)
|
||||||
|
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
|
||||||
|
apt-get install -y nodejs
|
||||||
|
|
||||||
|
# Python extras
|
||||||
|
RUN apt-get install -y python3-pip python3-venv python3-full
|
||||||
|
|
||||||
|
# Go
|
||||||
|
RUN curl -fsSL https://go.dev/dl/go1.23.0.linux-amd64.tar.gz \
|
||||||
|
| tar -C /usr/local -xz
|
||||||
|
ENV PATH="/usr/local/go/bin:${PATH}"
|
||||||
|
|
||||||
|
# Rust
|
||||||
|
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --no-modify-path
|
||||||
|
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||||
|
|
||||||
|
# PHP, Ruby, Java, C/C++ tools
|
||||||
|
RUN apt-get install -y \
|
||||||
|
php-cli php-curl php-mbstring \
|
||||||
|
ruby-full \
|
||||||
|
default-jdk-headless \
|
||||||
|
gcc g++ cmake make \
|
||||||
|
jq tree ripgrep fd-find fzf
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
RUN apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
USER coder
|
||||||
|
# Pre-install popular VS Code extensions
|
||||||
|
RUN code-server --install-extension ms-python.python \
|
||||||
|
--install-extension golang.go \
|
||||||
|
--install-extension rust-lang.rust-analyzer \
|
||||||
|
--install-extension dbaeumer.vscode-eslint \
|
||||||
|
--install-extension esbenp.prettier-vscode \
|
||||||
|
--install-extension eamodio.gitlens \
|
||||||
|
--install-extension ms-azuretools.vscode-docker \
|
||||||
|
--install-extension PKief.material-icon-theme \
|
||||||
|
2>/dev/null || true
|
||||||
|
DOCKERFILE
|
||||||
|
|
||||||
|
info "Building code-server image with multi-language runtimes (this takes a few minutes)..."
|
||||||
|
docker build -t devlab/code-server:latest -f /tmp/code-server.Dockerfile /tmp/
|
||||||
|
|
||||||
|
docker run -d \
|
||||||
|
--name code-server \
|
||||||
|
--restart unless-stopped \
|
||||||
|
--network devlab \
|
||||||
|
-e PASSWORD="${CODE_PASSWORD}" \
|
||||||
|
-e DOCKER_HOST=unix:///var/run/docker.sock \
|
||||||
|
-v "${LAB_ROOT}/workspace:/home/coder/workspace" \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
devlab/code-server:latest
|
||||||
|
|
||||||
|
success "code-server running → https://${CODE_HOST}"
|
||||||
|
|
||||||
|
# ─── Gitea ────────────────────────────────────────────────────────────────────
|
||||||
|
header "Deploying Gitea (self-hosted Git)"
|
||||||
|
|
||||||
|
docker run -d \
|
||||||
|
--name gitea \
|
||||||
|
--restart unless-stopped \
|
||||||
|
--network devlab \
|
||||||
|
-e USER_UID=1000 \
|
||||||
|
-e USER_GID=1000 \
|
||||||
|
-e GITEA__database__DB_TYPE=sqlite3 \
|
||||||
|
-e GITEA__server__DOMAIN="${GIT_HOST}" \
|
||||||
|
-e GITEA__server__ROOT_URL="https://${GIT_HOST}" \
|
||||||
|
-e GITEA__server__SSH_PORT=2222 \
|
||||||
|
-e GITEA__server__HTTP_PORT=3000 \
|
||||||
|
-e GITEA__service__DISABLE_REGISTRATION=true \
|
||||||
|
-e GITEA__service__REQUIRE_SIGNIN_VIEW=false \
|
||||||
|
-e GITEA__log__LEVEL=Info \
|
||||||
|
-e GITEA__actions__ENABLED=true \
|
||||||
|
-v "${LAB_ROOT}/gitea:/data" \
|
||||||
|
-p 2222:22 \
|
||||||
|
gitea/gitea:latest
|
||||||
|
|
||||||
|
# Wait for Gitea to boot
|
||||||
|
info "Waiting for Gitea to initialise..."
|
||||||
|
sleep 15
|
||||||
|
|
||||||
|
# Create admin user via gitea CLI inside the container
|
||||||
|
docker exec -u git gitea \
|
||||||
|
gitea admin user create \
|
||||||
|
--username "${GITEA_ADMIN}" \
|
||||||
|
--password "${GITEA_PASS}" \
|
||||||
|
--email "${GITEA_EMAIL}" \
|
||||||
|
--admin \
|
||||||
|
--must-change-password=false 2>/dev/null || \
|
||||||
|
warn "Gitea admin user may already exist — skipping"
|
||||||
|
|
||||||
|
success "Gitea running → https://${GIT_HOST}"
|
||||||
|
|
||||||
|
# ─── Gitea Actions Runner ─────────────────────────────────────────────────────
|
||||||
|
header "Deploying Gitea Actions Runner (CI/CD)"
|
||||||
|
|
||||||
|
# Generate a runner token via Gitea API
|
||||||
|
info "Generating runner registration token..."
|
||||||
|
sleep 5
|
||||||
|
RUNNER_TOKEN=$(docker exec -u git gitea \
|
||||||
|
gitea actions generate-runner-token 2>/dev/null | tail -1) || true
|
||||||
|
|
||||||
|
if [[ -z "${RUNNER_TOKEN}" ]]; then
|
||||||
|
warn "Could not auto-generate runner token — you will need to register manually."
|
||||||
|
warn "See documentation section 7 for instructions."
|
||||||
|
RUNNER_TOKEN="NEEDS_MANUAL_REGISTRATION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Write runner config
|
||||||
|
cat > "${LAB_ROOT}/gitea-runner/config.yml" <<EOF
|
||||||
|
log:
|
||||||
|
level: info
|
||||||
|
runner:
|
||||||
|
file: .runner
|
||||||
|
capacity: 5
|
||||||
|
envs: {}
|
||||||
|
timeout: 3h
|
||||||
|
insecure: false
|
||||||
|
fetch_timeout: 5s
|
||||||
|
fetch_interval: 2s
|
||||||
|
cache:
|
||||||
|
enabled: true
|
||||||
|
dir: ""
|
||||||
|
host: ""
|
||||||
|
port: 0
|
||||||
|
container:
|
||||||
|
network: "devlab"
|
||||||
|
enable_ipv6: false
|
||||||
|
valid_volumes:
|
||||||
|
- /opt/lab/**
|
||||||
|
EOF
|
||||||
|
|
||||||
|
docker run -d \
|
||||||
|
--name gitea-runner \
|
||||||
|
--restart unless-stopped \
|
||||||
|
--network devlab \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
-v "${LAB_ROOT}/gitea-runner:/data" \
|
||||||
|
-e GITEA_INSTANCE_URL="http://gitea:3000" \
|
||||||
|
-e GITEA_RUNNER_REGISTRATION_TOKEN="${RUNNER_TOKEN}" \
|
||||||
|
-e GITEA_RUNNER_NAME="devlab-runner-1" \
|
||||||
|
gitea/act_runner:latest
|
||||||
|
|
||||||
|
success "Gitea Actions Runner deployed"
|
||||||
|
|
||||||
|
# ─── Ollama ───────────────────────────────────────────────────────────────────
|
||||||
|
header "Deploying Ollama (local AI runtime)"
|
||||||
|
|
||||||
|
docker run -d \
|
||||||
|
--name ollama \
|
||||||
|
--restart unless-stopped \
|
||||||
|
--network devlab \
|
||||||
|
-v "${LAB_ROOT}/ollama:/root/.ollama" \
|
||||||
|
-p 127.0.0.1:11434:11434 \
|
||||||
|
ollama/ollama:latest
|
||||||
|
|
||||||
|
success "Ollama running on localhost:11434"
|
||||||
|
|
||||||
|
# Pull models if requested
|
||||||
|
if [[ -n "${PULL_MODELS}" ]]; then
|
||||||
|
info "Pulling AI models in background: ${PULL_MODELS}"
|
||||||
|
for model in $PULL_MODELS; do
|
||||||
|
docker exec ollama ollama pull "$model" &
|
||||||
|
info " → pulling ${model} (background)"
|
||||||
|
done
|
||||||
|
success "Model pulls started (run 'docker exec ollama ollama list' to check progress)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── Open WebUI ───────────────────────────────────────────────────────────────
|
||||||
|
header "Deploying Open WebUI (AI chat interface)"
|
||||||
|
|
||||||
|
docker run -d \
|
||||||
|
--name open-webui \
|
||||||
|
--restart unless-stopped \
|
||||||
|
--network devlab \
|
||||||
|
-e OLLAMA_BASE_URL=http://ollama:11434 \
|
||||||
|
-e WEBUI_SECRET_KEY="$(openssl rand -hex 32)" \
|
||||||
|
-v "${LAB_ROOT}/open-webui:/app/backend/data" \
|
||||||
|
ghcr.io/open-webui/open-webui:main
|
||||||
|
|
||||||
|
success "Open WebUI running → https://${AI_HOST}"
|
||||||
|
|
||||||
|
# ─── Portainer ────────────────────────────────────────────────────────────────
|
||||||
|
header "Deploying Portainer (container management UI)"
|
||||||
|
|
||||||
|
docker run -d \
|
||||||
|
--name portainer \
|
||||||
|
--restart unless-stopped \
|
||||||
|
--network devlab \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
-v "${LAB_ROOT}/portainer:/data" \
|
||||||
|
portainer/portainer-ce:latest
|
||||||
|
|
||||||
|
success "Portainer running → https://${PM_HOST}"
|
||||||
|
|
||||||
|
# ─── Watchtower (auto-updates) ────────────────────────────────────────────────
|
||||||
|
header "Deploying Watchtower (automatic container updates)"
|
||||||
|
|
||||||
|
docker run -d \
|
||||||
|
--name watchtower \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
containrrr/watchtower \
|
||||||
|
--schedule "0 0 4 * * *" \
|
||||||
|
--cleanup \
|
||||||
|
--label-enable
|
||||||
|
|
||||||
|
success "Watchtower scheduled (04:00 daily)"
|
||||||
|
|
||||||
|
# ─── Docker Compose stacks helper ────────────────────────────────────────────
|
||||||
|
header "Creating example test environment templates"
|
||||||
|
|
||||||
|
mkdir -p "${LAB_ROOT}/compose/examples"
|
||||||
|
|
||||||
|
# Example: full LAMP stack for testing
|
||||||
|
cat > "${LAB_ROOT}/compose/examples/lamp-stack.yml" <<'YAML'
|
||||||
|
# Example test environment: LAMP stack
|
||||||
|
# Usage: docker compose -f lamp-stack.yml up -d
|
||||||
|
# Access: http://localhost:8888
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: php:8.3-apache
|
||||||
|
ports: ["8888:80"]
|
||||||
|
volumes: ["./app:/var/www/html"]
|
||||||
|
networks: [test]
|
||||||
|
db:
|
||||||
|
image: mariadb:11
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: testpass
|
||||||
|
MYSQL_DATABASE: testdb
|
||||||
|
volumes: ["dbdata:/var/lib/mysql"]
|
||||||
|
networks: [test]
|
||||||
|
phpmyadmin:
|
||||||
|
image: phpmyadmin:latest
|
||||||
|
ports: ["8889:80"]
|
||||||
|
environment:
|
||||||
|
PMA_HOST: db
|
||||||
|
networks: [test]
|
||||||
|
volumes:
|
||||||
|
dbdata:
|
||||||
|
networks:
|
||||||
|
test:
|
||||||
|
YAML
|
||||||
|
|
||||||
|
# Example: Node.js + PostgreSQL
|
||||||
|
cat > "${LAB_ROOT}/compose/examples/node-postgres.yml" <<'YAML'
|
||||||
|
# Example test environment: Node.js + PostgreSQL + Redis
|
||||||
|
# Usage: docker compose -f node-postgres.yml up -d
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: node:22-alpine
|
||||||
|
working_dir: /app
|
||||||
|
volumes: ["./app:/app"]
|
||||||
|
command: sh -c "npm install && npm start"
|
||||||
|
ports: ["3000:3000"]
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgres://user:pass@db/appdb
|
||||||
|
REDIS_URL: redis://cache:6379
|
||||||
|
networks: [test]
|
||||||
|
depends_on: [db, cache]
|
||||||
|
db:
|
||||||
|
image: postgres:16
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: user
|
||||||
|
POSTGRES_PASSWORD: pass
|
||||||
|
POSTGRES_DB: appdb
|
||||||
|
volumes: ["pgdata:/var/lib/postgresql/data"]
|
||||||
|
networks: [test]
|
||||||
|
cache:
|
||||||
|
image: redis:7-alpine
|
||||||
|
networks: [test]
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
|
networks:
|
||||||
|
test:
|
||||||
|
YAML
|
||||||
|
|
||||||
|
# Example: Django + MySQL
|
||||||
|
cat > "${LAB_ROOT}/compose/examples/django-mysql.yml" <<'YAML'
|
||||||
|
# Example test environment: Django + MySQL
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: python:3.12-slim
|
||||||
|
working_dir: /app
|
||||||
|
volumes: ["./app:/app"]
|
||||||
|
command: sh -c "pip install -r requirements.txt && python manage.py runserver 0.0.0.0:8000"
|
||||||
|
ports: ["8000:8000"]
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: mysql://user:pass@db/appdb
|
||||||
|
networks: [test]
|
||||||
|
depends_on: [db]
|
||||||
|
db:
|
||||||
|
image: mysql:8.3
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: rootpass
|
||||||
|
MYSQL_DATABASE: appdb
|
||||||
|
MYSQL_USER: user
|
||||||
|
MYSQL_PASSWORD: pass
|
||||||
|
volumes: ["mysqldata:/var/lib/mysql"]
|
||||||
|
networks: [test]
|
||||||
|
volumes:
|
||||||
|
mysqldata:
|
||||||
|
networks:
|
||||||
|
test:
|
||||||
|
YAML
|
||||||
|
|
||||||
|
success "Example compose stacks written to ${LAB_ROOT}/compose/examples/"
|
||||||
|
|
||||||
|
# ─── Gitea workflow example ───────────────────────────────────────────────────
|
||||||
|
mkdir -p "${LAB_ROOT}/workspace/.gitea/workflows"
|
||||||
|
cat > "${LAB_ROOT}/workspace/.gitea/workflows/ci.yml" <<'YAML'
|
||||||
|
# Example Gitea Actions CI workflow
|
||||||
|
# Place this in your repo at: .gitea/workflows/ci.yml
|
||||||
|
name: CI Pipeline
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: pytest --tb=short
|
||||||
|
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Lint with ruff
|
||||||
|
run: pip install ruff && ruff check .
|
||||||
|
|
||||||
|
docker-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [test, lint]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Build Docker image
|
||||||
|
run: docker build -t myapp:${{ github.sha }} .
|
||||||
|
YAML
|
||||||
|
|
||||||
|
success "Example CI workflow written"
|
||||||
|
|
||||||
|
# ─── Backup script ────────────────────────────────────────────────────────────
|
||||||
|
header "Creating backup script"
|
||||||
|
|
||||||
|
cat > "${LAB_ROOT}/backup.sh" <<BACKUP
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# DevLab backup script — uses rsync to /opt/lab/backups/
|
||||||
|
# Schedule with: crontab -e → 0 3 * * * /opt/lab/backup.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
BACKUP_DIR="${LAB_ROOT}/backups/\$(date +%Y-%m-%d)"
|
||||||
|
mkdir -p "\$BACKUP_DIR"
|
||||||
|
|
||||||
|
echo "[BACKUP] Starting at \$(date)"
|
||||||
|
|
||||||
|
# Stop containers for consistent snapshots (optional — comment out for hot backup)
|
||||||
|
# docker stop gitea code-server open-webui portainer 2>/dev/null
|
||||||
|
|
||||||
|
rsync -a --exclude='ollama' "${LAB_ROOT}/" "\$BACKUP_DIR/" \
|
||||||
|
--exclude='backups' --exclude='*.log'
|
||||||
|
|
||||||
|
# Restart if stopped
|
||||||
|
# docker start gitea code-server open-webui portainer 2>/dev/null
|
||||||
|
|
||||||
|
# Prune backups older than 14 days
|
||||||
|
find "${LAB_ROOT}/backups" -maxdepth 1 -type d -mtime +14 -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "[BACKUP] Done → \$BACKUP_DIR"
|
||||||
|
BACKUP
|
||||||
|
|
||||||
|
chmod +x "${LAB_ROOT}/backup.sh"
|
||||||
|
|
||||||
|
# Install cron job for daily backup at 03:00
|
||||||
|
(crontab -l 2>/dev/null | grep -v backup.sh; echo "0 3 * * * ${LAB_ROOT}/backup.sh >> ${LAB_ROOT}/backups/backup.log 2>&1") | crontab -
|
||||||
|
success "Backup script installed (runs daily at 03:00)"
|
||||||
|
|
||||||
|
# ─── System tuning ────────────────────────────────────────────────────────────
|
||||||
|
header "Applying system optimisations"
|
||||||
|
|
||||||
|
# Increase file descriptor limits for Docker
|
||||||
|
cat > /etc/security/limits.d/99-devlab.conf <<EOF
|
||||||
|
* soft nofile 65536
|
||||||
|
* hard nofile 65536
|
||||||
|
root soft nofile 65536
|
||||||
|
root hard nofile 65536
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Kernel network tuning
|
||||||
|
cat > /etc/sysctl.d/99-devlab.conf <<EOF
|
||||||
|
net.core.somaxconn = 65535
|
||||||
|
net.ipv4.tcp_max_syn_backlog = 65535
|
||||||
|
net.ipv4.ip_local_port_range = 1024 65535
|
||||||
|
vm.max_map_count = 262144
|
||||||
|
fs.file-max = 2097152
|
||||||
|
EOF
|
||||||
|
sysctl --system &>/dev/null
|
||||||
|
success "System limits tuned"
|
||||||
|
|
||||||
|
# ─── Save config file ─────────────────────────────────────────────────────────
|
||||||
|
cat > "${LAB_ROOT}/.devlab-config" <<CONFIG
|
||||||
|
# DevLab configuration — generated $(date)
|
||||||
|
DOMAIN=${DOMAIN}
|
||||||
|
EMAIL=${EMAIL}
|
||||||
|
LAB_ROOT=${LAB_ROOT}
|
||||||
|
CODE_HOST=${CODE_HOST}
|
||||||
|
GIT_HOST=${GIT_HOST}
|
||||||
|
AI_HOST=${AI_HOST}
|
||||||
|
PM_HOST=${PM_HOST}
|
||||||
|
GITEA_ADMIN=${GITEA_ADMIN}
|
||||||
|
GITEA_EMAIL=${GITEA_EMAIL}
|
||||||
|
PULL_MODELS=${PULL_MODELS}
|
||||||
|
INSTALLED=$(date -Iseconds)
|
||||||
|
CONFIG
|
||||||
|
|
||||||
|
chmod 600 "${LAB_ROOT}/.devlab-config"
|
||||||
|
|
||||||
|
# ─── Final status ─────────────────────────────────────────────────────────────
|
||||||
|
header "Installation Complete"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}Service URLs${RESET}"
|
||||||
|
echo -e " Web IDE → ${GREEN}https://${CODE_HOST}${RESET}"
|
||||||
|
echo -e " Git (Gitea) → ${GREEN}https://${GIT_HOST}${RESET}"
|
||||||
|
echo -e " AI Chat → ${GREEN}https://${AI_HOST}${RESET}"
|
||||||
|
echo -e " Containers UI → ${GREEN}https://${PM_HOST}${RESET}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}Credentials${RESET}"
|
||||||
|
echo -e " code-server password : ${CYAN}${CODE_PASSWORD}${RESET}"
|
||||||
|
echo -e " Gitea admin : ${CYAN}${GITEA_ADMIN} / ${GITEA_PASS}${RESET}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}SSH Git access${RESET}"
|
||||||
|
echo -e " git clone ssh://git@${DOMAIN}:2222/username/repo.git"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}DNS records required${RESET}"
|
||||||
|
echo -e " A code.${DOMAIN} → <this server IP>"
|
||||||
|
echo -e " A git.${DOMAIN} → <this server IP>"
|
||||||
|
echo -e " A ai.${DOMAIN} → <this server IP>"
|
||||||
|
echo -e " A portainer.${DOMAIN} → <this server IP>"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}Useful commands${RESET}"
|
||||||
|
echo -e " docker ps -a # list all containers"
|
||||||
|
echo -e " docker logs -f <name> # follow container logs"
|
||||||
|
echo -e " docker exec ollama ollama list # list downloaded AI models"
|
||||||
|
echo -e " docker exec ollama ollama pull <m> # pull new AI model"
|
||||||
|
echo -e " ${LAB_ROOT}/backup.sh # run manual backup"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}⚠ Point your DNS A records before SSL certs can be issued.${RESET}"
|
||||||
|
echo -e "${YELLOW}⚠ If you were added to the docker group, log out and back in.${RESET}"
|
||||||
|
echo ""
|
||||||
|
success "DevLab is ready! Enjoy your lab, Harry."
|
||||||
Reference in New Issue
Block a user