Docker Interview Questions

Docker-in-Docker and CI Build Patterns

questions
Scroll to track progress

Your Jenkins CI pipeline runs in a Docker container. The pipeline needs to build Docker images (for deploying the app). The container running Jenkins needs to invoke docker build, but Docker isn't installed inside the container—there's no Docker daemon. You can't build images. Explain the Docker-in-Docker problem and solutions.

This is the classic Docker-in-Docker (DinD) problem: a container trying to build Docker images requires Docker daemon, which isn't available inside containers. Explain the problem: (1) Docker is client-server: docker CLI (client) talks to a daemon (server) via a socket (/var/run/docker.sock). The daemon manages containers, images, networking. (2) Inside a container, there's no daemon—only the filesystem and app processes. The docker CLI isn't even installed. Running docker build fails. (3) Solutions: (A) Socket mounting (DooD—Docker-outside-of-Docker): mount the host's /var/run/docker.sock into the container. The container's docker CLI talks to the host's daemon. docker run -v /var/run/docker.sock:/var/run/docker.sock jenkins. Now the Jenkins container can run docker build, and it controls the host's daemon. (B) DinD (true Docker-in-Docker): run a Docker daemon inside the container. docker run --privileged -d docker:dind. This container runs its own daemon. Other containers connect to it. More complex but isolated. (C) Kaniko: use Kaniko instead of Docker daemon. Kaniko builds images without a daemon. kaniko --dockerfile=Dockerfile --destination=registry/image. (D) Buildah/Podman: use Buildah (daemonless build tool) instead of Docker. (4) Pros and cons: socket mounting (DooD) is simple but has security implications (the container can access host's daemon and potentially other containers). DinD is isolated but requires --privileged flag and is slower. Kaniko/Buildah are daemonless and secure. (5) Best practice: use socket mounting (DooD) for development/CI, but understand the security model. In production, use Buildah or Kaniko for better isolation. Example: Jenkins in container, mounts host's docker socket: docker run -v /var/run/docker.sock:/var/run/docker.sock jenkins. Jenkins pipeline runs docker build; images are built on the host. This is the most common pattern in CI/CD.

Follow-up: What are the security implications of socket mounting (DooD)? Can a container escape isolation via the docker socket?

You're using DooD (docker socket mounting). A malicious Jenkinsfile could mount the host's root filesystem into a container and escape sandbox. This is a security risk. Your security team flags it as a critical issue. Design a secure alternative.

Socket mounting has serious security implications. Secure alternatives: (1) Rootless Docker: configure the Docker daemon to run as a non-root user. This limits what an escaped container can do. (2) Use Kaniko: Kaniko builds images without a daemon or root access. docker run kaniko build the image. This is isolated and secure. Example in CI: docker run -v /workspace:/workspace kaniko --dockerfile=/workspace/Dockerfile --destination=registry/image. (3) Use Buildah in unprivileged mode: Buildah is a daemonless build tool. It can build images as a non-root user. Example: podman run buildah bud -f Dockerfile -t myimage. (4) Use BuildKit (not traditional Docker daemon): docker buildx build uses BuildKit, which can run in isolation. docker run --rm -d buildkit-container. Then: docker buildx create --driver docker-container. (5) Run Docker daemon in unprivileged container: use Docker's --userns-remap to map container root to a non-root host user. This limits escape potential. (6) Restrict socket mounting with AppArmor/SELinux: apply mandatory access control policies to limit what the container can do via the socket. (7) Audit socket usage: log all docker socket calls and alert on suspicious behavior. (8) Use a dedicated secure registry: push images only to trusted registries; don't allow untrusted registries. Implementation: replace DooD with Kaniko: in Jenkinsfile, stage('Build') { steps { sh 'kaniko -f Dockerfile -d registry/image' } }. Kaniko runs unprivileged, builds the image, and pushes it. No socket mounting, no root access needed. This is the most secure approach for CI/CD image building.

Follow-up: How do you audit what a container does via the docker socket? What commands should you monitor?

