We were deploying manually. SSH into servers, git pull, restart services. It took 45 minutes and failed 30% of the time due to human error.

I set up Jenkins CI/CD pipeline. Now deployments take 8 minutes, fully automated, with automated tests. We deploy 5x more frequently with fewer bugs.

Table of Contents

The Manual Deploy Process

Our painful process:

  1. SSH into 3 production servers
  2. git pull origin master on each
  3. npm install (if dependencies changed)
  4. npm run build
  5. pm2 restart app
  6. Test manually
  7. If broken, rollback (another 20 minutes)

Time: 45 minutes
Success rate: 70%
Deploys per week: 2-3

Installing Jenkins

Downloaded Jenkins 2.7.4:

wget http://mirrors.jenkins.io/war-stable/latest/jenkins.war
java -jar jenkins.war --httpPort=8080

Access at http://localhost:8080

Initial setup:

  1. Get admin password from /var/jenkins_home/secrets/initialAdminPassword
  2. Install suggested plugins
  3. Create admin user

First Pipeline Job

Created “Freestyle project”:

Source Code Management:

  • Git repository: https://github.com/company/web-app.git
  • Branch: */master

Build Triggers:

  • Poll SCM: H/5 * * * * (check every 5 minutes)

Build Steps:

npm install
npm test
npm run build

Post-build Actions:

  • Archive artifacts: dist/**/*

Simple but effective!

Pipeline as Code

Jenkins 2.0 introduced Jenkinsfile. Much better than UI configuration.

Created Jenkinsfile in repo:

node {
    stage('Checkout') {
        checkout scm
    }
    
    stage('Install Dependencies') {
        sh 'npm install'
    }
    
    stage('Test') {
        sh 'npm test'
    }
    
    stage('Build') {
        sh 'npm run build'
    }
    
    stage('Archive') {
        archiveArtifacts artifacts: 'dist/**/*', fingerprint: true
    }
}

Created Pipeline job in Jenkins:

  • Pipeline script from SCM
  • Git repository URL
  • Script path: Jenkinsfile

Now pipeline is version controlled!

Adding Deployment

Extended Jenkinsfile:

node {
    def app
    
    stage('Checkout') {
        checkout scm
    }
    
    stage('Install Dependencies') {
        sh 'npm install'
    }
    
    stage('Test') {
        sh 'npm test'
    }
    
    stage('Build') {
        sh 'npm run build'
    }
    
    stage('Build Docker Image') {
        app = docker.build("web-app:${env.BUILD_NUMBER}")
    }
    
    stage('Push to Registry') {
        docker.withRegistry('https://registry.company.com', 'docker-credentials') {
            app.push("${env.BUILD_NUMBER}")
            app.push("latest")
        }
    }
    
    stage('Deploy to Staging') {
        sh """
            ssh deploy@staging-server '
                docker pull registry.company.com/web-app:${env.BUILD_NUMBER}
                docker stop web-app || true
                docker rm web-app || true
                docker run -d --name web-app -p 80:3000 registry.company.com/web-app:${env.BUILD_NUMBER}
            '
        """
    }
    
    stage('Smoke Test') {
        sh 'curl -f http://staging-server/ || exit 1'
    }
}

Parallel Stages

Run tests in parallel:

stage('Test') {
    parallel(
        'Unit Tests': {
            sh 'npm run test:unit'
        },
        'Integration Tests': {
            sh 'npm run test:integration'
        },
        'Lint': {
            sh 'npm run lint'
        }
    )
}

Saves time!

Manual Approval for Production

Don’t auto-deploy to production:

stage('Deploy to Production') {
    input message: 'Deploy to production?', ok: 'Deploy'
    
    sh """
        ssh deploy@prod-server-1 'docker pull registry.company.com/web-app:${env.BUILD_NUMBER} && docker service update --image registry.company.com/web-app:${env.BUILD_NUMBER} web-app'
        ssh deploy@prod-server-2 'docker pull registry.company.com/web-app:${env.BUILD_NUMBER} && docker service update --image registry.company.com/web-app:${env.BUILD_NUMBER} web-app'
        ssh deploy@prod-server-3 'docker pull registry.company.com/web-app:${env.BUILD_NUMBER} && docker service update --image registry.company.com/web-app:${env.BUILD_NUMBER} web-app'
    """
}

Requires manual click to deploy to production.

Environment Variables

Use different configs per environment:

environment {
    STAGING_SERVER = 'staging.company.com'
    PROD_SERVERS = 'prod1.company.com,prod2.company.com,prod3.company.com'
    DOCKER_REGISTRY = 'registry.company.com'
}

stage('Deploy to Staging') {
    sh """
        ssh deploy@${env.STAGING_SERVER} '
            docker pull ${env.DOCKER_REGISTRY}/web-app:${env.BUILD_NUMBER}
            docker stop web-app || true
            docker rm web-app || true
            docker run -d --name web-app -p 80:3000 ${env.DOCKER_REGISTRY}/web-app:${env.BUILD_NUMBER}
        '
    """
}

Credentials Management

