Running LocalStack on Jenkins with Docker-in-Docker

Learn how to run LocalStack on Jenkins with Docker-in-Docker when Docker socket mounting is unavailable, using an isolated DinD daemon for CI integration tests.

Running LocalStack on Jenkins with Docker-in-Docker

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

Terminal window
git clone https://github.com/localstack-samples/sample-localstack-jenkins
cd sample-localstack-jenkins

The 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 choices

Step 2: Start the stack

Terminal window
docker compose up -d

This starts two containers:

  • jenkins — a custom Jenkins image with Python, Node.js, Docker CLI, and awslocal pre-installed. It talks to the DinD daemon via the DOCKER_HOST environment variable.
  • dind — a docker:dind container 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:

Terminal window
docker compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword

Paste 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:

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_HOST is set to tcp://dind:2375 in the Jenkins container’s environment (configured in docker-compose.yml), so every docker command in the pipeline runs against the DinD daemon, not the host.
  • -p 4566:4566 publishes LocalStack out of the DinD daemon — this is what makes dind:4566 reachable from Jenkins.
  • AWS_ENDPOINT_URL=http://dind:4566 and LOCALSTACK_HOST=dind are set in the top-level environment block, so deploy scripts talk to the right endpoint.
  • LOCALSTACK_HOSTNAME is set to the container name so Lambda runtime containers can resolve LocalStack by name within the DinD daemon’s network.
  • /var/run/docker.sock here 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:

  1. Check out the quiz app from GitHub
  2. Install Python and Node dependencies
  3. Start LocalStack inside DinD
  4. Deploy the full AWS infrastructure (Lambda, S3, DynamoDB, API Gateway, CloudFront)
  5. Run the integration tests
  6. 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

Launch yourself in the world of local cloud development

Start a free trial