This project demonstrates a dockerized CI/CD pipeline using Jenkins to lint, test, build, and deploy a Streamlit app in Docker containers hosted on an EC2 instance. It includes GitHub integration, email notifications, and containerized deployment via Docker Compose.
simple-jenkins-dockerized/
โ
โโโ streamlit_app/
โ โโโ app.py
โ โโโ requirements.txt
โ โโโ test_app.py
โ โโโ Dockerfile
โ โโโ docker-compose.yml
โ โโโ .dockerignore
โ โโโ streamlit.log
โโโ Jenkinsfile
โโโ gitingore
โโโ README.md
Before setting up this project, ensure you have the following installed on your EC2 instance:
# Install Java (required for Jenkins)
sudo apt update
sudo apt install openjdk-17-jdk -y
# Add Jenkins repository and install
sudo wget -O /etc/apt/keyrings/jenkins-keyring.asc \
https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key
echo "deb [signed-by=/etc/apt/keyrings/jenkins-keyring.asc]" \
https://pkg.jenkins.io/debian-stable binary/ | sudo tee \
/etc/apt/sources.list.d/jenkins.list > /dev/null
sudo apt-get update
sudo apt-get install jenkins
# Start and enable Jenkins
sudo systemctl start jenkins
sudo systemctl enable jenkins
# uninstall all conflicting packages
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
# Install Docker Compose
sudo curl -SL https://github.com/docker/compose/releases/download/v2.37.3/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
# Start and enable Docker
sudo systemctl start docker
sudo systemctl enable docker
# Add jenkins user to docker group
sudo usermod -aG docker jenkins
# Restart Jenkins service to apply group changes
sudo systemctl restart jenkins
# Verify jenkins user can run docker commands
sudo su - jenkins
docker --version
docker-compose --version
To dockerize this project, several file were added, including the Dockerfile, which was built in a mulit-stage approach, the docker compose file, which orchestrates the Streamlit container with proper port mapping and restart policies, and the dockerignore file, which excludes unnecessary files from the Docker build context.
๐ First time setup? Generate and configure your SSH key:
ssh-keygen -t ed25519 -C "[email protected]"
cat ~/.ssh/id_ed25519.pub
-
Copy the public key and add it to your GitHub account:
- Go to
GitHub โ Settings โ SSH and GPG keys โ New SSH Key
- Paste the key and save.
- Go to
-
Add the private key
~/.ssh/id_ed25519
to Jenkins:- Navigate to
Jenkins Dashboard โ Manage Jenkins โ Credentials
- Select the
Global
domain and clickAdd Credentials
- Choose "SSH Username with private key"
- Username:
git
(recommended for GitHub) - Private Key: paste the contents of
~/.ssh/id_ed25519
- ID: give it a clear name like
github-ssh-key
- Username:
- Navigate to
To verify that your SSH key is correctly set up and GitHub recognizes it:
ssh -T [email protected]
If successful, you'll see a message like:
Hi username! You've successfully authenticated, but GitHub does not provide shell access.
Now you're ready to fork and clone the repo:
- Fork the original repository to your own GitHub account.
- clone your fork using SSH:
git clone [email protected]:<your-username>/simple_jenkins.git
cd simple_jenkins/streamlit_app
# Build and start the container
docker-compose up -d
# View logs
docker-compose logs -f
# Stop the container
docker-compose down
# Build the image
docker build -t streamlit-app .
# Run the container
docker run -d -p 8501:8501 --name streamlit_app streamlit-app
# View logs
docker logs streamlit_app
# Stop the container
docker stop streamlit_app
docker rm streamlit_app
Once the container is running, open your browser and navigate to:
http://<your-ec2-public-ip>:8501
The systemd service has been updated to manage the Docker containerized application:
sudo nano /etc/systemd/system/streamlit-app.service
[Unit]
Description=Streamlit Docker App Service
After=docker.service
Requires=docker.service
[Service]
WorkingDirectory=/var/lib/jenkins/simple-jenkins-dockerized/streamlit_app
ExecStart=/usr/local/bin/docker-compose up -d
ExecStop=/usr/local/bin/docker-compose down
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable streamlit-app
sudo systemctl start streamlit-app
sudo systemctl status streamlit-app
- Pulls code from GitHub
- Lints
app.py
using Docker containers - Runs unit tests in isolated Docker environments
- Builds Docker images with Docker Compose
- Manages container lifecycle (stop, build, start)
- Sends email notifications on build events
- Triggers automatically on GitHub push
Stage | Description | Docker Usage |
---|---|---|
Checkout | Clones the repository to Jenkins workspace | N/A |
Lint | Runs flake8 linting | docker run python:3.11 container |
Test | Executes pytest unit tests | docker run python:3.11 container |
Build & Deploy | Builds Docker image and deploys with docker-compose | docker-compose build + docker-compose up |
Archive | Creates deployment archive | N/A |
The original pipeline had separate stages with individual deploy script:
Stage | Description | Issues Identified |
---|---|---|
Pull Repo | Clones repository to specific location | Used GIT_DIR which conflicted with Git |
Lint | Runs flake8 in temporary Docker container | File path issues with artifact copying |
Build | Builds Docker image with docker-compose | Separate from deploy, causing context issues |
Test | Runs pytest in temporary Docker container | File path issues with artifact copying |
Deploy | Executes separate deploy.sh script |
Different context from build stage |
The improved version addresses the issues:
// Combined Build & Deploy stage eliminates context confusion
stage("Build & Deploy") {
steps {
timeout(time: 10, unit: 'MINUTES') {
sh '''
sudo docker-compose down --remove-orphans || true
sudo docker system prune -f || true
sudo docker-compose build --no-cache --pull
sudo docker-compose up -d
# Verification step
if sudo docker-compose ps | grep -q "Up"; then
echo "โ
Deployment successful!"
else
echo "โ Container failed to start"
sudo docker-compose logs --tail=20
exit 1
fi
'''
}
}
}
- Environment Variable Fix: Renamed
GIT_DIR
toWORKSPACE_DIR
to avoid Git conflicts - Combined Build & Deploy: Eliminated separate deploy script context issues
- Better Error Handling: Added container verification and logging
- Cache Management: Force pull latest base images with
--no-cache --pull
- Proper Artifact Handling: Fixed file path issues in lint and test stages
Problem: Jenkins couldn't run Docker commands due to permission restrictions.
# Error: permission denied while trying to connect to Docker daemon
Solution: Added jenkins user to docker group and restarted Jenkins service:
sudo usermod -aG docker jenkins
sudo systemctl restart jenkins
Problem: The GIT_DIR
environment variable was conflicting with Git operations in Jenkins, causing repository access issues.
# Error: Not a git repository
# Error: fatal: not a git repository (or any of the parent directories): .git
Solution: Renamed the environment variable from GIT_DIR
to WORKSPACE_DIR
:
environment {
WORKSPACE_DIR = '/var/lib/jenkins/simple-jenkins-dockerized' // Renamed from GIT_DIR
// ...
}
Problem: The original pipeline had separate Build and Deploy stages running from different contexts, causing Docker build inconsistencies.
Initial Approach (Problematic):
stage("Build") {
// Built Docker image in one context
sh 'docker-compose build'
}
stage("Deploy") {
// Ran deploy.sh script in different context
sh 'bash ${APP_DIR}/deploy.sh'
}
Solution: Combined Build & Deploy into a single stage with consistent context:
stage("Build & Deploy") {
steps {
dir("${APP_DIR}") {
sh '''
sudo docker-compose down --remove-orphans || true
sudo docker-compose build --no-cache --pull
sudo docker-compose up -d
'''
}
}
}
Problem: In the original pipeline, artifact files weren't being found due to path inconsistencies:
# Original problematic approach
sh 'cp /var/lib/jenkins/simple-jenkins-dockerized/streamlit_app/lint_report.txt lint_report.txt'
archiveArtifacts artifacts: '**/lint_report.txt'
Solution: Used proper dir()
blocks and relative paths:
dir("${APP_DIR}") {
sh '''
docker run --rm -v $(pwd):/app python:3.11 \
bash -c "pip install flake8 && flake8 /app/app.py" \
| tee lint_report.txt || true
'''
archiveArtifacts artifacts: 'lint_report.txt', allowEmptyArchive: true
}
Problem: Docker containers would stop when Jenkins build finished.
Solution: Used docker-compose up -d
(detached mode) and proper systemd service integration to ensure containers persist independently of Jenkins builds.
Same as before - configure webhook to trigger on push events:
- Navigate to your GitHub repo โ Settings > Webhooks
- Payload URL:
http://<JENKINS_PUBLIC_IP>:8080/github-webhook/
- Content type:
application/json
- Event: Just the push event
Email notifications remain configured for:
- โ Success
- โ Failure
โ ๏ธ Unstable- ๐ State Change
- ๐ง Fixed
- ๐ฆ Always (post actions)
- Jenkins user has Docker group permissions (required for container management)
- All containers run as non-root users where possible
- Docker daemon socket is protected by group permissions
- Container images are built from official Python base images
- Consistency: Same environment across development, testing, and production
- Isolation: Each stage runs in its own container
- Scalability: Easy to scale horizontally with Docker Compose
- Dependency Management: All dependencies are containerized
- Quick Rollbacks: Easy to revert to previous container versions
- Resource Efficiency: Containers share the host OS kernel
- Ensure port 8501 is open in the EC2 security group
- Monitor Docker resource usage:
docker stats
- Clean up unused containers periodically:
docker system prune
- Container logs are available via:
docker-compose logs
- Consider future enhancements:
- Multi-stage Docker builds for smaller images
- Container orchestration with Kubernetes
- Docker registry integration for image versioning
- Health checks and monitoring with Prometheus
-
Container won't start:
docker-compose logs streamlit
-
Permission errors:
# Check if jenkins user is in docker group groups jenkins
-
Port already in use:
sudo docker-compose down sudo netstat -tulpn | grep 8501
-
Build failures:
# Clean everything and rebuild sudo docker system prune -a sudo docker-compose build --no-cache
This project demonstrates the evolution from a traditional Jenkins CI/CD pipeline to a fully dockerized, production-ready deployment system. The journey highlighted several critical lessons that transformed a fragile, cache-dependent pipeline into a robust, deterministic deployment process.
The most significant breakthrough was realizing that build and deploy are not separate concerns - they're part of the same atomic operation that should happen in the same execution context. Splitting them across different stages created inconsistencies that manifested as "phantom deployments" where builds succeeded but changes weren't reflected.
Docker's caching mechanism, while beneficial for development, becomes a liability in automated pipelines. The solution was implementing aggressive cache-busting strategies:
--no-cache --pull
flags force fresh builds every timedocker system prune -f
eliminates stale artifacts- Assumption: cache is always stale in production pipelines
Eliminating the separate deploy.sh
script reduced complexity and failure points. The refactored pipeline has:
- Fewer moving parts โ Lower failure probability
- Single source of truth โ Easier debugging
- Unified execution context โ Consistent behavior
Naming conflicts like GIT_DIR
can cause subtle, hard-to-debug issues. Always research system-reserved variable names before using them in your pipeline.
Aspect | Before (v1) | After (v2) | Impact |
---|---|---|---|
Context | Split Build/Deploy stages | Unified Build & Deploy | โ Eliminates context confusion |
Caching | Relied on Docker cache | Aggressive cache busting | โ Guarantees fresh deployments |
Scripts | External deploy.sh |
Integrated pipeline logic | โ Reduces complexity |
Artifacts | Absolute path copying | Relative paths with dir() |
โ Consistent file handling |
Reliability | Intermittent cache issues | Deterministic builds | โ Production-ready |
- Keep Related Operations Together: Build and deploy should be atomic
- Assume Cache is Stale: Always use
--no-cache
in CI/CD - Verify Everything: Include health checks and container status verification
- Use Proper Directory Context: Leverage Jenkins
dir()
blocks for consistency - Minimize External Dependencies: Fewer scripts = fewer failure points
This foundation enables several advanced capabilities:
- Blue-Green Deployments: Zero-downtime updates with multiple container instances
- Multi-Environment Pipelines: Extend to staging/production with environment-specific configs
- Kubernetes Migration: Container-ready architecture simplifies orchestration adoption
- Monitoring Integration: Add health checks, metrics, and alerting
- Security Hardening: Implement container scanning and vulnerability assessments
The transformation from a traditional deployment to a dockerized CI/CD pipeline wasn't just about adopting new technology - it was about understanding the fundamental principles of reliable automation. The challenges we faced and solved - permission issues, environment conflicts, cache problems, and context confusion - are common pitfalls that many teams encounter.
The real value lies not in the specific tools used, but in the systematic approach to problem-solving: identifying root causes, implementing incremental fixes, and continuously refining the process. This methodology applies whether you're working with Jenkins and Docker, GitHub Actions and Kubernetes, or any other CI/CD stack.
By documenting both the failures and solutions, this project becomes a reference for future implementations and a testament to the iterative nature of DevOps engineering. Every challenge was an opportunity to build a more robust system - and that's the essence of continuous improvement in DevOps culture.