Last active
April 5, 2026 04:55
-
-
Save pyohei/06ae8211296767919ab5476b39d28710 to your computer and use it in GitHub Desktop.
bootstrap-rpi.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -Eeuo pipefail | |
| # Raspberry Pi bootstrap for Docker + Git + optional UFW + repo clone + docker compose up | |
| # Target: Raspberry Pi OS / Debian-based systems | |
| # | |
| # Example: | |
| # curl -fsSL https://gist.githubusercontent.com/pyohei/06ae8211296767919ab5476b39d28710/raw/3054634ae21664f0c707ad5eb5ea999bdf8bc2f6/bootstrap-rpi.sh | bash -s -- \ | |
| # --repo https://github.com/pyohei/house-portainer \ | |
| # --branch main \ | |
| # --dir /opt/house-portaine \ | |
| # --compose-path stacks/bootstrap \ | |
| # --ufw | |
| # | |
| # Notes: | |
| # - Run as a sudo-capable user, not necessarily root. | |
| # - The target repo should contain docker-compose.yml / compose.yaml in the compose path. | |
| # - If your compose includes Traefik, this script will launch it too. | |
| # - If --ufw is passed, this script installs/enables UFW and allows 22, 80, 443. | |
| SCRIPT_NAME="$(basename "$0")" | |
| REPO_URL="" | |
| REPO_BRANCH="main" | |
| TARGET_DIR="/opt/rpi-infra" | |
| COMPOSE_PATH="." | |
| ENABLE_UFW="false" | |
| SKIP_UP="false" | |
| GIT_USER_NAME="" | |
| GIT_USER_EMAIL="" | |
| log() { | |
| printf '\n[%s] %s\n' "$SCRIPT_NAME" "$*" | |
| } | |
| die() { | |
| printf '\n[%s] ERROR: %s\n' "$SCRIPT_NAME" "$*" >&2 | |
| exit 1 | |
| } | |
| usage() { | |
| cat <<'EOF' | |
| Usage: | |
| bootstrap-rpi.sh --repo <git_repo_url> [options] | |
| Required: | |
| --repo <url> Git repository URL to clone | |
| Optional: | |
| --branch <name> Git branch to checkout (default: main) | |
| --dir <path> Clone target directory (default: /opt/rpi-infra) | |
| --compose-path <path> Relative path inside repo containing compose file (default: .) | |
| --ufw Install/enable UFW and allow 22,80,443 | |
| --skip-up Do not run docker compose up -d | |
| --git-user-name <name> Configure global git user.name if not set | |
| --git-user-email <email> Configure global git user.email if not set | |
| -h, --help Show this help | |
| Examples: | |
| bootstrap-rpi.sh \ | |
| --repo https://github.com/yourname/rpi-infra.git \ | |
| --branch main \ | |
| --dir /opt/rpi-infra \ | |
| --compose-path stacks/bootstrap \ | |
| --ufw | |
| EOF | |
| } | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --repo) | |
| REPO_URL="${2:-}"; shift 2 ;; | |
| --branch) | |
| REPO_BRANCH="${2:-}"; shift 2 ;; | |
| --dir) | |
| TARGET_DIR="${2:-}"; shift 2 ;; | |
| --compose-path) | |
| COMPOSE_PATH="${2:-}"; shift 2 ;; | |
| --ufw) | |
| ENABLE_UFW="true"; shift ;; | |
| --skip-up) | |
| SKIP_UP="true"; shift ;; | |
| --git-user-name) | |
| GIT_USER_NAME="${2:-}"; shift 2 ;; | |
| --git-user-email) | |
| GIT_USER_EMAIL="${2:-}"; shift 2 ;; | |
| -h|--help) | |
| usage; exit 0 ;; | |
| *) | |
| die "Unknown argument: $1" ;; | |
| esac | |
| done | |
| [[ -n "$REPO_URL" ]] || die "--repo is required" | |
| if [[ $EUID -eq 0 ]]; then | |
| die "Run this script as a sudo-capable user, not root." | |
| fi | |
| require_sudo() { | |
| sudo -v || die "sudo is required" | |
| } | |
| apt_install_if_missing() { | |
| local pkgs_to_install=() | |
| for pkg in "$@"; do | |
| if ! dpkg -s "$pkg" >/dev/null 2>&1; then | |
| pkgs_to_install+=("$pkg") | |
| fi | |
| done | |
| if [[ ${#pkgs_to_install[@]} -gt 0 ]]; then | |
| log "Installing packages: ${pkgs_to_install[*]}" | |
| sudo apt-get update -y | |
| sudo apt-get install -y "${pkgs_to_install[@]}" | |
| else | |
| log "Packages already installed: $*" | |
| fi | |
| } | |
| install_git() { | |
| if command -v git >/dev/null 2>&1; then | |
| log "Git already installed: $(git --version)" | |
| else | |
| apt_install_if_missing git | |
| log "Git installed: $(git --version)" | |
| fi | |
| if [[ -n "$GIT_USER_NAME" ]] && ! git config --global user.name >/dev/null; then | |
| git config --global user.name "$GIT_USER_NAME" | |
| log "Configured git user.name" | |
| fi | |
| if [[ -n "$GIT_USER_EMAIL" ]] && ! git config --global user.email >/dev/null; then | |
| git config --global user.email "$GIT_USER_EMAIL" | |
| log "Configured git user.email" | |
| fi | |
| } | |
| install_docker() { | |
| if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then | |
| log "Docker and Compose plugin already available" | |
| else | |
| log "Installing Docker Engine from Docker's official apt repository" | |
| apt_install_if_missing ca-certificates curl gnupg | |
| sudo install -m 0755 -d /etc/apt/keyrings | |
| if [[ ! -f /etc/apt/keyrings/docker.asc ]]; then | |
| curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg.tmp | |
| sudo mv /etc/apt/keyrings/docker.gpg.tmp /etc/apt/keyrings/docker.gpg | |
| sudo chmod a+r /etc/apt/keyrings/docker.gpg | |
| fi | |
| local arch codename repo_line list_file | |
| arch="$(dpkg --print-architecture)" | |
| codename="$(. /etc/os-release && echo "${VERSION_CODENAME:-}")" | |
| [[ -n "$codename" ]] || die "Could not determine OS codename" | |
| list_file="/etc/apt/sources.list.d/docker.list" | |
| repo_line="deb [arch=${arch} signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian ${codename} stable" | |
| if [[ ! -f "$list_file" ]] || ! grep -Fq "$repo_line" "$list_file"; then | |
| echo "$repo_line" | sudo tee "$list_file" >/dev/null | |
| fi | |
| sudo apt-get update -y | |
| sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin | |
| fi | |
| sudo systemctl enable docker | |
| sudo systemctl start docker | |
| if ! getent group docker >/dev/null; then | |
| sudo groupadd docker | |
| fi | |
| if ! id -nG "$USER" | grep -qw docker; then | |
| sudo usermod -aG docker "$USER" | |
| log "Added $USER to docker group (new login session may be required later)" | |
| fi | |
| sudo docker version >/dev/null | |
| sudo docker compose version >/dev/null | |
| log "Docker ready" | |
| } | |
| setup_ufw() { | |
| [[ "$ENABLE_UFW" == "true" ]] || return 0 | |
| apt_install_if_missing ufw | |
| # Idempotent rules | |
| sudo ufw allow 22/tcp >/dev/null || true | |
| sudo ufw allow 80/tcp >/dev/null || true | |
| sudo ufw allow 443/tcp >/dev/null || true | |
| local status | |
| status="$(sudo ufw status | head -n 1 || true)" | |
| if [[ "$status" == "Status: inactive" ]]; then | |
| log "Enabling UFW" | |
| sudo ufw --force enable | |
| else | |
| log "UFW already enabled" | |
| fi | |
| } | |
| clone_or_update_repo() { | |
| local parent_dir | |
| parent_dir="$(dirname "$TARGET_DIR")" | |
| sudo mkdir -p "$parent_dir" | |
| sudo chown "$USER":"$USER" "$parent_dir" | |
| if [[ -d "$TARGET_DIR/.git" ]]; then | |
| log "Repo already exists, updating: $TARGET_DIR" | |
| git -C "$TARGET_DIR" fetch --all --prune | |
| git -C "$TARGET_DIR" checkout "$REPO_BRANCH" | |
| git -C "$TARGET_DIR" pull --ff-only origin "$REPO_BRANCH" | |
| else | |
| log "Cloning repo into: $TARGET_DIR" | |
| git clone --branch "$REPO_BRANCH" --single-branch "$REPO_URL" "$TARGET_DIR" | |
| fi | |
| } | |
| compose_up() { | |
| local workdir compose_file="" | |
| workdir="${TARGET_DIR%/}/${COMPOSE_PATH#./}" | |
| [[ -d "$workdir" ]] || die "Compose path does not exist: $workdir" | |
| if [[ -f "$workdir/compose.yaml" ]]; then | |
| compose_file="$workdir/compose.yaml" | |
| elif [[ -f "$workdir/docker-compose.yml" ]]; then | |
| compose_file="$workdir/docker-compose.yml" | |
| elif [[ -f "$workdir/docker-compose.yaml" ]]; then | |
| compose_file="$workdir/docker-compose.yaml" | |
| else | |
| die "No compose file found in $workdir" | |
| fi | |
| log "Using compose file: $compose_file" | |
| if [[ "$SKIP_UP" == "true" ]]; then | |
| log "--skip-up specified, not starting containers" | |
| return 0 | |
| fi | |
| log "Pulling images (best effort)" | |
| sudo docker compose -f "$compose_file" pull || true | |
| log "Starting stack" | |
| sudo docker compose -f "$compose_file" up -d | |
| log "Current containers:" | |
| sudo docker compose -f "$compose_file" ps || true | |
| } | |
| post_notes() { | |
| cat <<EOF | |
| Done. | |
| Repo: $REPO_URL | |
| Branch: $REPO_BRANCH | |
| Target dir: $TARGET_DIR | |
| Compose path: $COMPOSE_PATH | |
| Notes: | |
| - If your compose includes Portainer without Traefik, Portainer typically uses: | |
| https://<rpi-host-or-ip>:9443 | |
| - If your compose includes Traefik, access depends on your Traefik labels / DNS. | |
| - If you were just added to the docker group, a re-login may be needed before plain 'docker' works without sudo. | |
| Useful commands: | |
| cd "$TARGET_DIR" | |
| git pull | |
| sudo docker compose -f "${TARGET_DIR%/}/${COMPOSE_PATH#./}/$( [[ -f "${TARGET_DIR%/}/${COMPOSE_PATH#./}/compose.yaml" ]] && echo compose.yaml || ([[ -f "${TARGET_DIR%/}/${COMPOSE_PATH#./}/docker-compose.yml" ]] && echo docker-compose.yml || echo docker-compose.yaml) )" ps | |
| EOF | |
| } | |
| main() { | |
| require_sudo | |
| install_git | |
| install_docker | |
| setup_ufw | |
| clone_or_update_repo | |
| compose_up | |
| post_notes | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment