Initial commit
This commit is contained in:
@@ -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