CI/CD Pipeline Optimization: Cutting Build Times in Half on Norwegian Infrastructure
There is nothing more soul-crushing than pushing a hotfix on a Friday afternoon and watching the pipeline spinner for 45 minutes. I’ve been there. You’ve been there. The tests are flaky, the npm install hangs on some obscure dependency fetching from a slow mirror, and the deployment script times out because the SSH handshake took too long.
In the high-stakes world of DevOps, latency isn't just a network metric; it's a productivity killer. If your developers are waiting 30 minutes for feedback, they switch contexts. Once they switch contexts, their flow is broken. You’ve lost them.
This isn't a generic "best practices" list. This is a breakdown of how we slashed build times for a major Oslo-based fintech client by 60%, strictly focusing on infrastructure bottlenecks, Docker layer strategies, and the often-ignored impact of storage I/O.
The Hidden Bottleneck: It’s Not Your Code, It’s Your Disk
Most people blame the language. "Java is slow," they say. Or "Webpack is bloated." While often true, the real culprit in a CI environment is usually Disk I/O.
Consider a standard Node.js project. npm install extracts thousands of small files. On a budget VPS with shared SATA storage or throttled SSDs, your IOPS (Input/Output Operations Per Second) hit the ceiling instantly. The CPU sits idle, waiting for the disk to write. This is "I/O Wait," and it is the silent killer of build performance.
Pro Tip: Runiostat -x 1on your CI runner during a build. If%iowaitexceeds 5-10%, your storage solution is inadequate. You need NVMe.
War Story: The "Monday Morning" Meltdown
Back in early 2020, we managed a Kubernetes cluster for a media agency. Every Monday morning, builds would fail with timeouts. We spent weeks debugging Webpack configurations. Turns out, their cloud provider had "noisy neighbors." Other VMs on the same physical host were hammering the disk, stealing IOPS from our CI runners.
We migrated the runners to CoolVDS instances backed by NVMe storage. The "Monday timeouts" vanished. Build consistency returned. Why? Because raw I/O throughput matters more than marketing buzzwords.
Optimization 1: Intelligent Docker Layer Caching
If you are rebuilding your entire dependency tree on every commit, you are doing it wrong. Docker caches layers based on the checksum of the files added.
The Wrong Way:
FROM node:14-alpine
WORKDIR /app
COPY . .
# This busts the cache every time ANY file changes
RUN npm install
CMD ["npm", "start"]
The Right Way (Multi-stage & Ordered):
FROM node:14-alpine AS builder
WORKDIR /app
# Copy only dependency definitions first
COPY package.json package-lock.json ./
# Install dependencies. This layer is cached unless package.json changes.
RUN npm ci --quiet
# Now copy source code
COPY . .
RUN npm run build
# Final lightweight stage
FROM nginx:1.21-alpine
COPY --from=builder /app/build /usr/share/nginx/html
By copying package.json separately, Docker reuses the `node_modules` layer across builds as long as dependencies haven't changed. This one change can save 5 minutes per build.
Optimization 2: Sovereign Runners & Network Latency
In 2021, with Schrems II invalidating the Privacy Shield, where you process data matters. If your CI/CD pipeline runs tests against a database containing pseudo-anonymized user data, sending that data to a runner hosted in a US-owned cloud region is a compliance risk.
Hosting your own GitLab Runners or Jenkins agents on a VPS in Norway solves two problems:
- Compliance: Data stays within the EEA/Norway jurisdiction (Datatilsynet is happy).
- Latency: If your git repo, your registry, and your staging servers are in Northern Europe, why route traffic through Frankfurt or London?
We benchmarked the latency impact of pushing a 2GB Docker image to a registry. The difference between a local Norwegian line and a generalized European cloud endpoint was staggering.
| Source Location | Target Registry (Oslo) | Latency | Upload Time (2GB) |
|---|---|---|---|
| US East (Public Cloud) | Oslo | ~95ms | 1m 45s |
| Central Europe | Oslo | ~35ms | 42s |
| CoolVDS (Oslo) | Oslo | <2ms | 12s |
Optimization 3: Fine-Tuning the Runner Configuration
Simply throwing hardware at the problem isn't enough; you must configure it. For GitLab CI, the concurrent setting in config.toml is crucial. If set too high on a single VPS, context switching kills performance.
Here is a battle-tested configuration for a CoolVDS instance with 4 vCPUs and 8GB RAM:
concurrent = 4
check_interval = 0
[[runners]]
name = "coolvds-nvme-runner-01"
url = "https://gitlab.com/"
token = "PROJECT_TOKEN"
executor = "docker"
[runners.custom_build_dir]
[runners.cache]
Type = "s3"
ServerAddress = "minio.internal:9000"
AccessKey = "minio"
SecretKey = "minio123"
BucketName = "runner-cache"
Insecure = true
[runners.docker]
tls_verify = false
image = "docker:20.10.8"
privileged = true
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
shm_size = 0
Note the use of /var/run/docker.sock binding. This allows the container to spawn sibling containers rather than using "Docker-in-Docker" (dind), which is significantly slower and has filesystem overlay issues. However, be aware of the security implications; this should only be done on trusted, isolated runners—another reason to use a dedicated VPS rather than shared runners.
The CoolVDS Advantage: Why Hardware Matters
We often treat compute resources as abstract commodities. They aren't. When you are compiling Rust or C++, or transpiling TypeScript, raw CPU clock speed and cache size dictate your speed. When you are unzipping artifacts, disk speed dictates your speed.
CoolVDS uses KVM virtualization. Unlike OpenVZ or LXC containers used by budget providers, KVM provides a dedicated kernel and better isolation. This ensures that when your pipeline demands 100% CPU for a compile job, you actually get it, without the hypervisor throttling you because the neighbor is mining crypto.
Implementing a Cleanup Strategy
Finally, self-hosted runners fill up with dangling images fast. A full disk causes pipelines to fail abruptly. Automate the cleanup. Add this simple cron job to your runner server:
#!/bin/bash
# /usr/local/bin/docker-cleanup.sh
# Remove unused containers
docker container prune -f
# Remove dangling images (layers not used by any tagged image)
docker image prune -f
# Remove unused volumes
docker volume prune -f
# Aggressive cleanup for build cache older than 48h
docker builder prune --filter "until=48h" -f
Set this to run nightly via crontab -e:
0 3 * * * /usr/local/bin/docker-cleanup.sh >> /var/log/docker-cleanup.log 2>&1
Conclusion
Optimizing CI/CD is about removing friction. It requires a holistic view: efficient Dockerfiles, smart caching, and—crucially—infrastructure that doesn't gasp for air when you put it under load.
If you are serious about DevOps in Norway, relying on overloaded overseas infrastructure is a strategic error. You need low latency, data sovereignty, and NVMe performance that can keep up with your code.
Don't let slow I/O kill your developer experience. Deploy a high-performance runner on a CoolVDS NVMe instance today and see your build times drop.