This repository contains a production-ready, fully containerized Jenkins CI/CD server.
It is designed for low-cost VPS hosting (like DigitalOcean) and uses Docker outside of Docker (DooD) to spin up ephemeral build agents. It includes Caddy for automatic HTTPS and Jenkins Configuration as Code (JCasC) for version-controlled settings.
- Jenkins Controller: Runs as a container. Has 1 executor available, but builds should use Docker agents to avoid overloading the controller.
- Docker Agents: The controller mounts the host's Docker socket (
/var/run/docker.sock). When a pipeline runs, Jenkins spins up a temporary sibling container (e.g., Python, Node) on the host to execute the build. - Caddy: Acts as a reverse proxy (configured in
Caddyfile), handling Let's Encrypt SSL termination automatically. - JCasC: All Jenkins configuration (users, views, security) is defined in
jenkins.yaml.
numExecutors: 1 to prevent build hangs, but you must configure your pipelines to use Docker agents. Jobs without an agent specification will run directly on the controller, which can cause performance issues.
- A Linux Server: A DigitalOcean Droplet (Docker on Ubuntu) is recommended.
- Domain Name: An
A Recordpointing to your server's IP (e.g.,jenkins.yourdomain.com). - Swap Space (Critical): If using a server with < 4GB RAM, you must enable swap or Jenkins will crash.
Run these commands on your Droplet before installing Jenkins:
sudo fallocate -l 2G /swapfile && \
sudo chmod 600 /swapfile && \
sudo mkswap /swapfile && \
sudo swapon /swapfile && \
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstabgit clone <your-repo-url>
cd jenkins-configWe do not store passwords or domains in Git. Create your local environment file:
cp .env.example .env
nano .envFill in your details:
- ADMIN_PASSWORD: Set a strong password.
- JENKINS_URL: e.g.,
https://jenkins.yourdomain.com/ - DOMAIN_NAME: e.g.,
jenkins.yourdomain.com
To allow Jenkins to spin up containers, it needs the Group ID (GID) of the host's Docker group.
Find the ID:
getent group docker | cut -d: -f3
# Output example: 999Update docker-compose.yml: Ensure the group_add: section matches this number.
Run the stack using Docker Compose V2:
docker compose up -d --buildWait ~30-60 seconds for Jenkins to initialize and for Caddy to obtain SSL certificates.
- Visit:
https://jenkins.yourdomain.com - Login:
admin/<Password from .env>
Note: First-time SSL certificate issuance may take 1-2 minutes. If you see certificate errors, check Caddy logs:
docker compose logs caddyTo leverage the DooD architecture, always specify a Docker agent in your pipelines. This ensures builds run in isolated containers, not on the Jenkins controller.
pipeline {
agent {
docker {
image 'python:3.9'
}
}
stages {
stage('Test') {
steps {
sh 'python --version'
sh 'pip install -r requirements.txt'
sh 'pytest'
}
}
}
}pipeline {
agent any // ❌ Will use the controller's executor
stages {
stage('Test') {
steps {
sh 'python --version' // Runs on controller, may fail or cause issues
}
}
}
}Recommendation: Use agent { docker { image '...' } } for all production pipelines.
Do not change settings in the Jenkins UI. They will be lost on reboot.
- Edit
jenkins.yaml(e.g., to add a user or change a system message). - Apply changes without downtime:
- Go to Manage Jenkins → Configuration as Code.
- Click Reload existing configuration.
- Edit
plugins.txt. - Rebuild the container:
docker compose up -d --build
- Update
ADMIN_PASSWORDin your.envfile. - Restart the container to inject the new variable:
docker compose up -d
- Config: Stored in this Git repo.
- Data: Stored in the
jenkins_homeDocker volume (build history, logs). - Certificates: Stored in
caddy_datavolume.
To Backup Data:
docker run --rm -v jenkins_home:/data -v $(pwd):/backup ubuntu tar czf /backup/jenkins-data-$(date +%F).tar.gz /dataTo Restore Data:
docker compose down
docker run --rm -v jenkins_home:/data -v $(pwd):/backup ubuntu tar xzf /backup/jenkins-data-YYYY-MM-DD.tar.gz -C /data --strip-components=1
docker compose up -dSymptom: Jenkins cannot spin up Docker agents. Pipeline fails with permission errors when trying to use Docker.
Fix: Double-check that the group_add GID in docker-compose.yml matches the output of getent group docker on your host.
# Find your Docker GID
getent group docker | cut -d: -f3
# Update docker-compose.yml to match, then restart
docker compose up -dSymptom: Build queues indefinitely without starting.
Possible Causes:
-
Pipeline not using Docker agent: If your pipeline uses
agent anyor no agent specification, it needs the controller's executor. Verify your pipeline specifiesagent { docker { ... } }. See the "Pipeline Best Practices" section above. -
Disk full or Swap exhausted: Jenkins takes the node offline when resources are critical. Check disk space (
df -h) and swap (free -h). -
Docker socket permission issues: See the "Permission Denied" section above.
Symptom: Certificate errors or "Your connection is not private" warnings.
Fix: Check Caddy logs for certificate issuance errors:
docker compose logs caddyCommon causes:
- DNS A Record not propagated or pointing to wrong IP
- Port 80 blocked (required for ACME challenge)
- Domain name mismatch in
.envfile
MIT