Skip to content

Instantly share code, notes, and snippets.

@pyohei
Last active April 5, 2026 04:55
Show Gist options
  • Select an option

  • Save pyohei/06ae8211296767919ab5476b39d28710 to your computer and use it in GitHub Desktop.

Select an option

Save pyohei/06ae8211296767919ab5476b39d28710 to your computer and use it in GitHub Desktop.
bootstrap-rpi.sh
#!/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