Understanding Docker in GitLab CI
GitLab CI integrates seamlessly with Docker, allowing you to build, test, and deploy containerized applications within your CI/CD pipelines. At its core, this integration relies on the docker executor or the docker keyword within GitLab Runner configurations, enabling each job to run inside an isolated Docker container. This approach ensures reproducible builds, consistent environments across development and production, and eliminates the classic "it works on my machine" problem.
When you use Docker with GitLab CI, you're essentially defining a container image that serves as the runtime environment for your pipeline jobs. This image can be any public or private Docker image—from slim Alpine-based images for speed to full-featured images with all your dependencies pre-installed. Additionally, GitLab CI supports Docker-in-Docker (dind), which allows you to build Docker images inside a running Docker container, making it possible to build, tag, and push images as part of your pipeline.
Key Concepts and Terminology
Before diving into implementation details, it's important to understand the core components that make Docker work within GitLab CI:
- GitLab Runner — The agent that executes your CI jobs. It can be configured with the Docker executor to spin up containers for each job.
- Docker Executor — A Runner configuration that uses Docker to create ephemeral containers for each CI job, providing clean, isolated environments every time.
- Docker-in-Docker (dind) — A technique where a Docker daemon runs inside a container, allowing nested container operations like building and pushing images.
- Image — The base Docker image specified in your
.gitlab-ci.ymlfile that defines the environment for a job. - Service Containers — Auxiliary containers (like databases or message brokers) that run alongside your job container and provide networked services during pipeline execution.
- Registry Integration — GitLab's built-in Container Registry that stores your Docker images and integrates directly with CI variables for authentication.
Why Docker in GitLab CI Matters
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →The combination of Docker and GitLab CI addresses several fundamental challenges in modern software delivery:
Reproducible Build Environments
Without containerization, CI environments often drift over time. Manual package installations, differing OS versions, and inconsistent toolchains lead to builds that succeed on one Runner but fail on another. By defining your environment as a Docker image, you guarantee that every pipeline execution starts from an identical, known state. This reproducibility extends from your CI pipeline to local development when you use the same Docker image for both.
Simplified Dependency Management
Rather than maintaining complex scripts that install dependencies on bare-metal Runners, you encapsulate all dependencies within a Docker image. Need Node.js 20, PostgreSQL client libraries, and a specific version of Python? Build a custom image once, push it to your registry, and reference it in your pipeline. This approach reduces job setup time dramatically—often from minutes to seconds—since the image layers are cached and pulled incrementally.
Streamlined Deployment Pipeline
Docker in GitLab CI enables a unified artifact: the container image. You build the image in your CI pipeline, run tests against it, scan it for vulnerabilities, and then promote the exact same image through staging to production. This eliminates the disconnect between CI artifacts (like tarballs or compiled binaries) and production deployments, ensuring what you tested is precisely what goes live.
Scalable and Isolated Parallelism
Each Docker container runs in isolation, allowing multiple pipeline jobs to execute concurrently on the same host without interference. This isolation also enhances security—jobs cannot accidentally affect each other's file systems or processes. Combined with GitLab's parallel job execution, you can run extensive test matrices across different container images simultaneously.
Setting Up Docker with GitLab CI
Prerequisites
To use Docker in your GitLab CI pipelines, you need the following in place:
- A GitLab instance (self-hosted or GitLab.com) with a project repository
- A GitLab Runner registered with the Docker executor enabled
- Docker installed on the Runner host machine
- Access to a Docker registry (GitLab Container Registry or external like Docker Hub)
Configuring a GitLab Runner with Docker Executor
When registering a new Runner, select the docker executor and provide a default image. Here's how you register a Runner interactively:
gitlab-runner register \
--url https://gitlab.example.com \
--registration-token YOUR_TOKEN \
--executor docker \
--docker-image alpine:latest \
--docker-volumes /var/run/docker.sock:/var/run/docker.sock \
--description "docker-runner"
If you prefer configuring the Runner via its config.toml file directly, a typical Docker executor configuration looks like this:
[[runners]]
name = "docker-runner"
url = "https://gitlab.example.com"
token = "your-runner-token"
executor = "docker"
[runners.docker]
image = "alpine:latest"
privileged = false
volumes = ["/cache"]
shm_size = 256
[runners.cache]
[runners.cache.s3]
[runners.cache.gcs]
Writing Your First Docker-Based .gitlab-ci.yml
The simplest pipeline that uses Docker specifies an image at the job level. Here's a basic example that runs tests inside a Node.js container:
stages:
- test
- build
test_job:
stage: test
image: node:20-alpine
script:
- npm ci
- npm run test
artifacts:
paths:
- coverage/
build_job:
stage: build
image: node:20-alpine
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
In this configuration, each job pulls the node:20-alpine image, executes its scripts in an isolated container, and then the container is destroyed. Artifacts are passed between stages via GitLab's artifact mechanism, not through the Docker image itself.
Docker-in-Docker: Building Images Inside CI
One of the most powerful patterns is building Docker images within your CI pipeline. This requires Docker-in-Docker (dind) because you need access to a Docker daemon inside a container that is itself running via Docker.
Enabling Docker-in-Docker
There are two primary approaches to enable Docker commands inside your CI jobs:
Approach 1: Privileged Mode with dind Service
This is the recommended method for most use cases. You define a docker:dind service and use the docker image for your job:
stages:
- build
build_image:
stage: build
image: docker:24-dind
services:
- docker:24-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
Key elements in this configuration:
image: docker:24-dind— The job runs inside a Docker client container that includes the Docker CLIservices: docker:24-dind— Spins up a separate Docker daemon container that your job connects to via TCPDOCKER_TLS_CERTDIR: "/certs"— Configures TLS certificates for secure communication between client and daemonCI_REGISTRY_*variables — Automatically provided by GitLab for authentication with the built-in Container Registry
Approach 2: Socket Binding (Use with Caution)
An alternative is mounting the host's Docker socket into the container. This is simpler but poses security risks and is not recommended for production pipelines:
build_image:
stage: build
image: docker:24
script:
- docker build -t my-image:latest .
- docker push my-image:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
This approach gives the container direct access to the host's Docker daemon, meaning any container it spawns runs as sibling containers on the host rather than nested inside the job container. While this avoids the complexity of dind networking, it breaks container isolation and can cause naming conflicts with concurrently running jobs.
Using Service Containers for Integration Testing
Service containers provide auxiliary services during your pipeline execution. They are particularly useful for integration tests that require databases, message queues, or other networked services.
Example: PostgreSQL Service for Rails Testing
test_integration:
stage: test
image: ruby:3.2
services:
- postgres:16-alpine
variables:
POSTGRES_DB: test_db
POSTGRES_USER: runner
POSTGRES_PASSWORD: secret
DATABASE_URL: postgresql://runner:secret@postgres:5432/test_db
script:
- bundle install
- bundle exec rails db:migrate
- bundle exec rspec
artifacts:
paths:
- coverage/
The service container postgres:16-alpine starts before your job script executes. GitLab CI provides a hostname matching the service name (in this case postgres) that your job container can use to connect. All environment variables defined in the job are passed to the service container as well, allowing configuration of the database credentials.
Multiple Service Containers
You can define multiple services for complex integration scenarios:
full_integration_test:
stage: test
image: python:3.11
services:
- postgres:16-alpine
- redis:7-alpine
- mongo:7
variables:
POSTGRES_DB: app_db
POSTGRES_USER: tester
POSTGRES_PASSWORD: securepass
REDIS_HOST: redis
MONGO_HOST: mongo
script:
- pip install -r requirements.txt
- python -m pytest integration/
Each service gets its own hostname derived from the image name, and all run concurrently with your job container on the same Docker network.
Best Practices for Docker in GitLab CI
1. Use Specific Image Tags and Digest Pinning
Never rely on latest tags in CI pipelines. Floating tags introduce non-determinism—your pipeline might behave differently from one day to the next as upstream images change. Instead, pin to specific version tags or, for maximum reproducibility, use image digests:
# Good: specific version tag
image: node:20.11.1-alpine
# Better: digest pinning for absolute reproducibility
image: node@sha256:a1b2c3d4e5f6...
2. Build Slim CI-Optimized Images
If your pipeline frequently pulls large images, build custom images tailored for CI use. Remove unnecessary packages, use Alpine-based variants when possible, and pre-install commonly used dependencies to reduce job boot time:
# Example Dockerfile for a CI-optimized image
FROM alpine:3.19
RUN apk add --no-cache \
bash \
curl \
git \
openssh-client \
make \
python3 \
py3-pip
# Pre-install common CI dependencies
RUN pip3 install --no-cache-dir \
pytest \
coverage \
requests
Push this custom image to your registry and reference it in your .gitlab-ci.yml. The time saved on each job run compounds significantly across hundreds of pipeline executions.
3. Leverage Caching Effectively
Docker layer caching during image builds can dramatically speed up pipelines. Use GitLab CI caching for dependency files and leverage Docker's build cache:
# Cache Node.js dependencies between jobs
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .npm/
# For Docker builds, use buildx cache mounts
docker_build:
stage: build
image: docker:24-dind
services:
- docker:24-dind
script:
- docker buildx build
--cache-from type=registry,ref=$CI_REGISTRY_IMAGE:cache
--cache-to type=registry,ref=$CI_REGISTRY_IMAGE:cache,mode=max
--tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
4. Separate Build and Runtime Images
Use multi-stage Docker builds to keep your production images minimal. Install build dependencies in a builder stage, compile your application, and copy only the necessary artifacts into a slim runtime image:
# Multi-stage Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runtime
RUN addgroup -S app && adduser -S app -G app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json ./
USER app
CMD ["node", "dist/main.js"]
5. Use GitLab CI Variables for Secrets Management
Never hardcode credentials in your .gitlab-ci.yml or Dockerfiles. Use GitLab CI variables with appropriate protection levels:
deploy_to_production:
stage: deploy
image: docker:24-dind
services:
- docker:24-dind
script:
- docker login -u $DOCKER_HUB_USER -p $DOCKER_HUB_TOKEN
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA my-app:production
- docker push my-app:production
only:
- main
Set variables like DOCKER_HUB_TOKEN as masked and protected in your GitLab project settings to prevent exposure in logs and restrict access to protected branches.
6. Implement Image Scanning and Security Checks
Integrate container vulnerability scanning into your pipeline. GitLab provides built-in container scanning that you can include:
include:
- template: Jobs/Container-Scanning.gitlab-ci.yml
container_scanning:
stage: test
variables:
GITLAB_SECURITY_SCANNER_IMAGE: registry.gitlab.com/security-products/container-scanning:latest
dependencies:
- build_image
This ensures that every image built in your pipeline is automatically scanned for known vulnerabilities before promotion to production.
7. Optimize Docker Layer Ordering
Structure your Dockerfile so that frequently changing layers come last. This maximizes cache hits during CI builds:
# Optimal layer ordering
FROM python:3.11-alpine
# Layer 1: System dependencies (rarely change)
RUN apk add --no-cache postgresql-dev gcc musl-dev
WORKDIR /app
# Layer 2: Dependency files (change occasionally)
COPY requirements.txt .
RUN pip install -r requirements.txt
# Layer 3: Application code (changes frequently)
COPY . .
8. Clean Up After Builds
Docker-in-Docker jobs can leave behind dangling images and containers that consume disk space on Runners. Include cleanup steps to prevent Runner host disk exhaustion:
docker_build:
stage: build
image: docker:24-dind
services:
- docker:24-dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
after_script:
- docker rmi $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA || true
- docker system prune -f --volumes || true
Note that after_script runs in a new shell and may not have access to the Docker daemon in the same way. For dind setups, ensure the cleanup commands target the correct daemon.
9. Use Parallel Matrix Builds for Multi-Architecture Images
GitLab CI supports parallel matrix jobs that can build Docker images for multiple architectures simultaneously:
build_multi_arch:
stage: build
image: docker:24-dind
services:
- docker:24-dind
parallel:
matrix:
- ARCH: amd64
PLATFORM: linux/amd64
- ARCH: arm64
PLATFORM: linux/arm64
script:
- docker buildx build
--platform $PLATFORM
--tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA-$ARCH
--push .
tags:
- docker
10. Version Your CI Images Alongside Application Code
Treat CI environment images as part of your codebase. Store CI-specific Dockerfiles in your repository and rebuild them on scheduled pipelines to pick up security patches:
# .gitlab-ci.yml scheduled job
rebuild_ci_image:
stage: maintenance
image: docker:24-dind
services:
- docker:24-dind
script:
- docker build -f ci.Dockerfile -t $CI_REGISTRY_IMAGE/ci-runner:latest .
- docker push $CI_REGISTRY_IMAGE/ci-runner:latest
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"
changes:
- ci.Dockerfile'
Common Pitfalls and How to Avoid Them
Pitfall 1: "Cannot Connect to Docker Daemon" Errors
This is the most frequent issue encountered when setting up Docker-in-Docker. It typically occurs because the Docker client cannot reach the daemon.
Root Cause: Missing or misconfigured DOCKER_HOST variable, TLS certificate issues, or using the wrong Docker image.
Solution: Always use the docker:dind service and set the TLS certificate directory variable:
variables:
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_TLS_VERIFY: "1"
DOCKER_CERT_PATH: "/certs/client"
Alternatively, for non-TLS setups (less secure, suitable only for isolated test environments):
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
Pitfall 2: Disk Space Exhaustion on Runners
Docker images accumulate rapidly in CI pipelines, especially with dind where each job pulls and builds images. Runner hosts can run out of disk space, causing pipelines to fail mysteriously.
Solution: Implement a combination of strategies:
- Schedule regular
docker system prunevia cron on Runner hosts - Use
--rmflags ondocker runcommands to auto-remove containers - Configure GitLab Runner's
cachedockercleanup policies - Monitor disk usage with Runner metrics
# In Runner config.toml
[[runners]]
executor = "docker"
[runners.docker]
cleanup_policy = "docker system prune -f --volumes"
cleanup_interval = "1h"
Pitfall 3: Shared Docker Socket Security Vulnerabilities
Mounting /var/run/docker.sock into a CI container gives that container root-equivalent access to the host. A malicious pipeline (or compromised dependency) can escape the container, access other containers, or compromise the host.
Solution: Avoid socket mounting entirely. Use dind services instead. If you absolutely must use socket binding, run Runners with unprivileged user namespaces or use tools like sysbox or rootless Docker.
Pitfall 4: Race Conditions with Concurrent dind Jobs
When multiple dind jobs run concurrently, they may attempt to use the same container names, network names, or volume names, causing conflicts.
Solution: Always use unique identifiers in resource names. Incorporate CI variables like CI_JOB_ID or CI_COMMIT_SHORT_SHA:
script:
- docker build -t app-test-$CI_JOB_ID .
- docker run --name test-runner-$CI_JOB_ID app-test-$CI_JOB_ID npm test
- docker rm test-runner-$CI_JOB_ID
Pitfall 5: Inconsistent Environment Between CI and Local Development
Developers often use different Docker images locally than what the CI pipeline uses, leading to "works on my machine" issues despite using containers.
Solution: Publish and share CI images with the development team. Use docker-compose files that reference the exact same images used in CI. Consider using GitPod or VS Code Dev Containers with the CI image as the base.
Pitfall 6: Large Image Pull Times Killing Pipeline Speed
Pulling a 2GB Docker image before every job can add minutes to your pipeline. This is especially painful in monorepos with many parallel jobs.
Solution: Use slim images, implement image layer caching on Runners, and consider using GitLab's distributed caching or a local registry mirror on the Runner host:
# Configure a local registry mirror in Runner's config.toml
[[runners]]
[runners.docker]
registry_mirrors = ["http://local-mirror:5000"]
Pitfall 7: Secrets Leakage in Docker Build History
Secrets passed as build arguments (--build-arg) are embedded in image layers and can be extracted from the final image. Similarly, copying .env files into images bakes secrets into layers.
Solution: Use Docker BuildKit secrets mounting for sensitive data during builds:
# Dockerfile with BuildKit secrets
# syntax=docker/dockerfile:1
FROM node:20-alpine
RUN --mount=type=secret,id=npm_token \
export NPM_TOKEN=$(cat /run/secrets/npm_token) && \
npm ci
# In .gitlab-ci.yml
docker_build:
script:
- docker buildx build
--secret id=npm_token,env=NPM_TOKEN
-t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
This ensures secrets are mounted only during the build process and never persisted in image layers.
Pitfall 8: Ignoring Container Exit Codes
Docker commands in scripts that fail silently—for example, a test container that exits with code 1 but the docker run command isn't checked—can lead to passing pipelines despite actual test failures.
Solution: Always propagate container exit codes. Use set -e at the top of scripts or check exit codes explicitly:
script:
- set -e
- docker run --rm test-image pytest
- echo "Tests passed" # Only reaches here if exit code was 0
Pitfall 9: Network Conflicts Between Services and Jobs
Service containers and job containers communicate via Docker networking. If you misconfigure service hostnames or expose incorrect ports, your job fails with connection refused errors.
Solution: Service hostnames are derived from the image name (everything before the colon, with slashes replaced by underscores). Always test connectivity in your scripts:
script:
- |
echo "Waiting for PostgreSQL..."
for i in $(seq 1 30); do
if pg_isready -h postgres -p 5432; then
echo "PostgreSQL is ready"
break
fi
sleep 1
done
Pitfall 10: Not Handling Docker Rate Limits
Docker Hub imposes pull rate limits on anonymous and free authenticated users. CI pipelines that pull frequently from Docker Hub can hit these limits, causing jobs to fail with "toomanyrequests" errors.
Solution: Use GitLab's Container Registry as your primary image source. Mirror frequently used images to your own registry. Authenticate with Docker Hub using a paid account token for higher limits:
# Pull from GitLab registry instead of Docker Hub
image: registry.gitlab.com/my-group/my-project/ci-image:latest
Advanced Patterns and Real-World Examples
Pattern: Full CI/CD Pipeline with Docker
Here is a complete pipeline example that demonstrates build, test, security scan, and deployment stages using Docker:
stages:
- build
- test
- security
- deploy-staging
- deploy-production
variables:
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_HOST: tcp://docker:2376
# Build stage
build:
stage: build
image: docker:24-dind
services:
- docker:24-dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
tags:
- docker
# Unit tests inside built image
unit_test:
stage: test
image: docker:24-dind
services:
- docker:24-dind
script:
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- docker run --rm $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA npm test
needs:
- build
tags:
- docker
# Integration tests with services
integration_test:
stage: test
image: docker:24-dind
services:
- docker:24-dind
- postgres:16-alpine
variables:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
script:
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- |
docker run --rm \
--network host \
-e DB_HOST=postgres \
-e DB_USER=testuser \
-e DB_PASS=testpass \
-e DB_NAME=testdb \
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA npm run test:integration
needs:
- build
tags:
- docker
# Security scanning
container_scanning:
stage: security
image: docker:24-dind
services:
- docker:24-dind
script:
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- docker run --rm aquasec/trivy image $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
needs:
- build
tags:
- docker
# Deploy to staging
deploy_staging:
stage: deploy-staging
image: docker:24-dind
services:
- docker:24-dind
script:
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA $CI_REGISTRY_IMAGE:staging
- docker push $CI_REGISTRY_IMAGE:staging
# Trigger deployment webhook or Kubernetes rollout
- curl -X POST $STAGING_DEPLOY_WEBHOOK
environment:
name: staging
needs:
- unit_test
- integration_test
- container_scanning
only:
- main
tags:
- docker
# Deploy to production
deploy_production:
stage: deploy-production
image: docker:24-dind
services:
- docker:24-dind
script:
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA $CI_REGISTRY_IMAGE:production
- docker push $CI_REGISTRY_IMAGE:production
environment:
name: production
needs:
- deploy_staging
when: manual
only:
- main
tags:
- docker
Pattern: Docker Compose in CI for Complex Integration Tests
For applications with multiple interdependent services, you can use Docker Compose within your CI pipeline:
# docker-compose.ci.yml
version: '3.8'
services:
app:
image: ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
environment:
DATABASE_URL: postgresql://user:pass@db:5432/app
REDIS_URL: redis://redis:6379
command: ["pytest", "integration/"]
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: app
healthcheck:
test: ["CMD", "pg_isready", "-U", "user"]
interval: 2s
retries: 10
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 2s
retries: 10
# GitLab CI job using docker-compose
integration_compose:
stage: test
image: docker:24-dind
services:
- docker:24-dind
script:
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- docker compose -f docker-compose.ci.yml up --abort-on-container-exit --exit-code-from app
- docker compose -f docker-compose.ci.yml down --volumes
needs:
- build
tags:
- docker
This pattern ensures all services start with proper health checks before tests execute, closely mirroring production-like conditions.
Pattern: Kaniko for Rootless Image Building
When you cannot use privileged containers (for example, on Kubernetes clusters with strict security policies), Kaniko provides a way to build Docker images without a Docker daemon:
kaniko_build:
stage: build
image: gcr.io/kaniko-project/executor:debug
script:
- |
/kaniko/executor
--context $CI_PROJECT_DIR
--dockerfile $CI_PROJECT_DIR/Dockerfile
--destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
--cache=true
--cache-repo=$CI_REGISTRY_IMAGE/cache
tags:
- kubernetes
Kaniko builds images directly in user space without requiring a Docker daemon, making it ideal for environments where dind is prohibited.
Conclusion
Docker and GitLab CI together form a powerful foundation for modern software delivery pipelines. By containerizing your CI environments, you achieve reproducibility, simplify dependency management, and create a unified artifact—the container image—that flows from build to production. The key to success lies in following established best practices: pin image versions, use multi-stage builds, implement proper secret management, leverage caching, and always prefer Docker-in-Docker services over socket mounting for security. Be vigilant about the common pitfalls—daemon connectivity issues, disk space exhaustion, race conditions, and rate limits—as these can silently erode pipeline reliability. When implemented thoughtfully, Docker-based GitLab CI pipelines provide a robust, scalable, and maintainable framework that scales from small personal projects to enterprise-grade deployments with hundreds of daily pipeline executions. The patterns and examples covered in this tutorial give you a solid starting point; adapt them to your specific context, invest in optimizing your CI images, and your pipelines will reward you with speed, reliability, and confidence in every deployment.