on this page
You clone a project, run npm install, run npm run dev, and it just works. Your editor indents the way the project expects. The linter yells at the same things everyone else’s linter yells at. You’re on the same Node version as the person who wrote it, without anyone telling you which version that was.
That isn’t luck. Somebody set up the dotfiles.
Open a fresh terminal in any real repo and run ls -a. That little -a flag means “show all,” including the files that are normally hidden, and suddenly a pile of names starting with a dot appears: .gitignore, .env.example, .editorconfig, .nvmrc. Those are dotfiles, and they are quietly doing most of the work of keeping the project sane.
What a dotfile actually is
A dotfile is just a file whose name starts with a dot, like .gitignore. There is nothing special about the file format. The dot is a naming convention that means “hide me from the default listing.”
This is genuinely old Unix history, and it’s a little funny. Decades ago, someone wrote the ls command so it would skip any file starting with . to hide a couple of system entries. People noticed the side effect and started naming their personal config files with a leading dot so they’d stay out of the way. The behavior stuck. That accidental shortcut is the entire reason ls -a exists today: it’s the “no really, show me everything” version.
So when you see a dot-prefixed file, read it as “configuration that doesn’t need to clutter the view.” These files hold the settings for your tools: which files Git should ignore, how your formatter behaves, which Node version the project wants. They’re hidden because you set them once and mostly forget them, not because they’re unimportant.
Why they’re the project’s save file
Think about a save file in a game. It captures the state of the world so that when you load it back up, everything is exactly where you left it: your inventory, your map, your progress. Without it, every session starts from zero.
Dotfiles do that for a codebase. They encode the rules of the project so that a fresh clone, on a machine that has never seen this code, behaves the way the project expects. The indent size, the lint rules, the ignored files, the Node version: all of it travels with the repo instead of living in the original author’s head.
Here’s the part beginners miss. A project without committed dotfiles only runs correctly on the laptop it was built on. People assume a fresh clone is a clean slate, when really everything that makes it work is invisible setup the author did by hand and never wrote down. The dotfiles are how you write it down.
The common ones, and whether you commit them
Most of the dotfiles you’ll meet fall into a handful of jobs: tell Git what to ignore, hold your local config, and pin the rules for shared tools. Let’s go through the ones you’ll actually see.
.gitignore is a list of files and folders Git should never track. The classic entries are node_modules (hundreds of megabytes of downloaded packages that anyone can reinstall from package.json, so there’s no reason to store them in the repo) and .env (your secrets, which must never go into Git). A typical one looks like this:
# .gitignore
node_modules
dist
.env
.env.local
.DS_Store
You commit .gitignore. The whole point is that everyone on the project ignores the same junk, so it has to be shared. We’ll come back to the ways this file goes wrong, because it’s the one that bites people.
.env holds your actual local secrets and config: the real database password, the real API key. You do not commit this. Ever. It belongs in .gitignore, which is exactly why .gitignore lists it. If .env is new to you, read Environment variables explained before you go further, because the rest of this section assumes you know why these values live outside your code.
.env.example is the committed companion to .env. Since the real .env is hidden from Git, a teammate who clones the repo has no idea which variables the app even needs. So you commit a template: the same variable names, but with safe placeholder values instead of real ones.
# .env.example: safe to commit, no real values
DATABASE_URL=postgres://localhost:5432/myapp
REDIS_URL=redis://localhost:6379
STRIPE_SECRET_KEY=sk_test_your_key_here
This is the project’s answer to “what do I need to set?” You commit it. A new contributor copies it to .env and fills in the blanks.
.npmrc is configuration for npm itself: which registry to pull packages from, whether to save exact versions, settings that should apply to everyone building the project. You commit the project-level config. The one rule: keep authentication tokens out of the committed file. An auth token in a committed .npmrc is the same mistake as a secret in .env, just wearing a different hat. Tokens go in your machine-level .npmrc in your home folder, never the one in the repo.
.prettierrc holds the rules for
{
"semi": true,
"singleQuote": false,
"trailingComma": "all"
}
You commit it, because the entire value of a formatter is that everyone’s code comes out identical. If each person ran their own settings, every save would reshuffle the file and your Git history would fill with pointless formatting churn.
.eslintrc (or the newer eslint.config.js) holds the rules for await, that sort of thing. Where Prettier argues about looks, ESLint argues about correctness. You commit it so the whole team is held to the same checks.
.editorconfig is the humble one, and it earns its keep. It sets basic whitespace rules: indent size, tabs versus spaces, whether files end with a final newline. The reason it exists separately from Prettier is reach. Nearly every editor reads .editorconfig, including the ones that aren’t VS Code and don’t have Prettier wired up. It’s the lowest common denominator that keeps a Vim user and a VS Code user from fighting over indentation.
# .editorconfig
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
Commit it.
.nvmrc is one line: the Node version the project wants, like 20.11.0. Tools like nvm use and instantly switch to the right Node. This matters more than it looks. Code that runs fine on Node 20 can break on Node 18, and “works on my machine” is very often just “I’m on a different Node than you.” Commit it so nobody has to guess.
.dockerignore tells Docker which files to skip when it copies your project into an image during a build. It looks a lot like .gitignore and serves a similar purpose: keep node_modules, .git, and .env out of the image so your builds stay small and you never bake secrets into a container. Commit it.
The whole table
Here’s the lot in one place. When in doubt, this is the cheat sheet.
| File | What it controls | Commit or keep local |
|---|---|---|
.gitignore | Files Git should never track | Commit |
.env | Your real local secrets and config | Keep local |
.env.example | Template of which variables exist | Commit |
.npmrc | npm registry and config (no tokens) | Commit (tokens stay local) |
.prettierrc | Prettier formatting rules | Commit |
.eslintrc / eslint.config.js | ESLint lint rules | Commit |
.editorconfig | Whitespace, indent, final newline | Commit |
.nvmrc | The Node version | Commit |
.dockerignore | What Docker’s COPY should skip | Commit |
The one rule that decides everything
You don’t have to memorize that table if you understand the rule underneath it.
Commit the files that define how the project should behave for everyone. Keep local only the files that hold secrets or are specific to your one machine.
That’s the whole thing. A formatter config, a lint config, the Node version, the ignore list: those describe the project, so they’re shared. Your actual API keys and anything with a token describe a private value that happens to live on your laptop, so they stay off Git. When you meet a new dotfile and aren’t sure, ask: does this say something true for the whole team, or does it hold a secret? The answer tells you which pile it goes in.
What this buys you: joining a project in four commands
Pull it all together and you get the thing from the top of this guide, the project that just works. When the dotfiles are committed, onboarding a new machine, or a new teammate, looks like this:
git clone https://github.com/you/project.git
cd project
cp .env.example .env # then fill in your real values
npm install
After those, a lot has quietly already been agreed. Your editor is indenting the project’s way because of .editorconfig. Your formatter and linter match everyone else’s because of .prettierrc and the ESLint config. You’re on the right Node because of .nvmrc. You know which secrets to set because .env.example listed them. You filled in the real values in a .env that will never leave your machine.
Nobody had to send you a setup doc titled “things I forgot to mention.” The repo carried its own instructions. That’s the difference between a project and a pile of files that happened to run once.
Common mistakes
These are the ways dotfiles go wrong, roughly in order of how much they’ll ruin your day.
Committing .env with real secrets. The classic, and it’s a rite of passage nobody wants. You forget to add .env to .gitignore, commit, push, and now your live database password is sitting in your GitHub history forever. And it is forever: deleting the file in a later commit does not remove it from history, anyone can dig it out. The only real fix is to treat those secrets as compromised and rotate every one of them. Then add .env to .gitignore so it can’t happen again.
The wrong entries in .gitignore. This cuts both ways. Ignore too little and your build output (a dist or build folder full of generated files) gets tracked, so every build produces a noisy diff of files no human wrote. Ignore too much, with an overly broad pattern, and you accidentally hide real source files, so a teammate clones the repo and a chunk of the app is mysteriously missing. After you edit .gitignore, run git status and actually read it. It will tell you what’s tracked and what’s ignored before a mistake ships.
No .env.example. If you gitignore .env but never commit a template, a new contributor has no way to know the app needs DATABASE_URL and STRIPE_SECRET_KEY and four other things. They clone, run the app, watch it crash on a missing variable, and start guessing. Every variable you add to .env should get a placeholder line in .env.example in the same commit, so the two never drift apart.
Letting config files rot. This is the slow one. Over months, the project changes but the dotfiles don’t. The .nvmrc still says Node 18 while everyone’s actually on 20. The .prettierrc describes a style nobody follows anymore because half the team overrides it in their editor. Now the dotfiles are lying, which is worse than having none, because people trust them. When you change how the project really runs, update the file that’s supposed to describe it. A dotfile is only useful while it tells the truth.
Why this is really about shipping
It’s tempting to file dotfiles under housekeeping, the boring stuff you do before the real work. But the payoff shows up at the exact moment you want to ship.
The same committed config that makes your laptop consistent is what makes your deploys consistent. .nvmrc tells the build server which Node to use. Your .gitignore keeps junk and secrets out of what gets deployed. Your .dockerignore keeps the container lean. A build server is just a teammate who is very fast and has no patience for setup you forgot to write down.
So when a project clones clean, installs clean, and builds the same way on your laptop, your coworker’s laptop, and a server in someone else’s data center, you get something past tidiness: you can push to main and trust what comes out the other end. Get the dotfiles right and the boring part is done, which means the next time someone asks “can I see it?”, the answer is already yes.