Using .gitignore Effectively for Rails Projects
Somebody commits .DS_Store, someone else adds *.DS_Store to the project’s .gitignore, and three months later there are eleven personal editor entries in a file that’s supposed to describe the project. Meanwhile config/master.key is in the repo because nobody checked. .gitignore is a small file with a few rules that aren’t obvious, and getting them right prevents both problems.
Three Files, Three Purposes
This is the distinction that fixes most .gitignore mess.
| File | Scope | Committed | For |
|---|---|---|---|
.gitignore |
Repo, shared | Yes | Artifacts this project generates |
~/.config/git/ignore |
All your repos | No | Your editor, your OS, your tools |
.git/info/exclude |
This repo, you only | No | Local scratch files |
Your editor’s files do not belong in the project’s .gitignore. .idea/, .vscode/, *.swp, .DS_Store — these are yours, not the project’s. Put them in your global ignore once and never think about them again.
Example:
git config --global core.excludesfile ~/.config/git/ignore
# ~/.config/git/ignore
.DS_Store
.AppleDouble
Thumbs.db
*.swp
*.swo
*~
.idea/
.vscode/
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/extensions.json
.direnv/
.envrc
tags
TAGS
Note the negations on the VS Code files — a project that commits shared editor settings should still get them, and this lets your global rule ignore everything else in .vscode/.
.git/info/exclude is for the scratch file you made today: notes.md, debug.rb, dump.sql. Local, untracked, and it doesn’t clutter anyone’s diff.
A Rails .gitignore Worth Using
Example:
# --- Secrets --------------------------------------------------------------
/config/master.key
/config/credentials/*.key
/config/*.key
.env
.env.*
!.env.example
!.env.test
# --- Generated / runtime --------------------------------------------------
/.bundle
/vendor/bundle
/log/*
!/log/.keep
/tmp/*
!/tmp/.keep
!/tmp/pids/
/tmp/pids/*
!/tmp/pids/.keep
/storage/*
!/storage/.keep
/tmp/storage/*
!/tmp/storage/.keep
# --- Assets ---------------------------------------------------------------
/public/assets
/public/packs
/public/packs-test
/app/assets/builds/*
!/app/assets/builds/.keep
/node_modules
# --- Test output ----------------------------------------------------------
/coverage/
/spec/examples.txt
/tmp/screenshots/
/tmp/capybara/
# --- Local dev ------------------------------------------------------------
/config/database.local.yml
/db/*.sqlite3
/db/*.sqlite3-*
Three things in there deserve explanation.
The Directory Trap
Git tracks files, not directories. An empty log/ or tmp/pids/ does not exist in a fresh clone, and Rails will fail to boot because it can’t write there.
That’s what the .keep pattern solves:
/log/* # ignore everything inside log/
!/log/.keep # except this one file
touch log/.keep tmp/.keep tmp/pids/.keep storage/.keep
git add -f log/.keep tmp/.keep tmp/pids/.keep storage/.keep
Get this wrong — write /log instead of /log/* — and the negation can’t work, because you cannot re-include a file inside an excluded directory. Git doesn’t descend into an ignored directory at all, so !/log/.keep never gets evaluated.
That rule catches people constantly. If a negation isn’t working, check whether a parent directory is excluded.
The Leading Slash Matters
log/ # matches log/ ANYWHERE — including app/models/log/
/log/ # matches only the log/ at the repo root
Without the leading slash, the pattern applies at every level. config/ in a .gitignore will ignore app/javascript/config/ too, which is rarely intended and produces a confusing “why isn’t my file being tracked” morning.
Use a leading slash for anything you mean to anchor at the root — which for a Rails app is most of it.
Secrets Are the Part That Matters
Everything else in this file is tidiness. This part is security.
/config/master.key
/config/credentials/*.key
.env
.env.*
!.env.example
The ordering matters — the negation must come after the pattern that excludes it, because later rules win.
Check whether you already committed one:
git log --all --full-history -- config/master.key
git log --all --full-history -- '.env'
If either returns commits, the key is in your history and adding it to .gitignore now does nothing — anyone with the repo has it. The only correct response is to rotate the credential. Rewriting history with git filter-repo or BFG is worth doing as cleanup, but it doesn’t un-leak anything; assume it’s compromised and issue a new one.
Belt and braces, as a pre-commit hook:
#!/usr/bin/env bash
# .git/hooks/pre-commit
if git diff --cached --name-only | grep -qE '(master\.key|\.env$|credentials/.*\.key)'; then
echo "Refusing to commit a credential file. Check .gitignore."
exit 1
fi
Better still, run gitleaks or trufflehog in that hook — they catch keys pasted into ordinary source files, which is how most leaks actually happen.
Already Tracked? Ignoring Won’t Help
The most common confusion. .gitignore only affects untracked files. A file git is already tracking stays tracked.
Example:
# Stop tracking, keep the local file
git rm --cached config/database.local.yml
git commit -m "chore: stop tracking local database config"
# For a whole directory
git rm -r --cached log/
--cached is essential — without it you delete the file from disk as well.
To reset everything against a freshly-updated .gitignore:
git rm -r --cached .
git add .
git commit -m "chore: apply updated .gitignore"
That restages the entire repo, so review git status before committing.
Pro-Tip: When a file isn’t being ignored and you can’t work out why, stop reading the file and ask git directly:
git check-ignore -v path/to/file. It prints the exact file, line number, and pattern that decided the outcome — including rules from your global ignore and.git/info/exclude, which is where the answer usually is when the project’s.gitignorelooks correct. The other half of the same problem is a tracked file you expected to be ignored;git ls-files path/to/filereturning anything means it’s tracked and no ignore rule will ever apply. Between those two commands, every “why is git doing this” question about ignoring resolves in about ten seconds.
Useful Patterns
# Negation — ignore all .yml in config, keep the examples
/config/secrets/*.yml
!/config/secrets/*.example.yml
# Match at any depth
**/node_modules
**/.cache
# Character ranges
db/*.sqlite3[-0-9]*
# Ignore a file only at the root
/TODO.md
**/ matches any number of intermediate directories. * does not cross directory boundaries; ** does.
Conclusion
Split the three scopes properly — project artifacts in .gitignore, your editor and OS junk in your global ignore, today’s scratch files in .git/info/exclude — and the project file stays small and meaningful. Anchor patterns with a leading slash, use the dir/* plus !dir/.keep form so empty directories survive a clone, and remember that a negation can’t rescue a file inside an excluded directory. Put credentials at the top and back them with a pre-commit scan. And when something isn’t behaving, git check-ignore -v tells you exactly which line did it.
FAQs
Q1: Should Gemfile.lock be ignored?
No, for applications — commit it so deploys install exactly what you tested. For a published gem, don’t commit it; the lockfile should be resolved by the consuming application.
Q2: What about public/assets?
Ignore it. Precompiled assets are build output and belong to the deploy, not the repo. Committing them causes constant merge conflicts on fingerprinted filenames.
Q3: Can I have a .gitignore in a subdirectory?
Yes, and rules in it are relative to that directory. Useful for something like app/assets/builds/.gitignore, but scattering them makes the overall behaviour harder to reason about.
Q4: Does .gitignore affect files on other people’s machines?
The committed .gitignore applies to everyone who clones. Your global ignore and .git/info/exclude are yours alone, which is exactly why personal preferences belong there.
Q5: How do I force-add an ignored file just once?
git add -f path/to/file. Legitimate for .keep files and occasionally for a fixture; if you’re reaching for it often, the ignore rule is probably too broad.
Check viewARU - Brand Newsletter!
Newsletter to DEVs by DEVs - boost your Personal Brand & career! 🚀