“Det virker på min maskine” (og sådan stopper du med at sige det)

“Det virker på min maskine” (og sådan stopper du med at sige det)

De fleste tror, at “det virker på min maskine” handler om én udviklers rodede laptop. Det gør det ikke.

Det handler om, at teamet aldrig har aftalt, hvad det vil sige, at noget virker. Hvilken Node-version. Hvilken Python. Hvilken database. Hvilke scripts. Og hvornår man egentlig må klappe sig selv på skulderen og sige: “Nu kører det lokalt”.

I stedet for at skyde skylden på kollegaens MacBook, får du her en standard du kan kopiere, tilpasse og bruge til næste projekt. Den er tænkt til Node/Python + DB, både med og uden Docker.

1. Hvad “virker på min maskine” i virkeligheden dækker over

Udtrykket lyder uskyldigt, men det dækker næsten altid over de samme fire kategorier af forskelle mellem maskiner:

  1. Versioner (Node, Python, npm/pnpm, pip, OS-specifikke ting)
  2. Commands (forskellige scripts, forskellige måder at starte ting på)
  3. State (databasen ser forskellig ud, migrations er halve, filer mangler)
  4. Konfiguration (env-vars, .env filer, secrets, porte osv.)

Hvis du vil have et team-setup der holder, er planen derfor enkel:

  • Standardiser versioner
  • Standardiser commands
  • Gør state reproducible
  • Gør konfiguration synlig og delbar

Resten af artiklen er i praksis bare at gå de fire ting igennem én for én og gøre dem kedelige og forudsigelige. (Det er et kompliment i udviklerverden.)

2. Standardiser versioner: Node, Python og package managers

Hvis alle kører “noget med Node 18 eller 20” og “en eller anden pip”, så har du opskriften på små, irriterende fejl.

Mit mål i et projekt er altid:

  • Én Node-version, låst og dokumenteret
  • Én Python-version, låst og dokumenteret
  • Én måde at installere dependencies på, styret af lockfiler

2.1 Node: nvm + .nvmrc + lockfile

Brug nvm (Node Version Manager) eller tilsvarende til at sikre, at alle er på samme Node-version.

Eksempel på .nvmrc i projektroden:

18.19.0

Og i din README, meget tydeligt:

Node: 18.19.0 (defineret i .nvmrc)
Package manager: npm (brug "npm ci" ikke "npm install")

Hvis du vil være endnu mere eksplicit, kan du også have en engines-sektion i package.json:

{
  "engines": {
    "node": "18.19.0",
    "npm": ">=10"
  }
}

Lockfile: vælg én type og stå ved det. For Node er det typisk:

  • package-lock.json hvis du bruger npm
  • pnpm-lock.yaml hvis du bruger pnpm

Og ja: så skal alle bruge samme package manager. Det er ikke “valgfrit humørvalg”.

2.2 Python: venv + version i fil

I Python-land vil du have:

  • Fast Python-version (f.eks. 3.11)
  • Virtuelt miljø pr. projekt
  • En fil, der beskriver dependencies reproducerbart

Hvis du ikke er vant til venv endnu, så læs fx den her artikel om at tæmme Python-kaos med venv. Men den korte version i dit projekt kunne være:

# python-version (hvis du bruger pyenv eller lign.)
3.11.7

# Setup i README
python -m venv .venv
source .venv/bin/activate  # Mac/Linux
.venvScriptsactivate     # Windows

pip install -r requirements.txt

Hvis du vil have bedre reproducibility, kan du bruge værktøjer som pip-tools til at generere en låst requirements.txt. Det vigtigste er, at nye folk ikke bare kører pip install <ting> på må og få.

2.3 Mini-øvelse: gør versioner synlige

Gå dit nuværende projekt igennem og tjek:

  • Er Node-version tydeligt angivet (fil + README)?
  • Er Python-version tydeligt angivet (fil + README)?
  • Er der præcis én lockfile pr. eco-system?

Hvis ikke, så er dit første “det virker på min maskine”-fix bare at få det på plads.

3. Standardiser commands: én måde at gøre ting på

Den næste klassiker: alle kører forskellige kommandoer for at starte og teste projektet.

Nogle eksempler fra virkeligheden:

  • Du kører npm run dev
  • Din kollega kører node server.js
  • CI kører noget helt tredje

Løsning: aftal et lille sæt “standard-commands” som alle projekter i teamet følger.

3.1 Et fælles sæt commands

Jeg plejer at sigte efter de her som minimum:

  • dev – start udviklingsserver lokalt
  • test – kør alle tests én gang
  • lint – kør lint/format
  • build – byg produktion (hvis relevant)
  • migrate – kør database migrations
  • seed – load testdata

3.2 Node-eksempel med npm scripts

I package.json kunne det se sådan her ud:

{
  "scripts": {
    "dev": "node src/server-dev.js",
    "test": "node --test",
    "lint": "eslint src",
    "build": "tsc -p .",
    "migrate": "node scripts/migrate.js",
    "seed": "node scripts/seed.js"
  }
}

Og README:

# Install
npm ci

# Første run
npm run migrate
npm run seed
npm run dev

Pointen er, at det altid er scriptsene der er “kontrakten”, ikke random shell-kommandoer i folks hoveder.

3.3 Python-eksempel med Makefile

I Python-projekter giver en lille Makefile ofte god mening:

PYTHON=python
VENV=.venv

setup:
	$(PYTHON) -m venv $(VENV)
	$(VENV)/bin/pip install -r requirements.txt

dev:
	$(VENV)/bin/python app.py

test:
	$(VENV)/bin/pytest

migrate:
	$(VENV)/bin/alembic upgrade head

seed:
	$(VENV)/bin/python scripts/seed.py

Så kan folk nøjes med at lære:

make setup
make migrate
make seed
make dev

3.4 Mini-øvelse: ryd op i dine scripts

Åbn dit projekt og tjek:

  • Har du mere end én måde at starte dev-server på? Vælg én.
  • Ligger migrations/seed bag velnavngivne scripts, eller er de gemt i README som lange kommandoer?
  • Matcher de commands, CI bruger, dem du selv bruger lokalt?

Du kan hente lidt inspiration fra fx den her historie om rådne npm-scripts, hvis du vil nudges til at være lidt ærlig om dine egne scripts.

4. Database lokalt: Docker Compose eller lokal install

Her opstår mange “virker på min maskine”-scenarier: en kører Postgres 14, en anden 16, en tredje har ikke installeret noget som helst.

Der er to realistiske modeller:

  • Lokal install af din database
  • Docker Compose til at køre databasen (og evt. flere services)

4.1 Lokal database: simpelt, men kræver disciplin

Hvis du vælger “alle har Postgres/MySQL/SQLite lokalt installeret”-modellen, så gør det tydeligt:

# README
Database: PostgreSQL 15

Lokalt:
- Opret database "myapp_dev"
- Opret bruger "myapp" med password "devpassword"
- Giv brugeren alle rettigheder til "myapp_dev"

Kør derefter:
- npm run migrate
- npm run seed

Det er ikke fancy, men det er gennemskueligt. Til mindre backend-projekter eller studieprojekter er det ofte nok.

4.2 Docker Compose: samme database, samme version for alle

Hvis teamet er komfortabel med Docker, er Compose en god måde at fjerne “det er nok min lokale DB”-fejl.

Eksempel på en simpel docker-compose.yml til Postgres:

version: "3.9"
services:
  db:
    image: postgres:15
    environment:
      POSTGRES_USER: myapp
      POSTGRES_PASSWORD: devpassword
      POSTGRES_DB: myapp_dev
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

README-flowet bliver så:

docker compose up -d db
npm run migrate
npm run seed
npm run dev

Hvis du har flere services (f.eks. backend + worker + db), kan du tilføje dem løbende. Start simpelt og lad det vokse.

4.3 Migrations + seed: gør state gentagelig

Uanset lokal DB eller Docker: du vil kunne genskabe en “ren” database-tilstand på kommando.

Minimum:

  • Migrations du kan køre fra 0 til nu
  • Seed-script der fylder udviklingsdata i
  • Én kommando til hver

Eksempel i Node med et simpelt migrerings-script (pseudo):

// scripts/migrate.js
import { migrate } from "./db/migrate.js";

migrate()
  .then(() => {
    console.log("Migrations done");
    process.exit(0);
  })
  .catch((err) => {
    console.error(err);
    process.exit(1);
  });

Seed-scriptet kan f.eks. tømme tabeller og indsætte nogle fornuftige test-brugere, nogle ordrer, lidt random data.

Du vil kunne sige til en ny udvikler: “Hvis din DB er mærkelig, så kør bare:

npm run migrate
npm run seed

Så lander de samme sted som resten.

4.4 Typisk fejl: skjult state

Den mest irriterende kategori her er “min DB er fuld af gammel testdata, men jeg ved ikke hvordan jeg starter forfra”.

Hvis du vil undgå den, så overvej ekstra scripts som:

  • reset-db – drop, opret, migrate, seed i ét hug

For eksempel i en Makefile:

reset-db:
	$(VENV)/bin/python scripts/drop_db.py
	$(VENV)/bin/python scripts/create_db.py
	make migrate
	make seed

5. Miljøvariabler: .env.example og sikre defaults

Halvdelen af “det virker ikke hos mig”-fejl handler om env-vars. Der mangler en API-nøgle. En URL er forkert. En port er taget.

Så: stop med at lade folk gætte. Gør miljøvariabler til en del af projektets kontrakt.

5.1 .env.example som sandheden for lokale settings

Opret en .env.example i projektroden:

# Server
PORT=3000
NODE_ENV=development

# Database
DB_HOST=localhost
DB_PORT=5432
DB_USER=myapp
DB_PASSWORD=devpassword
DB_NAME=myapp_dev

# Eksterne APIer
PAYMENT_API_KEY=changeme

README:

cp .env.example .env
# Tilpas hvis nødvendigt

Og så selvfølgelig: .env skal aldrig committes. Ignorer den i .gitignore.

5.2 Safe defaults i koden

Når du læser env-vars ind, så giv fornuftige defaults i udviklingsmiljø:

// config.js
import "dotenv/config";

export const config = {
  port: process.env.PORT || 3000,
  db: {
    host: process.env.DB_HOST || "localhost",
    port: Number(process.env.DB_PORT || 5432),
    user: process.env.DB_USER || "myapp",
    password: process.env.DB_PASSWORD || "devpassword",
    name: process.env.DB_NAME || "myapp_dev",
  },
};

Det gør det nemmere at komme i gang, men stadig tydeligt hvor tingene kommer fra. Hvis du vil nørde mere env-vars og sikkerhed, er der en fin intro i artiklen om at gemme API-nøgler det rigtige sted.

5.3 Mini-øvelse: lav din .env.example ærlig

Åbn din .env.example (eller lav en). Spørg dig selv:

  • Står alle env-vars, appen faktisk bruger, der?
  • Er der noget, der kun står i din egen .env eller i dit hoved?
  • Kan en ny udvikler få appen til at køre med et rent checkout + .env.example?

6. CI som sandhed: få det til at fejle hurtigt

Hvis du vil slippe for “det virker lokalt”-diskussioner, skal I have én autoritet der bestemmer: CI.

Pointen er simpel: de commands, CI kører, er de samme som udviklerne kører lokalt, og hvis noget kun virker lokalt på din maskine, så er det din maskine, der er forkert, ikke projektet.

6.1 CI workflow som reference

Et minimalt Node CI-setup kunne f.eks. være:

name: CI

on: [push, pull_request]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
      - run: npm ci
      - run: npm run lint
      - run: npm test

Bemærk:

  • CI læser Node-version fra .nvmrc (samme som lokalt)
  • CI bruger npm ci (samme kommando, du skal bruge lokalt)
  • CI kører de samme scripts, du har standardiseret: lint, test

Det er næsten det samme princip som den artikel om små YAML-filer der rydder dit JS-kaos op: gør din automatik så gennemsigtig, at du kan efterligne den lokalt.

6.2 Database i CI

Til integrationstests med database har du to muligheder:

  • Spin en database op i CI (ofte med Docker)
  • Mock/stub og nøjes med unit tests (til mindre projekter)

Hvis du bruger Docker Compose lokalt, er det oplagt at gøre det samme i CI:

- run: docker compose up -d db
- run: npm run migrate
- run: npm test

Nu er standarden klar: hvis du kan køre det samme lokalt uden fejl, så er du “aligned” med CI.

7. Onboarding-tjekliste og “first successful run”

Det sidste skridt er det, de fleste projekter springer over: en eksplicit definition af, hvad det vil sige, at en ny udvikler er sat op.

Jeg arbejder altid med to ting:

  • En onboarding-tjekliste
  • En definition af “første vellykkede run”

7.1 Onboarding-tjekliste

Lav en ONBOARDING.md eller en sektion i README med noget i den her stil:

## Onboarding tjekliste

[ ] Installér Node (se .nvmrc) og npm
[ ] Installér Python 3.11
[ ] Klon repoet
[ ] Opret og aktiver Python venv
[ ] Installer Node dependencies (npm ci)
[ ] Installer Python dependencies (pip install -r requirements.txt)
[ ] Kopiér .env.example til .env
[ ] Start database (lokal Postgres eller docker compose up -d db)
[ ] Kør database migrations (npm run migrate / make migrate)
[ ] Seed data (npm run seed / make seed)
[ ] Start appen (npm run dev / make dev)
[ ] Ram http://localhost:3000/health og få "OK"

Din tjekliste skal være så konkret, at en ny udvikler kan gå den igennem uden at spørge i Slack hvert tredje minut.

7.2 Definér “first successful run”

Definér meget tydeligt, hvad målet er for en ny udviklers første session. Eksempel:

  • Appen kører lokalt på http://localhost:3000
  • Health endpoint returnerer 200 OK
  • Login med brugeren test@example.com / password virker

Så kan du i onboarding-dokumentet skrive noget i retning af:

Første mål:
Du er færdig med onboarding, når du kan:
- Starte appen lokalt
- Logge ind som testbruger
- Oprette en ny "Todo" og se den i listen