Store secrets in Jenkins:

  1. Manage Jenkins → Manage Credentials
  2. Add credentials (username/password, SSH key, secret text)
  3. Use in pipeline:
stage('Deploy') {
    withCredentials([sshUserPrivateKey(credentialsId: 'deploy-key', keyFileVariable: 'SSH_KEY')]) {
        sh """
            ssh -i ${SSH_KEY} deploy@server 'docker pull ...'
        """
    }
}

Notifications

Slack notifications:

post {
    success {
        slackSend(
            color: 'good',
            message: "Build ${env.BUILD_NUMBER} succeeded: ${env.BUILD_URL}"
        )
    }
    failure {
        slackSend(
            color: 'danger',
            message: "Build ${env.BUILD_NUMBER} failed: ${env.BUILD_URL}"
        )
    }
}

Requires Slack plugin.

Multi-Branch Pipeline

Automatically create pipelines for each branch:

  1. Create “Multibranch Pipeline” job
  2. Configure Git repository
  3. Jenkins scans branches and creates pipeline for each

Now feature branches get their own CI pipeline!

Our Final Pipeline

pipeline {
    agent any
    
    environment {
        DOCKER_REGISTRY = 'registry.company.com'
        APP_NAME = 'web-app'
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        
        stage('Install') {
            steps {
                sh 'npm install'
            }
        }
        
        stage('Test') {
            parallel {
                stage('Unit Tests') {
                    steps {
                        sh 'npm run test:unit'
                    }
                }
                stage('Integration Tests') {
                    steps {
                        sh 'npm run test:integration'
                    }
                }
                stage('Lint') {
                    steps {
                        sh 'npm run lint'
                    }
                }
            }
        }
        
        stage('Build') {
            steps {
                sh 'npm run build'
            }
        }
        
        stage('Docker Build') {
            steps {
                script {
                    docker.build("${env.DOCKER_REGISTRY}/${env.APP_NAME}:${env.BUILD_NUMBER}")
                }
            }
        }
        
        stage('Docker Push') {
            steps {
                script {
                    docker.withRegistry("https://${env.DOCKER_REGISTRY}", 'docker-credentials') {
                        docker.image("${env.DOCKER_REGISTRY}/${env.APP_NAME}:${env.BUILD_NUMBER}").push()
                        docker.image("${env.DOCKER_REGISTRY}/${env.APP_NAME}:${env.BUILD_NUMBER}").push('latest')
                    }
                }
            }
        }
        
        stage('Deploy to Staging') {
            steps {
                sh """
                    ssh deploy@staging 'docker pull ${env.DOCKER_REGISTRY}/${env.APP_NAME}:${env.BUILD_NUMBER} && \
                    docker stop ${env.APP_NAME} || true && \
                    docker rm ${env.APP_NAME} || true && \
                    docker run -d --name ${env.APP_NAME} -p 80:3000 ${env.DOCKER_REGISTRY}/${env.APP_NAME}:${env.BUILD_NUMBER}'
                """
            }
        }
        
        stage('Smoke Test') {
            steps {
                sh 'curl -f http://staging/ || exit 1'
            }
        }
        
        stage('Deploy to Production') {
            when {
                branch 'master'
            }
            steps {
                input message: 'Deploy to production?', ok: 'Deploy'
                
                script {
                    def servers = ['prod1', 'prod2', 'prod3']
                    for (server in servers) {
                        sh """
                            ssh deploy@${server} 'docker pull ${env.DOCKER_REGISTRY}/${env.APP_NAME}:${env.BUILD_NUMBER} && \
                            docker stop ${env.APP_NAME} || true && \
                            docker rm ${env.APP_NAME} || true && \
                            docker run -d --name ${env.APP_NAME} -p 80:3000 ${env.DOCKER_REGISTRY}/${env.APP_NAME}:${env.BUILD_NUMBER}'
                        """
                        sleep 30  // Rolling deployment
                    }
                }
            }
        }
    }
    
    post {
        success {
            slackSend color: 'good', message: "Build ${env.BUILD_NUMBER} succeeded"
        }
        failure {
            slackSend color: 'danger', message: "Build ${env.BUILD_NUMBER} failed"
        }
    }
}

Results

Before Jenkins:

  • Manual deploys: 45 minutes
  • Success rate: 70%
  • Deploys per week: 2-3
  • No automated tests
  • Frequent rollbacks

After Jenkins:

  • Automated deploys: 8 minutes
  • Success rate: 95%
  • Deploys per week: 10-15
  • All tests run automatically
  • Rare rollbacks

Lessons Learned

  1. Start simple - Basic pipeline first, add features later
  2. Pipeline as code - Version control your Jenkinsfile
  3. Automate tests - Catch bugs before production
  4. Manual approval for prod - Don’t auto-deploy to production
  5. Monitor builds - Set up notifications

Conclusion

Jenkins transformed our deployment process. From error-prone manual deploys to reliable automated pipeline.

Key takeaways:

  1. CI/CD saves time and reduces errors
  2. Pipeline as code is essential
  3. Automated tests catch bugs early
  4. Start simple, iterate
  5. Manual approval for production

If you’re still deploying manually, stop. Set up Jenkins today. Your team will thank you.