You're running a CI pipeline with DooD. Multiple Jenkinsfiles build images simultaneously. They all push to the same Docker registry. Two pipelines accidentally create images with the same name (registry/app:latest). Both try to push. One succeeds; the other fails or overwrites. How do you prevent image name collisions and ensure atomic image building?

Concurrent image building can cause collisions and overwrites. Coordinate: (1) Use unique image tags per build: docker build -t registry/app:build-$BUILD_NUMBER-$COMMIT_SHA . This ensures each build creates a unique image. Jenkins provides $BUILD_NUMBER and git provides $COMMIT_SHA. (2) Implement image naming conventions: registry/app:feature-branch-$BUILD_NUMBER or registry/app:pr-$PR_NUMBER. This prevents collisions and enables traceability. (3) Use a build queue to serialize builds for the same image: if two pipelines are building the same image, queue one. Only one build proceeds; the other waits. This prevents collisions. (4) Atomic pushes: use docker buildx build --cache-to type=registry --push, which is atomic. The image is pushed with a digest; if the push fails mid-way, the partial image isn't available. (5) Use image locking: before building, acquire a lock (via Consul, etcd, or Redis). Release after pushing. This serializes concurrent builds. (6) Versioning: tag images with semantic versions (v1.0.0, v1.0.1) instead of latest. latest is always the most recent build; versioned tags are immutable. (7) Implement image promotion: builds always tag as app:build-123 (unique). After tests pass, promote to app:staging. After staging tests, promote to app:latest. This avoids collisions—each stage has a distinct tag. (8) Use container image format v2: Docker v2 images support atomic operations better than v1. Implementation: in Jenkinsfile, uniquely tag each build: BUILD_IMAGE="registry/app:build-${env.BUILD_NUMBER}" && docker build -t $BUILD_IMAGE . && docker push $BUILD_IMAGE. After tests pass, promote: docker pull $BUILD_IMAGE && docker tag $BUILD_IMAGE registry/app:latest && docker push registry/app:latest. This prevents collisions and maintains ordered image versions.

Follow-up: How do you handle image garbage collection when you have many builds? Old images accumulate—when should you delete them?

Your Docker CI pipeline uses socket mounting. The build process creates intermediate Docker images (from multi-stage builds) that accumulate on the host. After thousands of builds, the host disk is filled with dangling images. The host storage is exhausted, and new builds fail. How do you prevent dangling image accumulation?

Dangling images consume storage and aren't cleaned up automatically. Prevention: (1) Use docker image prune after each build: in Jenkinsfile, after build succeeds, run docker image prune -f. This removes dangling images immediately. (2) Configure docker daemon to auto-prune: add to /etc/docker/daemon.json: "storage-driver-opts": ["dm.use_deferred_deletion": true]. This enables deferred deletion of dangling layers. (3) Implement a cleanup job: schedule a cron job to run docker system prune -a periodically (e.g., hourly). (4) Monitor dangling image count: emit a metric after each build: docker images -f dangling=true | wc -l. Alert if the count exceeds a threshold (e.g., > 100). (5) Aggressive cleanup for CI: since CI is stateless, aggressively prune. docker system prune -a --volumes removes all unused images and volumes. (6) Use BuildKit with proper cleanup: BuildKit's build cache can accumulate. Prune it: docker buildx prune -a. (7) Implement image retention policy: keep only the last N builds of each image. Delete older ones. Example: docker images | grep registry/app | tail -n +11 | awk '{print $3}' | xargs docker rmi (keep 10, delete older). (8) Use a registry with auto-cleanup: some registries (Artifactory, Nexus) support automatic cleanup policies. They delete images older than N days or beyond N versions. Implementation: in Jenkinsfile, after each build, run cleanup: stage('Cleanup') { steps { sh 'docker image prune -f && docker system prune -a -f' } }. Also schedule a daily cleanup job: 0 2 * * * docker system prune -a -f. This prevents dangling image accumulation and keeps host storage available.

Follow-up: How do you distinguish between images that should be kept vs. deleted? What's a good retention policy?

