Introduction
In a previous post, we walked through wiring LocalStack into a Jenkins pipeline using Docker socket mounting — the approach where the Jenkins agent mounts /var/run/docker.sock and starts LocalStack as a sibling container on the host daemon. It’s the simplest setup and works well for most self-hosted Jenkins installations.
But in some environments, the security policy disallows Docker socket mounting.
This is common in enterprise pipelines. Exposing /var/run/docker.sock gives the container root-equivalent access to the host Docker daemon — it can start, stop, or inspect any container on the machine. Many platform teams block it outright. If that’s your environment, Docker-in-Docker (DinD) is the practical alternative.
This post shows how to set up LocalStack on Jenkins using DinD. The sample application is the same serverless quiz app from the previous post, so you can compare the two setups side by side. The full working example is in this repository.
How Docker-in-Docker changes the setup
With socket mounting, Jenkins and LocalStack share the host’s Docker daemon. With DinD, Jenkins gets its own dedicated Docker daemon running inside a container. LocalStack starts inside that daemon, fully isolated from the host.
Jenkins talks to LocalStack through the DinD daemon over the shared Docker bridge network. Each build starts a fresh LocalStack container, runs tests against it, and cleans up — without touching the host.
The tradeoff: DinD containers need to run in --privileged mode, which is a security consideration of its own. For most teams, though, a privileged DinD container that their platform team controls is more acceptable than exposing the host Docker socket to every CI job.
Prerequisites
- Docker Desktop (macOS / Windows) or Docker Engine (Linux)
- Git
- A LocalStack Auth Token (Pro license required for Lambda, CloudFront, and other advanced services)
Step 1: Clone the repository
git clone https://github.com/localstack-samples/sample-localstack-jenkinscd sample-localstack-jenkinsThe project structure:
sample-localstack-jenkins/├── Dockerfile # Jenkins image — adds Python, Node, Docker CLI, awslocal├── docker-compose.yml # Two services: Jenkins + DinD, shared bridge network├── Jenkinsfile # 5-stage pipeline: checkout → install → start → deploy → test├── README.md└── ARCHITECTURE.md # Deep-dive into networking and configuration choicesStep 2: Start the stack
docker compose up -dThis starts two containers:
- jenkins — a custom Jenkins image with Python, Node.js, Docker CLI, and
awslocalpre-installed. It talks to the DinD daemon via theDOCKER_HOSTenvironment variable. - dind — a
docker:dindcontainer running a privileged Docker daemon. LocalStack starts inside this daemon during each build.
Both containers are on the same Docker bridge network (jenkins-net), so Jenkins can reach the DinD daemon by hostname. LocalStack runs inside that daemon and is reachable from Jenkins at dind:4566 — DinD publishes the port onto jenkins-net via -p 4566:4566.
Wait about 30 seconds for Jenkins to initialize, then open http://localhost:8080.
Step 3: Unlock Jenkins and install plugins
Get the initial admin password:
docker compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPasswordPaste it into the Jenkins UI when prompted, then select Install suggested plugins. Create your admin user and click Save and Finish.
Step 4: Add the LocalStack Auth Token
Go to Manage Jenkins → Credentials → System → Global credentials → Add Credentials and create a credential of kind Secret text:
- Secret: your
ls-…token from https://app.localstack.cloud/workspace/auth-token - ID:
LOCALSTACK_AUTH_TOKEN
This is identical to the previous post — the credential setup doesn’t change between approaches.
Step 5: Create the pipeline job
Click New Item, enter a name (e.g. quiz-app), select Pipeline, and click OK. In the Pipeline section:
- Definition: Pipeline script from SCM
- SCM: Git
- Repository URL:
https://github.com/localstack-samples/sample-localstack-jenkins - Script Path:
Jenkinsfile
Click Save.
How the Jenkinsfile differs
The pipeline has the same five stages as the previous post — install, start LocalStack, deploy, test, and clean up — but the LocalStack startup stage works differently.
With socket mounting, Jenkins and LocalStack share the host daemon, so LocalStack is directly reachable at localhost:4566. With DinD, LocalStack runs inside the DinD daemon. Jenkins reaches it at dind:4566 — via the port mapping -p 4566:4566 that publishes LocalStack out of the DinD daemon onto the jenkins-net bridge. The socat proxy is still present, because the quiz app’s test_infra.py hardcodes localhost:4566 — socat forwards that to dind:4566. What changes is not whether socat is needed, but what it’s proxying to.
stage('Start LocalStack') { steps { sh ''' set -eu
# User-defined network inside DinD so Lambda containers can # resolve LocalStack by container name (default bridge has no DNS) docker network create "ls-net-${BUILD_NUMBER}"
docker run -d \ --name "${LS_CONTAINER}" \ --network "ls-net-${BUILD_NUMBER}" \ -p 4566:4566 \ -p 4510-4560:4510-4560 \ -v /var/run/docker.sock:/var/run/docker.sock \ -e LOCALSTACK_AUTH_TOKEN="${LOCALSTACK_AUTH_TOKEN}" \ -e LOCALSTACK_HOSTNAME="${LS_CONTAINER}" \ -e LAMBDA_DOCKER_NETWORK="ls-net-${BUILD_NUMBER}" \ localstack/localstack-pro
echo "Waiting for LocalStack to be ready..." for i in $(seq 1 60); do if docker exec "${LS_CONTAINER}" \ curl -fsS http://localhost:4566/_localstack/health \ > /dev/null 2>&1; then echo "LocalStack ready after $((i * 2))s" break fi if [ "$i" -eq 60 ]; then echo "Timed out waiting for LocalStack" docker logs "${LS_CONTAINER}" exit 1 fi sleep 2 done
# Tests hardcode localhost:4566 — proxy it to dind:4566 socat TCP-LISTEN:4566,reuseaddr,fork TCP:dind:4566 & echo $! > /tmp/socat.pid ''' }}A few things to note:
DOCKER_HOSTis set totcp://dind:2375in the Jenkins container’s environment (configured indocker-compose.yml), so everydockercommand in the pipeline runs against the DinD daemon, not the host.-p 4566:4566publishes LocalStack out of the DinD daemon — this is what makesdind:4566reachable from Jenkins.AWS_ENDPOINT_URL=http://dind:4566andLOCALSTACK_HOST=dindare set in the top-levelenvironmentblock, so deploy scripts talk to the right endpoint.LOCALSTACK_HOSTNAMEis set to the container name so Lambda runtime containers can resolve LocalStack by name within the DinD daemon’s network./var/run/docker.sockhere refers to the socket inside the DinD container, not the host socket — which is the whole point.
The post { always } block is the same as before: stop LocalStack, remove the per-build network.
Step 6: Run a build
Click Build Now. The pipeline will:
- Check out the quiz app from GitHub
- Install Python and Node dependencies
- Start LocalStack inside DinD
- Deploy the full AWS infrastructure (Lambda, S3, DynamoDB, API Gateway, CloudFront)
- Run the integration tests
- Clean up
A green checkmark means all tests passed.
Socket mounting vs. Docker-in-Docker: which to use
Both approaches run the same pipeline and pass the same tests. The choice comes down to your environment:
| Socket mounting | Docker-in-Docker | |
|---|---|---|
| Setup complexity | Lower | Slightly higher |
| Host Docker socket exposed | Yes | No |
| Works in security-restricted pipelines | Often not | Yes |
| Agent runs on bare metal / VM | Great fit | Works, but DinD adds overhead |
| Agent runs in a container | Needs socat workaround |
socat still needed, but proxying to DinD instead of host |
If your platform team allows socket mounting, the previous post is the simpler starting point. If you’re in an environment where socket mounting is blocked, or you want stronger build isolation, use the DinD approach shown here.
Learn More
- Running LocalStack in Jenkins for end-to-end AWS integration tests — the original post, covering socket mounting and networking with
socat - localstack-samples/sample-localstack-jenkins — the full Docker-in-Docker example repository, including
ARCHITECTURE.mdwith a detailed breakdown of every configuration choice - LocalStack Auth Token — getting started with a Pro license



