DEV Community

Cover image for Automating Dockerized App Deployment with a Bash Script
Ifeanyi Nworji
Ifeanyi Nworji

Posted on

Automating Dockerized App Deployment with a Bash Script

DevOps is all about automation, reliability, and efficiency. In Stage 1 of my DevOps internship, I built a robust Bash script to automate the deployment of a Dockerized application to a remote Linux server.

Whether you’re a developer, system administrator, or DevOps enthusiast, this guide will walk you step by step through creating a production-ready deployment script that handles everything from cloning a repository to configuring Nginx as a reverse proxy.

Why Automate Deployment?

Manual deployment is error-prone, inconsistent, and time-consuming. Automation provides:

  • Consistency: Ensures deployments are identical every time.
  • Speed: Reduces human intervention and saves time.
  • Reliability: Detects errors early and logs them for troubleshooting.

By the end of this guide, you’ll have a single Bash script (deploy.sh) capable of fully deploying your Dockerized app to a remote server.

Step 1: Collect Parameters from User Input
The first step is interactively collecting essential details:

  • Git repository URL
  • Personal Access Token (PAT)
  • Branch name (defaults to main)
  • SSH credentials (username, server IP, key path)
  • Application port

We also validate the inputs to prevent script failure later. Example snippet:

# Git Repository URL
while [[ -z "${GIT_REPO_URL:-}" ]]; do
    read -rp "Enter the git repository URL: " GIT_REPO_URL
    [[ -z "$GIT_REPO_URL" ]] && log_error "Repository URL cannot be empty."
done

# Personal Access Token (PAT) - hidden input
while [[ -z "${GIT_PAT:-}" ]]; do
  read -rsp "Enter your Git Personal Access Token (PAT): " GIT_PAT
  echo ""
  [[ -z "$GIT_PAT" ]] && log_error "Personal Access Token cannot be empty."
done

# Branch name (default = main)
read -rp "Enter branch name [default: main]: " GIT_BRANCH
GIT_BRANCH=${GIT_BRANCH:-main}

#SSH username
while [[ -z "${SSH_USER:-}" ]]; do
    read -rp "Enter remote server SSH username: " SSH_USER
    [[ -z "$SSH_USER" ]] && log_error "SSH username cannot be empty."
done

