Skip to content

Instantly share code, notes, and snippets.

@tosin2013
Created April 7, 2026 15:57
Show Gist options
  • Select an option

  • Save tosin2013/67025b9020f0a7dfcf13d471da9230da to your computer and use it in GitHub Desktop.

Select an option

Save tosin2013/67025b9020f0a7dfcf13d471da9230da to your computer and use it in GitHub Desktop.
setup-dev-ubuntu.sh
#!/usr/bin/env bash
set -Eeuo pipefail
# Ubuntu developer workstation bootstrap
# Installs:
# - Node.js 20
# - Docker Engine + compose/buildx plugins
# - Git
# - Common developer tools
log() {
echo ""
echo "==> $*"
}
fail() {
echo "ERROR: $*" >&2
exit 1
}
require_root() {
if [[ "${EUID}" -ne 0 ]]; then
fail "Run this script with sudo or as root."
fi
}
detect_user() {
if [[ -n "${SUDO_USER:-}" && "${SUDO_USER}" != "root" ]]; then
TARGET_USER="${SUDO_USER}"
else
TARGET_USER="$(logname 2>/dev/null || true)"
TARGET_USER="${TARGET_USER:-root}"
fi
}
check_ubuntu() {
if [[ ! -f /etc/os-release ]]; then
fail "/etc/os-release not found. This does not appear to be Ubuntu."
fi
. /etc/os-release
if [[ "${ID:-}" != "ubuntu" ]]; then
fail "This script is intended for Ubuntu only. Detected ID=${ID:-unknown}"
fi
ARCH="$(dpkg --print-architecture)"
UBUNTU_CODENAME="${UBUNTU_CODENAME:-${VERSION_CODENAME:-}}"
if [[ -z "${UBUNTU_CODENAME}" ]]; then
fail "Could not determine Ubuntu codename."
fi
log "Detected Ubuntu ${VERSION_ID:-unknown} (${UBUNTU_CODENAME}) on ${ARCH}"
}
apt_update_upgrade() {
log "Updating apt metadata"
apt-get update -y
log "Upgrading installed packages"
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y
}
install_base_packages() {
log "Installing base packages"
DEBIAN_FRONTEND=noninteractive apt-get install -y \
ca-certificates \
curl \
wget \
gnupg \
lsb-release \
apt-transport-https \
software-properties-common \
git \
unzip \
zip \
jq \
build-essential \
make \
gcc \
g++ \
pkg-config \
python3 \
python3-pip \
pipx \
vim \
nano \
tmux \
htop \
tree \
ripgrep \
fd-find \
xclip \
openssl \
net-tools \
dnsutils \
iputils-ping \
telnet \
nmap \
rsync \
shellcheck \
bash-completion
}
cleanup_old_docker_packages() {
log "Removing conflicting Docker packages if present"
apt-get remove -y docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc || true
}
install_docker_repo() {
log "Configuring Docker official apt repository"
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
cat >/etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: ${UBUNTU_CODENAME}
Components: stable
Architectures: ${ARCH}
Signed-By: /etc/apt/keyrings/docker.asc
EOF
apt-get update -y
}
install_docker() {
log "Installing Docker Engine and plugins"
DEBIAN_FRONTEND=noninteractive apt-get install -y \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin
systemctl enable docker
systemctl start docker
}
configure_docker_group() {
log "Configuring docker group for user: ${TARGET_USER}"
groupadd docker 2>/dev/null || true
usermod -aG docker "${TARGET_USER}" || true
}
cleanup_old_nodesource_entries() {
log "Cleaning up old NodeSource entries if they exist"
rm -f /etc/apt/sources.list.d/nodesource.list
rm -f /etc/apt/sources.list.d/nodesource.sources
rm -f /usr/share/keyrings/nodesource.gpg
rm -f /etc/apt/keyrings/nodesource.gpg
}
install_nodejs_20() {
log "Installing Node.js 20 from NodeSource"
install -m 0755 -d /etc/apt/keyrings
# Use NodeSource setup script for Node 20 repo configuration
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs
}
install_global_npm_tools() {
log "Installing useful global npm tools"
npm install -g npm@latest
npm install -g \
yarn \
pnpm \
typescript \
ts-node \
eslint \
prettier \
nodemon \
serve
}
install_optional_cli_tools() {
log "Installing optional CLI tools from apt where available"
DEBIAN_FRONTEND=noninteractive apt-get install -y \
gh || true
# pipx is already installed above; ensure path is configured for the target user
if [[ "${TARGET_USER}" != "root" ]]; then
sudo -u "${TARGET_USER}" env PATH="/usr/bin:/usr/local/bin:${PATH}" pipx ensurepath || true
fi
}
verify_install() {
log "Verifying installations"
echo "Git version: $(git --version || true)"
echo "Node version: $(node --version || true)"
echo "npm version: $(npm --version || true)"
echo "Docker version: $(docker --version || true)"
echo "Buildx version: $(docker buildx version || true)"
echo "Compose version: $(docker compose version || true)"
echo "Python version: $(python3 --version || true)"
echo "pipx version: $(pipx --version || true)"
}
post_notes() {
cat <<EOF
Bootstrap complete.
Important:
- Log out and back in, or run: newgrp docker
so your '${TARGET_USER}' account can use Docker without sudo.
- Test Docker with:
docker run hello-world
Installed tool categories:
- Core dev: git, curl, wget, jq, build-essential, vim, tmux
- Node.js: node, npm, yarn, pnpm, typescript, eslint, prettier
- Containers: docker-ce, docker compose plugin, buildx
- Utilities: ripgrep, fd-find, tree, htop, xclip, rsync, shellcheck
EOF
}
main() {
require_root
detect_user
check_ubuntu
apt_update_upgrade
install_base_packages
cleanup_old_docker_packages
install_docker_repo
install_docker
configure_docker_group
cleanup_old_nodesource_entries
install_nodejs_20
install_global_npm_tools
install_optional_cli_tools
verify_install
post_notes
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment