672 lines
24 KiB
Bash
672 lines
24 KiB
Bash
#!/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." |