This repository contains a non-separated Docker Compose stack for running Jenkins with Docker-in-Docker, Nginx, and optional observability components on an Ubuntu server.
The stack is designed to be cloned and operated from a single root directory, for example:
/opt/compose-stack.
+-- compose.yaml
+-- compose.observability.yaml
+-- Dockerfile
+-- .env.example
+-- setup.sh
+-- setup.ps1
+-- nginx/
| +-- default.conf
+-- prometheus/
| +-- prometheus.yml
+-- grafana/
+-- provisioning/
+-- datasources/
+-- prometheus.yaml
There is no dedicated jenkins/ directory in this non-separated layout. Jenkins is defined as a service in compose.yaml, and its custom image is built from the root-level Dockerfile.
| Service | Purpose |
|---|---|
jenkins |
Jenkins LTS with Blue Ocean, Docker CLI, Pipeline, Docker Workflow, and Prometheus plugin. |
nginx |
Reverse proxy for Jenkins using nginx/default.conf. |
docker |
Docker-in-Docker daemon with TLS enabled and the internal network alias docker. |
prometheus |
Optional metrics scraper for Jenkins, host metrics, and container metrics. |
grafana |
Optional dashboard UI with Prometheus provisioned as the default datasource. |
node-exporter |
Optional Linux host metrics exporter. On Docker Desktop, it observes the Linux VM rather than Windows itself. |
cadvisor |
Optional container-level metrics exporter for Docker workloads. |
The stack uses Docker named volumes for stateful data. Application data is not stored inside this Git repository.
| Volume | Stores | Default host path on Ubuntu |
|---|---|---|
compose-stack-jenkins-data |
Jenkins home, jobs, plugins, credentials, and configuration | /var/lib/docker/volumes/compose-stack-jenkins-data/_data |
compose-stack-jenkins-docker-certs |
Docker-in-Docker client TLS certificates | /var/lib/docker/volumes/compose-stack-jenkins-docker-certs/_data |
compose-stack-prometheus-data |
Prometheus time-series database | /var/lib/docker/volumes/compose-stack-prometheus-data/_data |
compose-stack-grafana-data |
Grafana database, sessions, and plugins | /var/lib/docker/volumes/compose-stack-grafana-data/_data |
This is the recommended approach for this setup because the repository remains configuration-only, while stateful data is managed by Docker.
The only host bind mount used by default is:
${HOST_HOME:-/srv/jenkins/home}:/home
Use this path for files that need to be visible from both the Ubuntu host and the Jenkins container. Create it before starting the stack:
sudo mkdir -p /srv/jenkins/home
sudo chown -R 1000:1000 /srv/jenkins/homeOn Windows, setup.ps1 replaces the default Linux path with an absolute data/jenkins-home path inside the repository and creates the directory automatically.
If you later choose to use repository-local bind mounts, place them under data/. The data/ directory is ignored by Git.
Because this is a non-separated stack, Docker networks are defined directly in the Compose files. A separate infrastructure folder is not necessary unless each service is split into its own Compose project.
| Compose network | Default Docker network name | Members |
|---|---|---|
ci |
compose-stack-ci |
Jenkins, Nginx, Docker-in-Docker, and Prometheus |
observability |
compose-stack-observability |
Prometheus, Grafana, node-exporter, and cAdvisor |
Prometheus is attached to both networks so it can scrape Jenkins from the CI network and exporters from the observability network.
Internal service discovery uses Docker DNS:
- Nginx proxies Jenkins through
http://jenkins:8080. - Jenkins connects to Docker-in-Docker through
tcp://docker:2376.
Common requirements:
- Internet access for package, image, and plugin downloads
- Git
Ubuntu additionally requires either a root shell or a non-root user with sudo access. Docker Engine and the Docker Compose plugin are installed automatically when unavailable.
Windows requires Windows 10 or 11 with hardware virtualization enabled. Docker Desktop is installed through winget when unavailable; its installer may display a Windows UAC prompt or request a restart. Docker Desktop must use Linux containers.
Recommended deployment path:
sudo mkdir -p /opt/compose-stack
sudo chown -R "$USER:$USER" /opt/compose-stackClone or copy this repository into /opt/compose-stack.
Start the core stack:
cd /opt/compose-stack
bash setup.shStart the complete stack with Prometheus, Grafana, node-exporter, and cAdvisor:
cd /opt/compose-stack
bash setup.sh --with-observabilityThe commands above assume that the current shell is already running as root. From a non-root account, prefix setup and update commands with sudo, for example sudo bash setup.sh. The --check mode does not require root when the current user has permission to access Docker.
Ubuntu modes:
| Command | Purpose |
|---|---|
bash setup.sh |
Install or start the core stack from a root shell. |
bash setup.sh --with-observability |
Install or start the full monitoring stack from a root shell. |
bash setup.sh --check |
Validate and check an existing core deployment without changing it. |
bash setup.sh --with-observability --check |
Validate and check an existing full deployment. |
bash setup.sh --update |
Pull service images, rebuild Jenkins, and apply the core stack from a root shell. |
bash setup.sh --with-observability --update |
Update and apply the full stack from a root shell. |
bash setup.sh --skip-docker-install |
Start the stack but fail if Docker is unavailable. |
Run the core stack from PowerShell without sudo:
Set-Location C:\path\to\compose-stack
.\setup.ps1Run the complete stack with observability:
.\setup.ps1 -WithObservabilityWindows modes:
| Command | Purpose |
|---|---|
.\setup.ps1 |
Install Docker Desktop when required, then start the core stack. |
.\setup.ps1 -WithObservability |
Install or start the full monitoring stack. |
.\setup.ps1 -Check |
Validate and check an existing core deployment without changing it. |
.\setup.ps1 -WithObservability -Check |
Validate and check an existing full deployment. |
.\setup.ps1 -Update |
Pull service images, rebuild Jenkins, and apply the core stack. |
.\setup.ps1 -WithObservability -Update |
Update and apply the full stack. |
.\setup.ps1 -SkipDockerInstall |
Start the stack but fail if Docker Desktop is unavailable. |
.\setup.ps1 -Help |
Display the Windows setup options and examples. |
If the PowerShell execution policy blocks local scripts, use:
powershell -ExecutionPolicy Bypass -File .\setup.ps1 -WithObservabilityThe Windows script starts Docker Desktop when needed and waits for its daemon. Complete any Docker Desktop first-run, WSL 2, or license prompts shown on screen. Observability containers run inside Docker Desktop's Linux VM; node-exporter does not expose native Windows host metrics.
Both platform scripts perform the following operations:
- Verifies the platform and Docker requirements.
- Installs the platform's Docker runtime when required.
- Creates
.envfrom.env.examplewithout replacing an existing.env. - Replaces the default Grafana password with a generated password when observability is enabled.
- Validates and creates the configured Jenkins shared directory.
- Validates the merged Compose configuration.
- Builds and starts the requested services.
- Waits for service readiness and verifies the HTTP endpoints.
- On Windows observability runs, verifies every Prometheus target and the Grafana datasource.
- Displays the service URLs and the initial Jenkins administrator password when available.
The scripts are safe to run repeatedly. They do not remove containers, networks, named volumes, backups, or an existing .env file. Destructive reset operations remain manual.
Internet access is required when Docker, packages, container images, or Jenkins plugins must be downloaded. Review .env after the first setup, especially ports, HOST_HOME, and credentials.
Production-oriented Node.js pipeline references are available under examples/node-app:
Jenkinsfile.developmentbuilds, tests, pushes, deploys, and verifies an immutable image digest in development.Jenkinsfile.productionpromotes an existing digest with security scanning, authorized approval, health verification, and rollback.compose.deploy.example.yamldefines the remote Compose service contract expected by both pipelines.examples/node-app/README.mddocuments credentials, registry requirements, remote host preparation, and required customization.
The templates deploy to remote Ubuntu hosts over SSH with strict host-key verification. They are reference implementations and must be adapted to the application, registry, environments, and organizational release policy before production use.
The Jenkins image includes the OpenSSH client required by these examples. Rebuild an existing installation after pulling this change:
bash setup.sh --updateOn Windows:
.\setup.ps1 -UpdateCreate the local environment file:
cd /opt/compose-stack
cp .env.example .envOn Windows PowerShell, use:
Copy-Item .env.example .envReview and update .env before starting the stack. Important values:
| Variable | Default | Description |
|---|---|---|
COMPOSE_PROJECT_NAME |
compose-stack |
Compose project name. |
CI_NETWORK_NAME |
compose-stack-ci |
Docker network for CI services. |
OBSERVABILITY_NETWORK_NAME |
compose-stack-observability |
Docker network for monitoring services. |
JENKINS_HTTP_PORT |
49000 |
Direct Jenkins HTTP port on the host. |
JENKINS_AGENT_PORT |
50000 |
Jenkins inbound agent port. |
NGINX_HTTP_PORT |
9000 |
Nginx reverse proxy port on the host. |
HOST_HOME |
/srv/jenkins/home |
Host directory mounted to /home in Jenkins. |
PROMETHEUS_PORT |
9090 |
Prometheus HTTP port on the host. |
GRAFANA_PORT |
3030 |
Grafana HTTP port on the host. |
GRAFANA_ADMIN_PASSWORD |
change-me |
Initial Grafana admin password. Change this before production use. |
Prepare the shared host directory:
sudo mkdir -p /srv/jenkins/home
sudo chown -R 1000:1000 /srv/jenkins/homeStart Jenkins, Nginx, and Docker-in-Docker:
docker compose config --quiet
docker compose up -d --buildAccess endpoints:
| Endpoint | URL |
|---|---|
| Jenkins direct access | http://SERVER_IP:49000 |
| Jenkins through Nginx | http://SERVER_IP:9000 |
| Jenkins inbound agent port | SERVER_IP:50000 |
Get the initial Jenkins administrator password:
docker compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPasswordValidate and start the full stack including Prometheus, Grafana, node-exporter, and cAdvisor:
docker compose -f compose.yaml -f compose.observability.yaml config --quiet
docker compose -f compose.yaml -f compose.observability.yaml up -d --buildAccess endpoints:
| Endpoint | URL |
|---|---|
| Prometheus | http://SERVER_IP:9090 |
| Grafana | http://SERVER_IP:3030 |
Grafana uses the credentials from .env:
GRAFANA_ADMIN_USERGRAFANA_ADMIN_PASSWORD
The observability Compose file is equivalent to these base commands, with persistent volumes and provisioning added:
docker run -d --name prometheus -p 9090:9090 prom/prometheus
docker run -d --name grafana -p 3030:3000 grafana/grafanaPrometheus scrapes Jenkins, node-exporter, and cAdvisor through Docker DNS on the internal Compose networks. Prometheus and Grafana are exposed directly on their configured host ports; Nginx is dedicated to Jenkins.
Verify the monitoring targets after startup:
- Open
http://SERVER_IP:9090/targets. - Confirm that
prometheus,jenkins,node-exporter, andcadvisorareUP. - Open Grafana and confirm that the provisioned Prometheus datasource is healthy.
Create a backup directory:
mkdir -p backupsBack up Jenkins data:
docker run --rm -v compose-stack-jenkins-data:/data -v "$PWD/backups:/backup" alpine tar czf /backup/jenkins-data.tgz -C /data .Restore should be performed while Jenkins is stopped:
docker compose stop jenkins
docker run --rm -v compose-stack-jenkins-data:/data -v "$PWD/backups:/backup" alpine sh -c "cd /data && tar xzf /backup/jenkins-data.tgz"
docker compose start jenkinsCheck service status:
docker compose psFor the observability stack, always include both Compose files:
docker compose -f compose.yaml -f compose.observability.yaml psFollow Jenkins logs:
docker compose logs -f jenkinsRestart Jenkins:
docker compose restart jenkinsStop containers and remove networks while keeping volumes:
docker compose -f compose.yaml -f compose.observability.yaml downReset the full stack, including volumes:
docker compose -f compose.yaml -f compose.observability.yaml down -v- The Docker-in-Docker daemon port
2376is not published to the host. - Jenkins accesses Docker-in-Docker through the internal Docker network using
tcp://docker:2376. - The
docker:dindservice runs withprivileged: true, which is required for this Docker-in-Docker setup. - Do not commit
.env; it contains local configuration and credentials. - Change
GRAFANA_ADMIN_PASSWORDbefore exposing Grafana outside a trusted network.