You're running tests inside Docker containers as part of CI. The test container needs to spin up additional containers (e.g., for integration tests: app container, postgres container, redis container). This requires Docker-in-Docker. But you want to avoid the security issues of DooD socket mounting. Design a secure test infrastructure.

Integration tests often need multiple services. Secure approach: (1) Use docker-compose inside the test container: instead of socket mounting, run docker-compose. For this to work, install Docker and docker-compose inside the test container AND run the container with Docker daemon (true DinD). Example: Dockerfile: FROM docker:dind, RUN apk add docker-compose, COPY tests /tests. Run: docker run --privileged -d my-test-container. (2) Alternatively, use Podman instead of Docker: Podman is daemonless and can run unprivileged. Run your tests with Podman. Podman can spin up containers without a daemon. (3) Use Testcontainers: this is a library (available in Java, Python, Go, etc.) that manages container lifecycle for tests. Testcontainers.testcontainers_python import PostgresContainer. It creates and tears down containers automatically for tests. Usage: during test setup, Testcontainers creates a postgres container; test runs; container is cleaned up. No manual docker compose needed. (4) Use kind (Kubernetes in Docker) for Kubernetes tests: if you're testing on Kubernetes, use kind to create a test cluster. kind create cluster. (5) Use microservices running as separate hosts: instead of containers within containers, deploy services to separate VMs or hosts. This avoids DinD complexity entirely. (6) For CI, use a test runner that manages services: GitLab CI, GitHub Actions, etc., have native support for services. Services are spun up for each job. In GitHub Actions: services: postgres: image: postgres. This creates a postgres container available to the test, without needing Docker-in-Docker. Implementation: use Testcontainers for tests that need ephemeral services. in GitHub Actions, use services. For complex test environments, use kind for Kubernetes tests. This avoids socket mounting and provides secure, isolated test environments. Example GitHub Actions workflow: services: postgres: image: postgres; jobs: test: runs-on: ubuntu-latest, services: [postgres]. The test has postgres available without managing containers manually.

Follow-up: How do you implement Testcontainers for your specific language? What's the setup?

You have a true DinD setup (Docker daemon inside a container). A container builds an image and pushes it to the internal registry. The push succeeds from inside the container's Docker, but external tools can't find the image. The image is isolated to the DinD container's daemon, not accessible to the host or other containers. How do you share images between DinD and the host?

