The Infrastructure: Setting Up Jenkins on AWS
The foundation of this project began by provisioning an Ubuntu EC2 instance on AWS. Setting up the environment meant defining strict networking rules (opening Port 22 for SSH and Port 8080 for the Jenkins UI) and structuring the Jenkins environment with clear access controls.
In Jenkins, maintaining a secure and organized environment generally falls into two roles:
- Administrators: Responsible for managing the Jenkins cluster, installing necessary plugins, and handling data backups.
- Users: Focused purely on creating jobs to run their respective workflows.
The Magic of Docker-out-of-Docker (DooD)
One of the most critical architectural choices was deciding how to let Jenkins build Docker images without installing a heavy, nested Docker engine inside the Jenkins container itself. The solution was a Docker-out-of-Docker configuration.
By running the following command, I spun up the Jenkins container while binding it directly to the host machine's Docker socket:
docker run -p 8080:8080 -p 50000:50000 -d \
-v jenkins_home:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
-v $(which docker):/usr/bin/docker jenkins/jenkins:lts
This single command did a lot of heavy lifting. It mapped port 8080 for the UI and 50000 for Jenkins agent communication. More importantly, mapping /var/run/docker.sock gave the Jenkins container the ability to pass docker build and docker push commands directly to the EC2 hostβs Docker engine. (Just remember to ensure your jenkins user has the right permissions to access that socket!).
Hitting the Wall: The Limitations of Freestyle Jobs
Initially, I set up the application lifecycle running npm install, npm test, and npm pack using a standard Jenkins Freestyle job.
Freestyle jobs are great for quick, isolated tasks. However, their limitations become glaringly obvious when you try to build a project with multiple automation steps. Orchestrating a complex workflow by chaining multiple Freestyle jobs together is inefficient and quickly leads to overloaded jobs that are impossible to maintain or debug.
The Solution: Embracing Declarative Pipelines
To solve this, I migrated the entire workflow to a Pipeline job. Pipelines are written in Groovy (a language similar to Java) and are designed specifically to orchestrate long-running, complex activities that can span multiple build agents.
The repository for this project contains all the necessary configuration files, including the Dockerfile, Jenkinsfile, package.json, and the main application code in server.js.
By placing a Jenkinsfile directly in the repository, the pipeline became part of the application's source code (Infrastructure as Code). The declarative syntax allowed me to clearly define distinct stages:
- Checkout: Pulling the latest code from GitHub.
- Install & Test: Running standard Node.js NPM commands.
-
Package & Build: Compiling the
.tgzartifact and building the Docker image. - Push: Authenticating and pushing the final image to a private DockerHub repository.
Pro-Tip: When pushing to a private registry in a pipeline, you need to use Jenkins' "Secret text or file" plugin to securely pass your credentials into the docker login command via a withCredentials block, preventing sensitive data from leaking into your build logs!
What's Next?
Moving forward, I plan to implement Multibranch Pipelines, which will allow Jenkins to automatically scan my repository, detect any branch containing a Jenkinsfile, and dynamically build a pipeline for it using when condition statements. I am also exploring Jenkins Parameters to inject external configurations, making it incredibly easy to select specific application versions for deployment dynamically.
The transition from clicking through the Jenkins UI to writing robust, version-controlled pipelines has completely transformed how I approach CI/CD.
Top comments (0)