Det føles banalt. Men det gør en kæmpe forskel, både for nye folk og for dig selv om seks måneder.

7.3 Brug nye udviklere som “sandhedstest”

Når en ny udvikler starter, så giv dem en opgave: “Skriv ned, hver gang du støder på et step, der ikke var forklaret.”

Opdater din onboarding efterfølgende. Det er næsten samme tankegang som i at skrive en README, der faktisk bliver læst: dokumentation er først god, når nye øjne kan følge den uden at gætte.

8. Din lille standard: sådan kan du samle det hele

For at samle trådene, kan du lave en mini-standard for dit team eller dine egne fremtidige projekter.

Eksempel på struktur i repoet:

  • .nvmrc (Node-version)
  • python-version eller .python-version
  • package.json med standard-scripts
  • Makefile (hvis du har Python-del)
  • docker-compose.yml (valgfrit, men rart til DB)
  • .env.example (env-vars kontrakt)
  • README.md (hurtig start)
  • ONBOARDING.md (tjekliste og first run)

Og som menneskelig regel:

  • Hvis du manuelt kører en shell-kommando mere end én gang, så overvej at pakke den ind i et script
  • Hvis du forklarer noget om setup i en Slack-tråd, så flyt det til README/ONBOARDING bagefter
  • Hvis noget kun virker hos dig, så antag, at det er din maskine, der afviger, ikke de andres

9. Sådan kan du bygge videre herfra

Når du først har en stabil lokal dev-standard, kan du begynde at lege med alt det sjove: flere services, bedre test-strategi, automatiseret deployment, finere full stack workflows osv.

Men jeg vil faktisk anbefale, at du gør noget endnu mere lavpraktisk først: tag dit nuværende projekt og se, hvor langt du kan skære onboarding-tiden ned, bare ved at rydde op i versioner, scripts, env-vars og DB-setup.

Hvis du kan gå fra “to halve dage og fem Slack-tråde” til “30 minutter med én README”, har du gjort mere for dit team end ved at introducere tre nye frameworks. Jeg synes det er en ret god byttehandel.

Brug migrations som eneste sandhed for skemaet og et versioneret seed-script til testdata. Til udvikling kan du tilbyde enten en init-sql eller en kopi af en anonymiseret dump, og automatiser opbygning via docker-compose eller et setup-script så alle får samme startstate.
Hold en .env.example med dokumenterede nøgle-navne i repoet og undlad at checke faktiske værdier ind. Brug en hemmelighedsmanager eller kryptering til rigtige secrets (fx Vault, cloud-secrets eller git-crypt) og sørg for at CI henter secrets fra sikre lagre, ikke fra repo.
Brug Docker når projektet har tunge native-depender, kræver en specifik OS-opsætning eller når du vil garantere 100% miljøparitet mellem dev og CI. Hvis projektet er simpelt kan nvm/venv være hurtigere at komme i gang med; lav begge workflows hvis nogle i teamet foretrækker containers.
Klon repo, læs README og .nvmrc eller python-version, kør projektets bootstrap-script (install dependencies, opret virtuel env eller start container), kør migrations og seed, start dev-server og udfør et simpelt smoke-test. Med disse trin kan nyudviklere bekræfte at 'det virker lokalt' uden ekstra hjælp.

Sara Vestergaard er selvlært kode-nørd, der stille og roligt er gået fra at rode med en enkelt HTML-side til at bygge små værktøjer, scripts og hjemmesider til sig selv og vennerne. Hun startede med at lave en simpel band-hjemmeside som teenager og opdagede, hvor tilfredsstillende det er, når noget, du har skrevet, pludselig lever på skærmen.

For Sara handler kodning ikke om store ord eller imponerende titler, men om meget konkrete problemer: den kedelige opgave, der tager for lang tid, den ven der mangler en lille porteføljeside, eller den liste, der burde sortere sig selv. Hun elsker at pille ting fra hinanden – også kode – for at se, hvad der egentlig foregår, og hun har brugt utallige aftener på at google fejlbeskeder, teste små eksempler og langsomt bygge sin forståelse op.

På Coding Class deler hun den tilgang videre. Hun skriver til dig, der gerne vil lære at kode ved at gøre det i praksis: små projekter, korte kodebidder og forklaringer, der hænger sammen med det, du faktisk sidder med på skærmen. Hun skærer ind til benet, viser typiske fejl og deres løsninger og giver altid et forslag til, hvordan du kan bygge en tand videre, når grundideen først virker.

Når hun ikke skriver til Coding Class eller nørkler med nye små projekter, hænger Sara på klatrevæggen, vander sine altanplanter eller spiller gamle Nintendo-spil. Men hun ender næsten altid tilbage ved tasterne – for der er altid endnu en lille ting, der kunne være smartere, hurtigere eller bare lidt sjovere at bruge.

Send kommentar

You May Have Missed