DinD creates an isolated Docker daemon; images built inside are isolated from the host. Share images: (1) Push to an external registry: instead of saving locally, push the image to a registry (internal or Docker Hub). Other containers/hosts pull from the registry. docker push registry/image:tag. This is the standard pattern. (2) Use docker save/docker load with volumes: save the image as a tar file in the DinD container, mount a volume to access it from the host. In DinD: docker save registry/image -o /shared/image.tar. The /shared directory is mounted from the host: docker run -d -v /host/shared:/shared dind:dind. Outside, load: docker load < /host/shared/image.tar. (3) Use docker export for containers: if you want to export a container as an image, use docker export (creates a tarball of the filesystem). Then import on the host: docker import. (4) Configure DinD to share the registry with the host: both DinD and host Docker can push to the same internal registry. This is the preferred method. DinD and host both have access to registry (not to each other's local images). (5) Network DinD with the host: if DinD needs to access host Docker, expose the Docker socket over a network. docker run -d -e DOCKER_HOST=tcp://host:2375 dind. But this has security implications. Best practice: always push images to a central registry. DinD builds and pushes → registry. Host pulls from registry. This ensures images are accessible everywhere and provides a single source of truth. Example: DinD pipeline: docker build -t registry/app:build-123 . && docker push registry/app:build-123. Host/external systems pull: docker pull registry/app:build-123. This decouples DinD from the host and ensures scalability.

Follow-up: What's the performance difference between DinD and socket mounting (DooD)? Which is faster?

Your CI system uses socket mounting (DooD). A container accesses /var/run/docker.sock and executes docker run -v /:/mnt. This mounts the host's root filesystem into a new container. The attacker escapes the container sandbox and gains host access. Your security team demands zero-socket CI. Design a completely socket-free CI architecture.

Socket mounting is a security liability in untrusted CI environments. Socket-free architecture: (1) Use Kaniko for image building: Kaniko builds images without Docker daemon or socket. It's daemonless and can run unprivileged. Example: docker run kaniko -f Dockerfile -d registry/image. No socket mounting needed. (2) Use Buildah for image building: Buildah is another daemonless build tool. podman run buildah bud -f Dockerfile -t image. (3) Use native container runtimes: containerd (used by Kubernetes) has a CLI (ctr) for building images. Less common than Docker but works. (4) Use cloud-native build systems: Cloud Build (GCP), AWS CodeBuild, or similar manage the build environment securely. You submit a build configuration; the service builds it. You don't need Docker access. (5) For integration tests, use Testcontainers or native container APIs: your test code directly interacts with container runtimes (containerd, CRI) instead of Docker CLI. (6) Use Docker without socket exposure: in a controlled environment (Kubernetes pod), the container can use the Docker client to talk to a socket, but the socket is managed by Kubernetes and not directly exposed. (7) Use a build server with restricted permissions: instead of giving containers socket access, run builds in a restricted environment (seccomp, AppArmor) where they can't escape. (8) Orchestrate via Git + GitOps: Git is your source of truth. A CD system (ArgoCD, Flux) watches Git, detects changes, and triggers builds in a secure build environment (not via container socket). Implementation: replace all docker build with kaniko. Replace docker run for tests with Testcontainers or native container APIs. Use Cloud Build for production CI. This eliminates socket exposure entirely and provides better security isolation. Example: CI pipeline: kaniko -f Dockerfile -d registry/image. No docker build, no socket needed. Tests use Testcontainers (no socket). This is the most secure approach for untrusted CI environments.

Follow-up: What are the trade-offs of socket-free CI? Is it worth the complexity compared to well-secured socket mounting?

You've migrated your CI from docker socket mounting to Kaniko. Kaniko is more secure (daemonless, unprivileged), but your build times have increased by 30%. Builds that took 2 minutes now take 2.5-3 minutes. The team is complaining about slower feedback loops. Analyze the performance difference and find optimizations.

Kaniko trades security for some performance overhead. Analyze: (1) Kaniko doesn't leverage Docker daemon caching or layer reuse across builds. Each build starts fresh. Solution: implement external cache with --cache=true and --cache-repo=registry/cache. This replicates Docker's layer cache behavior. (2) Kaniko doesn't support BuildKit's parallel layer building. Stages build sequentially. Solution: refactor Dockerfile to minimize sequential dependencies. Put independent stages first. (3) Kaniko uses different image format than Docker daemon. The builds are optimized slightly differently. Solution: profile both with time docker build vs time kaniko. Compare layer timings. (4) Network speed: Kaniko may pull base images and dependencies differently. Ensure your network stack (DNS, bandwidth) is optimal. (5) Use cache efficiently: kaniko --cache=true --cache-repo=registry/cache pushes cache to the registry. Subsequent builds reuse cached layers. Without this, every build rebuilds all layers. (6) Dockerfile optimization: combine RUN commands to reduce layers. Use .dockerignore aggressively to exclude files from COPY. (7) Use distroless or minimal base images: smaller base images pull faster. (8) Consider hybrid: use Kaniko only for builds from untrusted branches. Use fast builds (Docker with socket) for trusted branches (main, develop). This balances security and performance. Implementation: enable Kaniko cache in CI: kaniko -f Dockerfile -d registry/image --cache=true --cache-repo=registry/app/cache. Optimize Dockerfile: combine RUN commands, use smaller base images. For trusted builds, use docker build (faster). For untrusted builds, use Kaniko (secure). This maintains security while minimizing performance impact. The 30% slowdown can often be recovered with proper caching and Dockerfile optimization.

Follow-up: What's the trade-off between security and performance in CI? Is there a build tool that's both fast and secure?

Want to go deeper?