Initial commit

This commit is contained in:
adminharis
2026-06-09 21:38:35 +00:00
commit f80dbe45c0
3 changed files with 873 additions and 0 deletions
+192
View File
@@ -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
+672
View File
@@ -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."
+9
View File
@@ -0,0 +1,9 @@
# Upload the new install-devlab.sh to the server, then:
chmod +x install-devlab.sh
sudo ./install-devlab.sh
# After install:
sudo cp devlab.sh /usr/local/bin/devlab
devlab status