“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:
- Versioner (Node, Python, npm/pnpm, pip, OS-specifikke ting)
- Commands (forskellige scripts, forskellige måder at starte ting på)
- State (databasen ser forskellig ud, migrations er halve, filer mangler)
- 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.jsonhvis du bruger npmpnpm-lock.yamlhvis 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 lokalttest– kør alle tests én ganglint– kør lint/formatbuild– byg produktion (hvis relevant)migrate– kør database migrationsseed– 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/passwordvirker
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-versioneller.python-versionpackage.jsonmed standard-scriptsMakefile(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.









1 kommentar