From 72bcee099d68782d1bf66749296d55b47f434813 Mon Sep 17 00:00:00 2001 From: Alexis Keates Date: Sun, 1 Feb 2026 15:30:57 +0000 Subject: [PATCH] Initial commit: Docker setup script with Docker, Docker Compose, and GitHub CLI installation --- .DS_Store | Bin 0 -> 6148 bytes LICENSE | 21 ++ README.md | 167 +++++++++++++++ docker-setup-script.tar.gz | Bin 0 -> 29230 bytes docker-setup.sh | 409 +++++++++++++++++++++++++++++++++++++ 5 files changed, 597 insertions(+) create mode 100644 .DS_Store create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docker-setup-script.tar.gz create mode 100755 docker-setup.sh diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..2e3e0bedc38e07559c2eb2a468b83400c0abe702 GIT binary patch literal 6148 zcmeHKu}TCn5S`Hz4lKOVGQZ#-gySr%{Q=oK1tGA-VZWW9t?y+6%{y)Ocy#MDUjii7S_)`k_YWKX`;+L|vE`E-+w!mNDKZe|%j%IcV gz<{Or{v@xkkGz^V1$H{(oetDRfV#+}z;7t<4P^5nIRF3v literal 0 HcmV?d00001 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8cbc985 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Alexis Keates + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4418082 --- /dev/null +++ b/README.md @@ -0,0 +1,167 @@ +# Docker Setup Script + +A comprehensive bash script to set up Docker, Docker Compose, and GitHub CLI on Ubuntu/Debian systems with proper permissions and user configuration. + +## Features + +- ✅ System package updates +- ✅ Docker Engine installation (if not present) +- ✅ Docker Compose V2 plugin installation +- ✅ GitHub CLI (gh) installation +- ✅ Docker group configuration with proper permissions +- ✅ `/srv/containers/` directory creation with ACL support +- ✅ User added to docker group for sudo-less operation +- ✅ Comprehensive error handling and status reporting + +## Quick Start + +### One-Line Installation + +```bash +curl -sL https://raw.githubusercontent.com/alexisskeates/docker-setup-script/main/docker-setup.sh | sudo bash +``` + +### Manual Installation + +```bash +# Download the script +curl -sL https://raw.githubusercontent.com/alexisskeates/docker-setup-script/main/docker-setup.sh -o docker-setup.sh + +# Make it executable +chmod +x docker-setup.sh + +# Run it +sudo ./docker-setup.sh +``` + +## What It Does + +### 1. System Updates +- Updates package lists +- Upgrades existing packages +- Reports number of packages updated + +### 2. Docker Installation +- Checks if Docker is installed +- Adds Docker's official GPG key and repository +- Installs Docker Engine, CLI, containerd, and plugins +- Enables Docker service to start on boot +- Verifies Docker daemon is running + +### 3. Docker Group Configuration +- Creates docker group if it doesn't exist +- Adds the current user to the docker group +- Sets up proper permissions for group access + +### 4. Docker Compose +- Checks for Docker Compose V2 plugin +- Installs if missing +- Verifies installation + +### 5. GitHub CLI +- Checks if `gh` is installed +- Adds GitHub CLI repository +- Installs the latest version +- Verifies installation + +### 6. Container Directory Setup +- Creates `/srv/containers/` directory +- Sets ownership to `root:docker` +- Applies `770` permissions (rwxrwx---) +- Configures ACL for inherited permissions +- Ensures all docker group members can access + +### 7. Verification +- Tests Docker access for the user +- Provides summary of all installed components +- Lists next steps for activation + +## Post-Installation + +After running the script, you'll need to activate the docker group membership: + +### Option 1: Re-login (Recommended) +```bash +# Log out and log back in +exit +``` + +### Option 2: Activate in Current Session +```bash +# Start a new shell with docker group active +newgrp docker +``` + +### Verify Installation + +```bash +docker --version +docker compose version +docker ps +gh --version +``` + +## Requirements + +- **OS**: Ubuntu 20.04+ or Debian 10+ +- **Privileges**: Must be run with `sudo` +- **Network**: Internet connection required for package downloads + +## Permissions + +The script creates `/srv/containers/` with the following permissions: + +``` +Owner: root (rwx) +Group: docker (rwx) +Others: --- (no access) +``` + +This allows all members of the docker group to: +- Create containers in `/srv/containers/` +- Manage docker-compose projects +- Share container configurations + +## Troubleshooting + +### "Docker access requires logout/login" +This is normal. The docker group membership requires a new login session. Use `newgrp docker` or log out/in. + +### "Failed to update package lists" +Check your internet connection and ensure apt sources are configured correctly. + +### "Docker daemon is not running" +Try: `sudo systemctl start docker` and check logs with `sudo journalctl -u docker` + +## Security Notes + +- The script must be run with `sudo` privileges +- All docker group members have full access to `/srv/containers/` +- Docker group membership is equivalent to root access +- Only add trusted users to the docker group + +## Directory Structure + +After installation, you'll have: + +``` +/srv/containers/ # Root directory for all containers + ├── (your projects) # Docker Compose projects + └── ... +``` + +## License + +MIT License - See LICENSE file for details + +## Contributing + +Issues and pull requests are welcome! Please feel free to contribute improvements. + +## Author + +Alexis Keates - [GitHub](https://github.com/alexisskeates) + +## Support + +If you encounter any issues, please [open an issue](https://github.com/alexisskeates/docker-setup-script/issues) on GitHub. diff --git a/docker-setup-script.tar.gz b/docker-setup-script.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..6455de09eed83e37d5759334a255b4c36de06261 GIT binary patch literal 29230 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 V!NI}7!NKwE;~%Z?U*Z6e0RRVR+&};T literal 0 HcmV?d00001 diff --git a/docker-setup.sh b/docker-setup.sh new file mode 100755 index 0000000..98f214f --- /dev/null +++ b/docker-setup.sh @@ -0,0 +1,409 @@ +#!/bin/bash + +################################################################################ +# Docker Setup Script +################################################################################ +# +# Description: +# Comprehensive automated setup script for Docker development environments. +# This script prepares Ubuntu/Debian systems with all necessary tools for +# containerized application development and deployment. +# +# What this script does: +# 1. System Updates +# - Updates apt package lists +# - Upgrades all installed packages to latest versions +# - Reports number of packages upgraded +# +# 2. Docker Installation (if not present) +# - Adds Docker's official GPG key and repository +# - Installs Docker Engine, CLI, containerd +# - Installs Docker Buildx and Compose plugins +# - Enables Docker service to start on boot +# - Verifies Docker daemon is running +# +# 3. Docker Group Configuration +# - Creates docker group if it doesn't exist +# - Adds the current user to docker group +# - Enables sudo-less Docker commands (after re-login) +# - Sets up proper group permissions +# +# 4. Docker Compose V2 +# - Checks for Docker Compose plugin +# - Installs docker-compose-plugin if missing +# - Verifies installation and version +# +# 5. GitHub CLI Installation +# - Adds GitHub CLI official repository +# - Installs latest version of 'gh' command +# - Verifies installation +# +# 6. Container Directory Setup +# - Creates /srv/containers/ directory structure +# - Sets ownership to root:docker +# - Applies 770 permissions (rwxrwx---) +# - Configures ACL for inherited permissions +# - Ensures all docker group members can create/manage containers +# +# 7. Verification & Summary +# - Tests Docker access for current user +# - Displays installation summary +# - Provides next steps for activation +# +# Requirements: +# - Ubuntu 20.04+ or Debian 10+ +# - sudo privileges +# - Internet connection +# +# Usage: +# Quick install: +# curl -sL https://raw.githubusercontent.com/alexisskeates/docker-setup-script/main/docker-setup.sh | sudo bash +# +# Manual download and run: +# curl -O https://raw.githubusercontent.com/alexisskeates/docker-setup-script/main/docker-setup.sh +# chmod +x docker-setup.sh +# sudo ./docker-setup.sh +# +# Post-installation: +# After running, you must activate docker group membership: +# Option 1: Log out and log back in (recommended) +# Option 2: Run 'newgrp docker' in current session +# +# Author: Alexis Keates +# Repository: https://github.com/alexisskeates/docker-setup-script +# License: MIT +# +################################################################################ + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_section() { + echo -e "${BLUE}[SECTION]${NC} $1" +} + +# Check if running with elevated privileges +if [[ $EUID -ne 0 ]]; then + print_error "This script must be run with sudo" + print_error "Usage: curl -sL https://raw.githubusercontent.com/alexisskeates/docker-setup-script/main/docker-setup.sh | sudo bash" + exit 1 +fi + +# Get the original user who ran sudo +if [[ -n "$SUDO_USER" ]]; then + ORIGINAL_USER="$SUDO_USER" + ORIGINAL_USER_HOME=$(eval echo "~$SUDO_USER") +else + print_error "Could not determine original user. Please run with sudo, not as root directly." + exit 1 +fi + +print_status "Running as: $(whoami) (original user: $ORIGINAL_USER)" +echo + +# ============================================================================ +# SECTION 1: System Updates +# ============================================================================ +print_section "Checking for system updates..." + +print_status "Updating package lists..." +if apt-get update; then + print_status "Package lists updated successfully" +else + print_error "Failed to update package lists" + exit 1 +fi + +print_status "Checking for upgradeable packages..." +UPGRADABLE=$(apt list --upgradable 2>/dev/null | tail -n +2 | wc -l) + +if [[ $UPGRADABLE -gt 0 ]]; then + print_status "Found $UPGRADABLE upgradeable package(s)" + print_status "Upgrading system packages (this may take a few minutes)..." + if DEBIAN_FRONTEND=noninteractive apt-get upgrade -y; then + print_status "System packages upgraded successfully" + else + print_warning "Some packages failed to upgrade, but continuing..." + fi +else + print_status "System is already up to date" +fi +echo + +# ============================================================================ +# SECTION 2: Docker Installation Check +# ============================================================================ +print_section "Verifying Docker installation..." + +if ! command -v docker > /dev/null 2>&1; then + print_warning "Docker is not installed. Installing Docker..." + + # Install prerequisites + print_status "Installing prerequisites..." + apt-get install -y ca-certificates curl gnupg lsb-release + + # Add Docker's official GPG key + print_status "Adding Docker GPG key..." + install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + chmod a+r /etc/apt/keyrings/docker.gpg + + # Set up the repository + print_status "Adding Docker repository..." + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null + + # Update apt and install Docker + print_status "Installing Docker Engine..." + apt-get update + if apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; then + print_status "Docker installed successfully" + else + print_error "Failed to install Docker" + exit 1 + fi +else + print_status "Docker is already installed ($(docker --version))" +fi + +# Enable and start Docker service +print_status "Ensuring Docker service is enabled and running..." +systemctl enable docker +systemctl start docker + +# Verify Docker daemon is running +if docker ps > /dev/null 2>&1; then + print_status "Docker daemon is running" +else + print_error "Docker daemon is not running properly" + exit 1 +fi +echo + +# ============================================================================ +# SECTION 3: Docker Group Setup +# ============================================================================ +print_section "Setting up Docker group and permissions..." + +# Check if docker group exists +if ! getent group docker > /dev/null 2>&1; then + print_warning "Docker group does not exist. Creating it..." + if groupadd docker; then + print_status "Docker group created successfully" + else + print_error "Failed to create docker group" + exit 1 + fi +else + print_status "Docker group already exists" +fi + +# Check if the original user is in the docker group +if ! groups "$ORIGINAL_USER" | grep -q docker; then + print_status "Adding user $ORIGINAL_USER to docker group..." + if usermod -aG docker "$ORIGINAL_USER"; then + print_status "User added to docker group successfully" + print_warning "Note: User $ORIGINAL_USER will need to log out and back in for group changes to take effect" + print_warning "Or run: newgrp docker" + else + print_error "Failed to add user to docker group" + exit 1 + fi +else + print_status "User $ORIGINAL_USER is already in docker group" +fi +echo + +# ============================================================================ +# SECTION 4: Docker Compose Installation +# ============================================================================ +print_section "Checking Docker Compose installation..." + +# Check if docker compose is available (V2 plugin style) +if docker compose version > /dev/null 2>&1; then + COMPOSE_VERSION=$(docker compose version --short 2>/dev/null || echo "unknown") + print_status "Docker Compose is already installed (version: $COMPOSE_VERSION)" +# Also check for old standalone docker-compose +elif command -v docker-compose > /dev/null 2>&1; then + COMPOSE_VERSION=$(docker-compose version --short 2>/dev/null || echo "unknown") + print_status "Docker Compose (standalone) is installed (version: $COMPOSE_VERSION)" + print_warning "Consider upgrading to Docker Compose V2 (plugin)" +else + print_warning "Docker Compose not found. Installing..." + + # Install Docker Compose plugin (V2) + print_status "Installing docker-compose-plugin..." + if apt-get install -y docker-compose-plugin; then + print_status "Docker Compose plugin installed successfully" + + # Verify installation + if docker compose version > /dev/null 2>&1; then + COMPOSE_VERSION=$(docker compose version --short 2>/dev/null || echo "unknown") + print_status "Docker Compose version $COMPOSE_VERSION is now available" + else + print_error "Docker Compose installation verification failed" + exit 1 + fi + else + print_error "Failed to install Docker Compose plugin" + exit 1 + fi +fi +echo + +# ============================================================================ +# SECTION 5: GitHub CLI Installation +# ============================================================================ +print_section "Checking GitHub CLI installation..." + +if command -v gh > /dev/null 2>&1; then + GH_VERSION=$(gh --version | head -n1 | awk '{print $3}') + print_status "GitHub CLI is already installed (version: $GH_VERSION)" +else + print_warning "GitHub CLI not found. Installing..." + + # Add GitHub CLI repository + print_status "Adding GitHub CLI repository..." + + # Install prerequisites + apt-get install -y curl gpg + + # Add GPG key + curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | gpg --dearmor -o /usr/share/keyrings/githubcli-archive-keyring.gpg + + # Add repository + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null + + # Update and install + print_status "Installing GitHub CLI..." + apt-get update + if apt-get install -y gh; then + GH_VERSION=$(gh --version | head -n1 | awk '{print $3}') + print_status "GitHub CLI version $GH_VERSION installed successfully" + else + print_error "Failed to install GitHub CLI" + exit 1 + fi +fi +echo + +# ============================================================================ +# SECTION 6: Container Directory Setup +# ============================================================================ +print_section "Setting up /srv/containers/ directory..." + +# Create the directory if it doesn't exist +if [[ ! -d "/srv/containers" ]]; then + print_status "Creating directory /srv/containers/" + if mkdir -p /srv/containers; then + print_status "Directory created successfully" + else + print_error "Failed to create directory" + exit 1 + fi +else + print_status "Directory /srv/containers/ already exists" +fi + +# Set ownership to root:docker +print_status "Setting ownership to root:docker" +if chown root:docker /srv/containers; then + print_status "Ownership set successfully" +else + print_error "Failed to set ownership" + exit 1 +fi + +# Set permissions: owner (root) has full access, group (docker) has read/write, others have no access +print_status "Setting permissions (rwxrwx---)" +if chmod 770 /srv/containers; then + print_status "Permissions set successfully" +else + print_error "Failed to set permissions" + exit 1 +fi + +# Install ACL if needed +if ! command -v setfacl > /dev/null 2>&1; then + print_status "Installing ACL package..." + apt-get install -y acl > /dev/null 2>&1 +fi + +# Set default ACL for future files and directories +if command -v setfacl > /dev/null 2>&1; then + print_status "Setting default ACL for future contents" + if setfacl -d -m g:docker:rwx /srv/containers && setfacl -m g:docker:rwx /srv/containers; then + print_status "ACL set successfully" + else + print_warning "Failed to set ACL" + fi +else + print_warning "ACL tools not available. Future files may not inherit proper permissions" +fi +echo + +# ============================================================================ +# SECTION 7: Verification and Summary +# ============================================================================ +print_section "Setup Summary" + +echo "Directory details:" +ls -lad /srv/containers 2>/dev/null || print_warning "Could not list directory details" +echo + +if command -v getfacl > /dev/null 2>&1; then + echo "ACL details:" + getfacl /srv/containers 2>/dev/null || print_warning "Could not display ACL details" + echo +fi + +# Test Docker access for original user +print_status "Testing Docker access for $ORIGINAL_USER..." +if sudo -u "$ORIGINAL_USER" docker ps > /dev/null 2>&1; then + print_status "✓ Docker access working (group change already effective)" +else + print_warning "Docker access requires logout/login or 'newgrp docker'" +fi +echo + +print_status "✓ System packages updated" +print_status "✓ Docker installed and verified" +print_status "✓ Docker Compose installed and verified" +print_status "✓ GitHub CLI installed and verified" +print_status "✓ Docker group configured" +print_status "✓ User $ORIGINAL_USER added to docker group" +print_status "✓ /srv/containers/ directory created with proper permissions" +echo + +# Final instructions +print_section "IMPORTANT: Next Steps" +print_warning "To use Docker without sudo, $ORIGINAL_USER needs the docker group to be active." +print_warning "Since we just added you to the docker group, you need to either:" +echo " 1. Log out and back in to your SSH session, OR" +echo " 2. Run: newgrp docker (in current session)" +echo +print_status "After re-logging or running 'newgrp docker', test with:" +echo " • docker --version" +echo " • docker compose version" +echo " • docker ps" +echo " • gh --version" +echo +print_status "Setup completed successfully at $(date)!"