#Server IP address
while [[ -z "${SERVER_IP:-}" ]]; do
    read -rp "Enter remote server IP address: " SERVER_IP
    if [[ ! "$SERVER_IP" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
        log_error "Invalid IP format. Please enter a valid IPV4 address."
        SERVER_IP=""
    fi
done

# SSH key path
while [[ -z "${SSH_KEY_PATH:-}" ]]; do
    read -rp "Enter path to SSH private key: " SSH_KEY_PATH
    if [[ ! -f "$SSH_KEY_PATH" ]]; then
        log_error "SSH key file not found at $SSH_KEY_PATH"
        SSH_KEY_PATH=""
    fi
done

# Application port (internal container port)
while [[ -z "${APP_PORT:-}" ]]; do
    read -rp "Enter internal application (container) port (e.g., 8080): " APP_PORT
    if ! [[ "$APP_PORT" =~ ^[0-9]+$ ]]; then
        log_error "Invalid port. Please enter a numeric value."
        APP_PORT=""
    fi
done
Enter fullscreen mode Exit fullscreen mode

Tip: Always validate user input using conditionals to ensure URLs are valid and files exist.

Step 2: Clone or Pull the Repository
Next, we authenticate using the PAT and clone the repo, or pull the latest changes if it already exists:

REPO_NAME=$(basename -s .git "$GIT_REPO_URL")
WORK_DIR="$HOME/deployment/$REPO_NAME"

# Create deployment directory if not exists
mkdir -p "$(dirname "$WORK_DIR")"

# Authenticated URL (PAT safely embedded)
GIT_USERNAME=$(echo "$GIT_REPO_URL" | awk -F[/:] '{print $(NF-1)}')
AUTH_REPO_URL=$(echo "$GIT_REPO_URL" | sed "s#https://#https://$GIT_USERNAME:$GIT_PAT@#")
if [[ -d "$WORK_DIR/.git" ]]; then
    log_info " Repository already exists. Pulling latest changes..."
    cd "$WORK_DIR"
    git reset --hard
    git clean -fd
    git fetch origin "$GIT_BRANCH"
    git checkout "$GIT_BRANCH"
    git pull origin "$GIT_BRANCH" || {
        log_error " Failed to pull latest changes from $GIT_BRANCH"
        exit $EXIT_UNKNOWN
    }
else
    log_info " Cloning repository into $WORK_DIR..."
    git clone --branch "$GIT_BRANCH" "$AUTH_REPO_URL" "$WORK_DIR" || {
        log_error " Failed to clone repository. Please check your URL or PAT"
        exit $EXIT_UNKNOWN
    }
    cd "$WORK_DIR"
fi

log_success " Repository is ready at: $WORK_DIR"
Enter fullscreen mode Exit fullscreen mode

This step ensures your local project folder is always up-to-date with the remote repo.

Step 3: Navigate into the Project Directory
After cloning, we enter the project folder and validate the Docker setup:

# Check for Docker configuration  files
if [[ -f "Dockerfile" ]]; then
    log_success " Found Dockerfile - ready for Docker build."
elif [[ -f "compose.yaml" || -f "compose.yml" || -f "docker-compose.yaml" || -f "docker-compose.yml" ]]; then
    log_success " Found docker-compose.yml - ready for multi-service deployment"
else
    log_error " No Dockerfile or docker-compose.yml found. Cannot continue deployment."
    exit $EXIT_UNKNOWN
fi
Enter fullscreen mode Exit fullscreen mode

Step 4: SSH into the Remote Server
Using SSH, we perform connectivity checks and prepare to execute remote commands:

# Validate SSH key exists
if [ ! -f "$SSH_KEY_PATH" ]; then
    log_error "SSH key not found at: $SSH_KEY_PATH"
    exit $EXIT_SSH_FAIL
fi

# Test SSH connection (non-interactive)
ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no -o BatchMode=yes "$SSH_USER@$SERVER_IP" "echo 'SSH connection successful!'" >/dev/null 2>&1
if [ $? -ne 0 ]; then
    log_error "Unable to connect to remote server via SSH. Please verify credentials, key permissions, and IP address."
    exit $EXIT_SSH_FAIL
else
    log_success "SSH connection verified successfully."
fi
Enter fullscreen mode Exit fullscreen mode

Step 5: Prepare the Remote Environment
On the remote server, we need to ensure all dependencies exist:

  1. Update packages
  2. Install Docker, Docker Compose, and Nginx
  3. Add user to Docker group
  4. Enable and start services
ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no "$SSH_USER@$SERVER_IP" bash <<EOF
set -e

echo "Updating system packages..."
sudo apt-get update -y && sudo apt-get upgrade -y

echo "Installing required packages (curl, ca-certificates, gnupg, lsb-release)..."
sudo apt-get install -y ca-certificates curl gnupg lsb-release

# --- Install Docker if not installed ---
if ! command -v docker &>/dev/null; then
    echo "Docker not found. Installing Docker..."
    curl -fsSL https://get.docker.com | sudo bash
else
    echo "Docker already installed."
fi

# --- Install Docker Compose if not installed ---
if ! command -v docker-compose &>/dev/null; then
    echo "Installing Docker Compose..."
    sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-\$(uname -s)-\$(uname -m)" -o /usr/local/bin/docker-compose
    sudo chmod +x /usr/local/bin/docker-compose
else
    echo "Docker Compose already installed."
fi

# --- Install nginx if not installed ---
if ! command -v nginx &>/dev/null; then
    echo "Installing nginx..."
    sudo apt-get install -y nginx
else
    echo "nginx already installed."
fi

# --- Add SSH user to docker group ---
if ! groups $SSH_USER | grep -q docker; then
    echo "Adding user '$SSH_USER' to docker group..."
    sudo usermod -aG docker $SSH_USER
    echo "You may need to log out and back in for this to take effect."
else
    echo "User '$SSH_USER' already in docker group."
fi

# --- Enable and start services ---
sudo systemctl enable docker
sudo systemctl start docker
sudo systemctl enable nginx
sudo systemctl start nginx

# --- Confirm installation versions ---
echo "Confirming versions..."
docker --version
docker-compose --version
nginx -v

echo "Remote environment setup complete."
EOF
Enter fullscreen mode Exit fullscreen mode

Best practice: Always confirm installation versions and service status:

docker --version
docker-compose --version
nginx -v
Enter fullscreen mode Exit fullscreen mode

Step 6: Deploy the Dockerized Application
We transfer project files to the server and build/run Docker containers:

rsync -avz -e "ssh -i $SSH_KEY_PATH -o StrictHostKeyChecking=no" \
    --exclude '.git' --exclude 'node_modules' --exclude '.env' \
    "$CLONE_DIR/" "$SSH_USER@$SERVER_IP:$REMOTE_APP_DIR" >/dev/null 2>&1
Enter fullscreen mode Exit fullscreen mode
ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no "$SSH_USER@$SERVER_IP" bash <<EOF
set -e

cd $REMOTE_APP_DIR

# --- Detect Docker configuration ---
if [[ -f "docker-compose.yml" ]]; then
    echo "docker-compose.yml found. Starting with Docker Compose..."
    sudo docker-compose pull
    sudo docker-compose build
    sudo docker-compose up -d
elif [[ -f "Dockerfile" ]]; then
    echo "Dockerfile found. Building and running manually..."
    APP_NAME=\$(basename \$(pwd))
    sudo docker build -t \$APP_NAME .
    sudo docker run -d -p $APP_PORT:$APP_PORT --name \$APP_NAME \$APP_NAME
else
    echo "No Dockerfile or docker-compose.yml found in project directory."
    exit 1
fi

# --- Step 3: Validate container health ---
echo "Checking running containers..."
sudo docker ps

# --- Step 4: Verify app is accessible on the specified port ---
echo "Validating application accessibility on port $APP_PORT..."
sleep 5
if curl -s "http://localhost:$APP_PORT" >/dev/null; then
    echo "Application is running and accessible on port $APP_PORT!"
else
    echo "Application did not respond on port $APP_PORT. Check container logs."
    sudo docker logs \$(sudo docker ps -q --latest)
fi
EOF
Enter fullscreen mode Exit fullscreen mode

This ensures the container is up, healthy, and accessible on the specified port.

Step 7: Configure Nginx as a Reverse Proxy
Nginx forwards traffic from port 80 to your Docker container:

DOMAIN_NAME="${DOMAIN_NAME:-example.com}"

# Create a temporary nginx config locally with correct escaping for nginx $-vars
TMP_NGINX_CONF="$(mktemp /tmp/app_proxy.XXXXXX.conf)"

cat > "$TMP_NGINX_CONF" <<EOF
server {
    listen 80;
    server_name ${DOMAIN_NAME};

    location / {
        proxy_pass http://127.0.0.1:${APP_PORT};
        proxy_http_version 1.1;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
        proxy_cache_bypass \$http_upgrade;
    }

    access_log /var/log/nginx/app_access.log;
    error_log /var/log/nginx/app_error.log;
}

# SSL placeholder (Certbot or self-signed)
# server {
#     listen 443 ssl;
#     server_name ${DOMAIN_NAME};
#     ssl_certificate /etc/ssl/certs/app.crt;
#     ssl_certificate_key /etc/ssl/private/app.key;
#     location / {
#         proxy_pass http://127.0.0.1:${APP_PORT};
#         proxy_set_header Host \$host;
#     }
# }
EOF
Enter fullscreen mode Exit fullscreen mode
NGINX_CONFIG_PATH="/etc/nginx/sites-available/app_proxy"
NGINX_ENABLED_PATH="/etc/nginx/sites-enabled/app_proxy"

# Copy config to remote /tmp then move with sudo to proper location to avoid permission issues
scp -i "$SSH_KEY_PATH" "$TMP_NGINX_CONF" "$SSH_USER@$SERVER_IP:/tmp/app_proxy.conf" >/dev/null 2>&1 || {
  log_error "Failed to upload nginx config to remote host."
  rm -f "$TMP_NGINX_CONF"
  exit $EXIT_NGINX_FAIL
}

rm -f "$TMP_NGINX_CONF"

# Apply config remotely: install nginx if missing, remove default, move config, enable, test, reload
ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no "$SSH_USER@$SERVER_IP" bash -s <<'REMOTE_EOF'
set -e

NGINX_CONFIG_PATH="/etc/nginx/sites-available/app_proxy"
NGINX_ENABLED_PATH="/etc/nginx/sites-enabled/app_proxy"

# Install nginx if missing
if ! command -v nginx &>/dev/null; then
  echo "Installing nginx..."
  sudo apt-get update -y
  sudo apt-get install -y nginx
fi

# Remove default site if present
if [ -f /etc/nginx/sites-enabled/default ]; then
  echo "Removing default nginx site..."
  sudo rm -f /etc/nginx/sites-enabled/default
fi

# Move uploaded config into place
sudo mv /tmp/app_proxy.conf "$NGINX_CONFIG_PATH"
sudo chown root:root "$NGINX_CONFIG_PATH"
sudo chmod 644 "$NGINX_CONFIG_PATH"

# Enable site (idempotent)
sudo ln -sf "$NGINX_CONFIG_PATH" "$NGINX_ENABLED_PATH"

# Test and reload
if sudo nginx -t; then
  echo "nginx config OK - reloading"
  sudo systemctl reload nginx
else
  echo "nginx config test FAILED"
  sudo nginx -t || true
  exit 1
fi

echo "Nginx reverse proxy configured"
REMOTE_EOF
Enter fullscreen mode Exit fullscreen mode

Step 8: Validate Deployment
Check that everything is running and accessible:

ssh -i $SSH_KEY $REMOTE_USER@$REMOTE_IP "docker ps"
curl http://$REMOTE_IP
Enter fullscreen mode Exit fullscreen mode

All services should respond correctly.

Step 9: Logging and Error Handling
Logging is critical for troubleshooting. Our script:

  • Saves logs to deploy_YYYYMMDD.log
  • Uses trap to catch errors
  • Exits with meaningful codes
LOG_FILE="$LOG_DIR/deploy_$(date +'%Y%m%d_%H%M%S').log"

# --- Logging functions ---
log_info()    { echo -e "\033[1;34m[INFO]\033[0m $1"    | tee -a "$LOG_FILE"; }
log_success() { echo -e "\033[1;32m[SUCCESS]\033[0m $1" | tee -a "$LOG_FILE"; }
log_warn()    { echo -e "\033[1;33m[WARN]\033[0m $1"    | tee -a "$LOG_FILE"; }
log_error()   { echo -e "\033[1;31m[ERROR]\033[0m $1"   | tee -a "$LOG_FILE"; }

# --- Error trapping ---
trap 'handle_error $? $LINENO' ERR
handle_error() {
    local exit_code=$1
    local line_no=$2
    log_error " Script failed at line $line_no (exit code: $exit_code)"
    log_error " Deployment aborted. Check $LOG_FILE for details."
    exit "$exit_code"
}
Enter fullscreen mode Exit fullscreen mode

Step 10: Idempotency and Cleanup
The script is safe to rerun. Previous containers are stopped/removed before redeployment. Optional --cleanup removes all deployed resources.

Conclusion

By building this Automated Deployment Bash Script, I’ve learned how to:

  • Interact with users in Bash scripts
  • Automate Docker deployment
  • Configure remote servers and Nginx
  • Implement robust logging, error handling, and idempotency

This project mirrors real-world DevOps workflows and is a great starting point for anyone looking to build production-grade deployment scripts.
Complete repo link

Top comments (0)