DEV Community

Ursula Okafo
Ursula Okafo

Posted on

Building a Production-Grade Automated Deployment Script

Introduction

Automation is the backbone of modern DevOps. In this comprehensive guide, I'll walk you through building a production-grade Bash script that automates the entire deployment process for a Dockerized application on a remote Linux server. This is my journey through the HNG DevOps Stage 1 task.

What We Built

A single Bash script (deploy.sh) that handles:

  • Git repository authentication and cloning
  • Remote server SSH connectivity
  • Automated Docker, Docker Compose, and Nginx installation
  • Docker image building and container deployment
  • Nginx reverse proxy configuration
  • Comprehensive logging and error handling
  • Deployment validation

Why Automation Matters

Manual deployments are error-prone, time-consuming, and don't scale. By automating the entire process, we achieve:

  • Consistency: Same process every time
  • Speed: Deploy in minutes, not hours
  • Reliability: Fewer human errors
  • Repeatability: Deploy multiple times safely
  • Documentation: The script IS the deployment process

Architecture Overview

┌─────────────────────────────────────────────────────────┐
│                    Local Machine                        │
│  ┌──────────────────────────────────────────────────┐  │
│  │  deploy.sh (Deployment Script)                   │  │
│  │  - Collects parameters                           │  │
│  │  - Clones Git repo                               │  │
│  │  - SSH into remote server                        │  │
│  └──────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘
                          │
                          │ SSH
                          ▼
┌─────────────────────────────────────────────────────────┐
│              Remote Server (EC2/VPS)                    │
│  ┌──────────────────────────────────────────────────┐  │
│  │  Step 1: Install Docker & Nginx                  │  │
│  │  Step 2: Clone application repo                  │  │
│  │  Step 3: Build Docker image                      │  │
│  │  Step 4: Run container                           │  │
│  │  Step 5: Configure Nginx proxy                   │  │
│  └──────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

The 8-Step Deployment Process

Step 1: Collect Parameters

The script prompts for all necessary information:

  • Git repository URL
  • Personal Access Token (for authentication)
  • Git branch to deploy
  • SSH credentials (username, IP, key path)
  • Application port

This interactive approach makes the script reusable and flexible.

Enter Git Repository URL: https://github.com/yourname/your-app
Enter Personal Access Token (PAT): ••••••••••
Enter Branch name (default: main): main
Enter SSH Username: ubuntu
Enter Server IP Address: 54.123.45.67
Enter SSH Key Path: ~/.ssh/id_rsa
Enter Application Port: 3000
Enter fullscreen mode Exit fullscreen mode

Step 2: Clone the Repository

The script authenticates using the PAT and clones the repository with the specified branch. This ensures we have the exact code version to deploy.

Step 3: Verify Docker Configuration

Before proceeding, the script verifies that either a Dockerfile or docker-compose.yml exists. This validation prevents failed deployments early.

Step 4: Test SSH Connection

A connectivity check ensures the remote server is reachable before we start any remote operations. This saves time if there's a network issue.

Step 5: Prepare Remote Environment

The script SSH's into the server and:

  • Updates system packages
  • Installs Docker and Docker Compose
  • Installs Nginx
  • Adds the user to the Docker group
  • Enables and starts all services

All operations are idempotent—if Docker is already installed, the script skips it.

Step 6: Deploy the Application

The script:

  • Transfers project files to the remote server
  • Builds a Docker image
  • Stops any old containers
  • Launches a new container with the specified port

Step 7: Configure Nginx

A reverse proxy configuration is created that forwards HTTP traffic (port 80) to the Docker container's internal port. This allows external users to access your app.

