Local vs CI-omgevingen: waar dingen misgaan en waarom

Begrijpen van lokale en CI-omgevingen
Wat is een lokale ontwikkelomgeving
Wanneer developers het hebben over hun lokale omgeving, bedoelen ze de setup op hun eigen machine waarin ze code schrijven, testen en experimenteren. Deze omgeving is vaak sterk gepersonaliseerd—soms bewust, soms onbewust. Je hebt bijvoorbeeld specifieke versies van Node.js, Python of Java geïnstalleerd, samen met globaal geïnstalleerde packages, gecachte dependencies en environment variables die zich in de loop van de tijd hebben opgebouwd.
Hier zit de uitdaging: lokale omgevingen zijn zelden schoon of reproduceerbaar. Je installeert iets één keer, vergeet het, en het blijft gewoon werken. Dat is geweldig voor productiviteit, maar problematisch voor consistentie. Het is alsof je kookt in je eigen keuken—je weet precies waar alles ligt en hebt alles naar je hand gezet. Maar als iemand anders jouw recept probeert te volgen in een andere keuken, kan het resultaat heel anders zijn.
Een ander belangrijk aspect is dat lokale omgevingen vaak impliciete aannames bevatten. Misschien heeft jouw systeem een bepaalde mapstructuur, of gaat je besturingssysteem anders om met line endings. Misschien draaien er achtergrondservices waar je applicatie ongemerkt van afhankelijk is. Deze verborgen afhankelijkheden staan niet in je code, maar beïnvloeden wel degelijk hoe je applicatie zich gedraagt.
Daarom zeggen developers vaak: “It works on my machine.” Het is geen excuus—het is een symptoom van omgevingsdrift. En tenzij je die drift actief beheert, wordt je lokale setup een unieke snowflake die niemand anders—ook je CI-systeem niet—kan reproduceren.
Wat is een CI-omgeving
Een Continuous Integration (CI)-omgeving is ontworpen als het tegenovergestelde van je lokale setup. In plaats van persoonlijk en voortdurend aangepast, is deze bedoeld om schoon, consistent en reproduceerbaar te zijn. Elke keer dat een CI-pipeline draait, start deze meestal vanaf nul—met een nieuwe omgeving waarin dependencies worden geïnstalleerd en builds en tests in een gecontroleerde setting worden uitgevoerd.
Zie CI als een steriel laboratorium. Niets wordt verondersteld, alles is expliciet. Als je project een dependency nodig heeft, moet die gedeclareerd zijn. Als er een environment variable nodig is, moet die geconfigureerd worden. Er is geen ruimte voor “het is er gewoon toevallig”.
CI-systemen zoals GitHub Actions, GitLab CI of Jenkins zijn bewust strikt. Ze leggen zwakke punten in je setup bloot door alle onzichtbare ondersteuning uit je lokale omgeving weg te nemen. Daarom falen builds vaak in CI, terwijl alles lokaal prima lijkt te werken.
Een ander belangrijk verschil is automatisering en schaal. CI-omgevingen draaien vaak meerdere jobs parallel, op verschillende machines en soms zelfs op verschillende besturingssystemen. Dit introduceert variabiliteit die je lokale machine niet kent. Het is alsof je je code onder stress test in omstandigheden waarvoor deze oorspronkelijk niet ontworpen was.
Kort gezegd functioneren CI-omgevingen als een reality check. Ze beantwoorden een cruciale vraag: “Werkt deze code overal, of alleen op jouw laptop?” En vaker dan developers verwachten, is het antwoord—tenminste in het begin—dat laatste.
Belangrijkste verschillen tussen lokale en CI-systemen
Verschillen in omgevingsconfiguratie
Een van de meest voorkomende oorzaken van failures tussen lokale en CI-omgevingen is een mismatch in configuratie. Op je lokale machine kunnen environment variables in je shellprofiel staan, terwijl je CI-pipeline afhankelijk is van expliciet gedefinieerde variabelen in configuratiebestanden of secrets management systemen.
Deze verschillen kunnen subtiel zijn. Je kunt bijvoorbeeld lokaal een standaard database-URL hebben ingesteld, terwijl die variabele in CI ontbreekt of naar een andere service verwijst. Plots falen je tests—niet omdat je code fout is, maar omdat de omgeving niet overeenkomt.
Een ander probleem zijn verschillen in toolversies. Misschien gebruik je lokaal Node.js 18, terwijl je CI-pipeline draait op Node.js 16. Zelfs kleine versieverschillen kunnen breaking changes veroorzaken, vooral in snel evoluerende ecosystemen. Dit geldt ook voor package managers, compilers en systeemlibraries.
Configuratiedrift ontstaat ook in de tijd. Terwijl je je lokale setup blijft updaten, kan je CI-configuratie achterblijven. Zonder regelmatige synchronisatie groeien deze omgevingen langzaam uit elkaar—tot failures onvermijdelijk worden.
Problemen met dependency management
Dependencies zijn een andere grote boosdoener. Lokaal kun je gecachte of globaal geïnstalleerde packages hebben waar je project onbedoeld op vertrouwt. In CI bestaan deze packages niet—tenzij ze expliciet worden geïnstalleerd.
Hier worden lockfiles cruciaal. Tools zoals package-lock.json, yarn.lock of poetry.lock zorgen ervoor dat exact dezelfde versies van dependencies overal worden geïnstalleerd. Zonder deze bestanden kan je lokale omgeving dependencies anders resolven dan CI, wat leidt tot inconsistent gedrag.
Daarnaast zijn er transitieve dependencies. Een package die je gebruikt, kan afhankelijk zijn van een andere package die een breaking change introduceert. Als jouw lokale cache nog een oudere versie bevat, lijkt alles te werken. Maar CI, die vanaf nul start, haalt de nieuwste versie binnen—en plots faalt je build.
Dependencyproblemen voelen vaak willekeurig, maar dat zijn ze niet. Ze ontstaan doordat omgevingen niet deterministisch zijn. Zolang je geen strikte versiecontrole afdwingt, blijven deze issues terugkomen.
Veelvoorkomende redenen waarom code lokaal werkt maar faalt in CI
Ontbrekende environment variables
Environment variables zijn als onzichtbare draden die je applicatie bij elkaar houden. Lokaal worden ze vaak één keer ingesteld en daarna vergeten. In CI moeten ze elke keer expliciet worden gedefinieerd.
Een ontbrekende API key, database-URL of feature flag kan ervoor zorgen dat tests falen of applicaties crashen. Wat dit lastig maakt, is dat foutmeldingen niet altijd duidelijk zijn. Je ziet misschien een algemene error, zonder direct te beseffen dat een variabele ontbreekt.
Dit komt vooral voor bij projecten die integreren met externe services. Lokaal gebruik je misschien een mock of een opgeslagen credential. In CI bestaat die setup niet—tenzij je deze expliciet nabootst.
De oplossing is in theorie simpel: maak alle afhankelijkheden expliciet. In de praktijk vraagt dit discipline. Elke variabele waar je applicatie afhankelijk van is, moet gedocumenteerd en correct geconfigureerd worden in je CI-pipeline.
Verschillen in het bestandssysteem
Bestandssystemen gedragen zich verschillend per besturingssysteem. Je lokale machine draait misschien op macOS of Windows, terwijl je CI-omgeving meestal Linux gebruikt. Deze verschillen kunnen subtiele bugs veroorzaken.
Bijvoorbeeld: macOS gebruikt standaard een niet-hoofdlettergevoelig bestandssysteem, terwijl Linux hoofdlettergevoelig is. Een bestand met de naam Config.js kan lokaal werken, zelfs als je het importeert als config.js. In CI zal dit verschil echter een fout veroorzaken.
Ook line endings vormen een veelvoorkomend probleem. Windows gebruikt CRLF, terwijl Unix-gebaseerde systemen LF gebruiken. Dit kan invloed hebben op scripts, tests en zelfs version control gedrag.
Daarnaast spelen permissies een rol. Een script dat lokaal werkt, kan falen in CI omdat het geen uitvoerrechten heeft. Dit soort issues worden vaak over het hoofd gezien, maar kunnen je pipeline volledig breken.
Verborgen factoren die CI-pipelines breken
Tijd, locale en OS-variaties
Sommige van de meest frustrerende CI-fouten komen voort uit factoren die bijna onzichtbaar zijn—totdat ze alles laten falen. Tijdzones, systeemlocale en gedrag van het besturingssysteem kunnen inconsistenties veroorzaken die moeilijk lokaal te reproduceren zijn, tenzij je precies weet waar je moet zoeken.
Laten we beginnen met tijd. Stel dat je tests datumlogica bevatten—bijvoorbeeld het valideren van vervaldatums of het sorteren van events. Op je lokale machine werkt alles, omdat je systeem op jouw tijdzone staat. Maar je CI-omgeving gebruikt vaak UTC. Plots falen tests die afhankelijk zijn van “huidige tijd”, zonder duidelijke reden. Dit is geen bug in je code—het is een verschil in aannames.
Locale voegt nog een extra laag complexiteit toe. De manier waarop getallen, valuta en datums worden weergegeven, kan verschillen per systeeminstelling. Een test die 1,000.50 verwacht, kan falen in een omgeving die 1.000,50 gebruikt. Dit zijn geen randgevallen—het zijn realistische verschillen die CI zichtbaar maakt doordat lokale defaults verdwijnen.
Besturingssysteemverschillen versterken dit effect. Zelfs iets simpels als het sorteren van strings kan anders werken door onderliggende libraries. Als je CI op Linux draait terwijl jij ontwikkelt op macOS of Windows, kunnen deze verschillen onverwacht naar boven komen.
De belangrijkste les hier is dat CI-omgevingen bewust neutraal zijn, terwijl lokale omgevingen persoonlijk en aangepast zijn. Als je code afhankelijk is van impliciet systeemgedrag, zal CI dat vroeg of laat blootleggen. De oplossing is niet om CI “aan te passen tot het werkt”, maar om je code expliciet te maken—voor tijdzones, locale-instellingen en OS-afhankelijkheden.
Parallelle uitvoering en race conditions
Een andere verborgen oorzaak van CI-failures is parallelisme. CI-systemen zijn ontworpen voor snelheid en voeren tests vaak gelijktijdig uit—over meerdere threads of containers. Lokaal draai je tests meestal sequentieel, vaak zonder dat je je daarvan bewust bent. Alleen dat verschil kan al een hele categorie bugs blootleggen.
Race conditions zijn hier de meest voorkomende oorzaak. Deze ontstaan wanneer meerdere processen tegelijkertijd toegang hebben tot gedeelde resources—zoals bestanden, databases of data in geheugen—zonder goede synchronisatie. Lokaal lijkt alles te werken, omdat acties in een voorspelbare volgorde plaatsvinden. In CI zorgt parallelle uitvoering voor onvoorspelbaarheid, en beginnen tests plotseling sporadisch te falen.
Dit zijn de lastigste fouten: flaky, inconsistent en moeilijk te debuggen. Je draait de pipeline opnieuw en alles werkt. Nog een keer—en het faalt weer. Het voelt willekeurig, maar dat is het niet—het is timing.
Een andere factor is resource contention. CI-omgevingen hebben vaak minder CPU en geheugen dan je lokale machine. Tests die afhankelijk zijn van performance of timing kunnen falen onder deze beperkingen. Bijvoorbeeld: een test die verwacht dat een response binnen 100 ms komt, kan falen omdat de CI-runner onder belasting staat.
De enige betrouwbare oplossing is het ontwerpen van tests die geïsoleerd, deterministisch en onafhankelijk van uitvoervolgorde zijn. Als tests state delen, zijn ze in CI een tikkende tijdbom.
Hoe je lokale en CI-omgevingen op één lijn brengt
Containers gebruiken voor consistentie
Als er één technologie is die fundamenteel heeft veranderd hoe developers omgaan met omgevingsconsistentie, dan zijn het containers—vooral Docker. Containers maken het mogelijk om je omgeving één keer te definiëren en overal identiek uit te voeren—lokaal, in CI en in productie.
Zie een container als een snapshot van alles wat je applicatie nodig heeft: het besturingssysteem, runtime, dependencies en configuratie. In plaats van afhankelijk te zijn van je lokale setup, verpakt je alles in een reproduceerbare eenheid. Dit elimineert het “works on my machine”-probleem vrijwel volledig.
Bijvoorbeeld: als je CI-pipeline een Docker-image gebruikt, kun je exact diezelfde image lokaal draaien. Plots is er geen verschil meer tussen omgevingen—ze zijn letterlijk identiek. Dit maakt debugging veel eenvoudiger, omdat je niet meer hoeft te raden waar het verschil zit.
Containers stimuleren ook betere practices. Je wordt gedwongen om dependencies expliciet te definiëren, versies strikt te beheren en verborgen aannames te vermijden. Op de lange termijn leidt dit tot robuustere en beter draagbare code.
Natuurlijk zijn containers geen magische oplossing. Ze brengen extra complexiteit met zich mee en vereisen een leercurve. Maar voor teams die regelmatig CI-problemen ervaren, is de trade-off vrijwel altijd de moeite waard.
Infrastructure as Code
Een andere krachtige aanpak is Infrastructure as Code (IaC). In plaats van omgevingen handmatig te configureren, definieer je ze via code—met tools zoals Terraform, Ansible of zelfs CI-configuratiebestanden.
Dit zorgt ervoor dat je omgevingen version-controlled, reproduceerbaar en transparant zijn. Als er iets verandert, kun je dat volgen, reviewen en indien nodig terugdraaien. Er is geen mysterie meer rond hoe je CI-omgeving is opgezet—alles ligt vast in code.
IaC helpt ook de kloof tussen development en operations te verkleinen. Developers kunnen precies zien hoe hun code draait in CI en productie, wat verrassingen vermindert. Het is alsof je werkt met een bouwtekening in plaats van te moeten raden hoe een gebouw in elkaar zit.
De combinatie van containers en IaC is bijzonder krachtig. Containers definiëren de applicatieomgeving, terwijl IaC de infrastructuur eromheen vastlegt. Samen zorgen ze ervoor dat consistentie niet achteraf wordt toegevoegd, maar vanaf het begin ingebouwd is.
Best practices voor betrouwbare builds
Deterministische builds
Een deterministische build is een build die altijd hetzelfde resultaat oplevert, ongeacht waar of wanneer deze wordt uitgevoerd. Dit is de gouden standaard voor betrouwbaarheid, maar het vereist discipline om dit te bereiken.
De eerste stap is het vastzetten van dependencies. Gebruik altijd lockfiles en vermijd “floating versions” zoals ^1.2.3. Hoewel flexibele versies handig lijken, introduceren ze onvoorspelbaarheid. Wat vandaag werkt, kan morgen breken zonder dat je code verandert.
Een andere belangrijke factor is het elimineren van externe variabiliteit. Als je build afhankelijk is van externe API’s, netwerkcondities of systeemtijd, wordt deze inherent instabiel. Gebruik waar mogelijk mocks, fixtures en gecontroleerde inputs.
Caching kan ook een tweesnijdend zwaard zijn. Het versnelt builds, maar kan problemen maskeren door verouderde artefacten te hergebruiken. Een build die alleen werkt dankzij cache, is niet echt deterministisch.
Uiteindelijk vereist het bouwen van deterministische systemen een andere mindset. Je schrijft niet alleen code—je ontwerpt een systeem dat onder alle omstandigheden consistent moet functioneren.
Logging en observability
Wanneer er toch iets misgaat—en dat zal gebeuren—zijn logging en observability je belangrijkste hulpmiddelen. CI-failures zijn vaak moeilijker te debuggen omdat je geen directe toegang hebt tot de omgeving. Je kunt niet zomaar “even rondkijken” zoals lokaal.
Daarom zijn gedetailleerde logs essentieel. Elke stap in je pipeline moet voldoende informatie geven om te begrijpen wat er is gebeurd en waarom. Stille failures of vage foutmeldingen maken van kleine problemen tijdrovende onderzoeken.
Observability gaat verder dan alleen logs. Metrics, traces en artifacts geven diepere inzichten in het gedrag van je pipeline. Denk aan testoutputs, screenshots of de status van het systeem op het moment van falen—dit helpt om issues lokaal te reproduceren.
Een goede vuistregel: als een failure optreedt, moet je deze kunnen analyseren zonder de pipeline meerdere keren opnieuw te draaien. Als dat niet lukt, is je observability onvoldoende.
Tools die de kloof overbruggen
Docker en dev containers
Docker is uitgegroeid tot dé standaard voor consistente omgevingen, maar wordt nog krachtiger in combinatie met development containers (dev containers). Hiermee kun je je ontwikkelomgeving definiëren in een configuratiebestand dat direct gebruikt kan worden in editors zoals VS Code.
Dit betekent dat elke developer in je team—en je CI-systeem—exact dezelfde setup gebruikt. Geen “works on my machine”-verschillen meer. Iedereen werkt letterlijk in dezelfde omgeving.
Dev containers maken onboarding ook veel eenvoudiger. In plaats van uren te besteden aan het installeren van dependencies, kunnen nieuwe developers direct aan de slag met een vooraf geconfigureerde omgeving.
CI-debuggingtools
Moderne CI-platformen bieden steeds meer tools die speciaal zijn ontworpen om failures te debuggen. Functionaliteiten zoals interactieve sessies, het downloaden van artifacts en het opnieuw draaien van pipelines met SSH-toegang maken het mogelijk om de omgeving direct te inspecteren.
Deze tools overbruggen de kloof tussen lokaal en CI-debuggen. In plaats van te gokken wat er misging, kun je de CI-omgeving in realtime verkennen.
Sommige platforms bieden zelfs de mogelijkheid om CI-runs lokaal te reproduceren, waardoor je het beste van beide werelden krijgt. Dit verandert CI van een “black box” in een systeem dat je daadwerkelijk kunt begrijpen en controleren.
Conclusie
De spanning tussen lokale en CI-omgevingen is geen fout in het systeem—het is een weerspiegeling van hoe softwareontwikkeling werkt. Lokale omgevingen zijn gericht op snelheid en flexibiliteit, terwijl CI-omgevingen consistentie en reproduceerbaarheid afdwingen. De frictie tussen deze twee werelden is waar de meeste problemen ontstaan.
Begrijpen waarom dingen stukgaan, is de eerste stap naar het oplossen ervan. Of het nu gaat om configuratieverschillen, dependency mismatches of verborgen systeemvariaties—elke failure geeft waardevolle inzichten in je setup. In plaats van CI te zien als een obstakel, helpt het om het te beschouwen als een vangnet dat garandeert dat je code ook buiten je eigen machine werkt.
Het overbruggen van deze kloof vereist bewuste inspanning: het gebruik van containers, het definiëren van infrastructuur als code, het afdwingen van deterministische builds en het verbeteren van observability. Deze practices lossen niet alleen CI-problemen op—ze maken je hele ontwikkelproces betrouwbaarder.
Uiteindelijk is het doel niet alleen om CI “groen” te krijgen. Het doel is om systemen te bouwen die zich voorspelbaar gedragen, ongeacht waar ze draaien.
ASD Team
The team behind ASD - Accelerated Software Development. We're passionate developers and DevOps enthusiasts building tools that help teams ship faster. Specialized in secure tunneling, infrastructure automation, and modern development workflows.