server {
    listen 80;
    server_name _;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 8: Validate Deployment

The script confirms:

  • Docker service is running
  • Container is active
  • Nginx is running
  • Application is accessible

Key Technical Decisions

Bash Over Other Languages

Why Bash instead of Python or Go?

  • Portability: Works on any Linux/Unix system
  • No dependencies: Pre-installed on all servers
  • Simplicity: Close to shell commands we'd run manually
  • Production-tested: Used by major organizations

Error Handling

The script uses set -euo pipefail to catch errors immediately and trap functions to handle unexpected failures gracefully.

set -euo pipefail
trap 'handle_error $? $LINENO' ERR
Enter fullscreen mode Exit fullscreen mode

Logging

All operations are logged to timestamped files:

./logs/deploy_20251020_180825.log
Enter fullscreen mode Exit fullscreen mode

This provides an audit trail and helps with debugging failed deployments.

Idempotency

The script can be run multiple times safely:

  • It stops and removes old containers before creating new ones
  • Installation commands check if packages exist first
  • Nginx configuration is overwritten, not appended

Lessons Learned

Repository Structure Matters

Keep your deployment scripts alongside your application code in the same repository. This makes it easier for others to deploy your application without hunting for separate tools. A single repository with all necessary deployment files is more maintainable.

Separate Concerns

While the deployment script lives in the app repository, you might keep a separate deployment folder locally for running deployments. This keeps your working directory organized without affecting the repository structure.

Environment Configuration

Store .env files in your repository for non-sensitive defaults, but add them to .gitignore for production deployments. For sensitive data (API keys, database passwords), use GitHub Secrets or environment variable management tools instead of checking them into version control.

Testing Before Deployment

Always test your endpoints after deployment:

curl http://your-server-ip/
curl http://your-server-ip/health
curl http://your-server-ip/api/info
Enter fullscreen mode Exit fullscreen mode

SSH Key Management

Store SSH keys securely and never commit them to repositories. Use appropriate file permissions (chmod 600).

Real-World Considerations

What We Didn't Include (But Should in Production)

  • SSL/TLS Certificates: Use Let's Encrypt with Certbot
  • Health Checks: Kubernetes-style readiness probes
  • Monitoring: Prometheus, Datadog, or similar
  • Scaling: Load balancers for multiple instances
  • Backup Strategy: Database backups before deployments
  • Rollback Plan: Ability to revert to previous versions

Security Improvements

  • Use GitHub Secrets instead of manual PAT entry
  • Implement IP whitelisting for SSH access
  • Use IAM roles instead of long-lived credentials
  • Scan Docker images for vulnerabilities
  • Implement least-privilege Docker containers

CI/CD Integration

This script integrates well with GitHub Actions:

- name: Deploy
  run: |
    chmod +x deploy.sh
    ./deploy.sh
Enter fullscreen mode Exit fullscreen mode

Results

After running the deployment script:

  • Application successfully deployed to EC2
  • Accessible at http://3.78.183.186
  • All endpoints responding correctly
  • Nginx proxying traffic to container
  • Logs show successful deployment

The app returns:

{
  "message": "Deploy Wizard - HNG DevOps Stage 1",
  "status": "running",
  "version": "1.0.0",
  "timestamp": "2025-10-20T17:26:58.469Z"
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Building an automated deployment script teaches you the fundamentals of DevOps:

  • Infrastructure as code
  • Automation principles
  • Server configuration
  • Container orchestration basics
  • Monitoring and validation

This script is a foundation you can build upon. In a real-world scenario, you'd integrate it with CI/CD pipelines, add monitoring, implement rollback strategies, and scale to multiple servers.

The journey from manual deployments to automation is what separates junior developers from seasoned DevOps engineers. Start here, and keep building.

The Implementation

The complete deployment script uses several key patterns that make it production-ready:

Error Handling: The script implements bash error handling with set -euo pipefail and trap functions to catch failures and provide meaningful error messages at each stage.

Logging: All operations are logged to timestamped files, creating an audit trail for debugging and compliance.

Idempotency: The script safely handles re-runs by checking if services exist before installation and cleanly stopping containers before redeployment.

SSH Integration: Rather than requiring multiple manual SSH commands, the script orchestrates the entire remote deployment through a single SSH connection, executing setup and deployment steps sequentially.

Parameter Validation: User inputs are validated immediately (checking SSH keys exist, ports are numbers, URLs have correct format) to fail fast.

View the Full Code

The complete deployment script and all supporting files are available in the repository:

View on GitHub

In the repository you'll find:

  • deploy.sh - The main deployment script
  • Dockerfile - Container configuration
  • package.json - Node.js dependencies
  • index.js - Sample application
  • README.md - Detailed documentation
  • .gitignore - Version control configuration

Feel free to fork, modify, and use this script as a foundation for your own deployments.

Resources


Have you built your own deployment script? Share your experience in the comments below.

Top comments (0)