Ignoring unwanted files
Why ignore files?
Your project directory will almost always contain files that don't belong in your Git repository. Build artifacts, dependency folders, secret configuration, editor settings, OS-generated junk — committing any of these creates noise for collaborators and can even leak credentials.
Common examples include:
- Dependency directories installed by package managers (
node_modules/,vendor/,venv/) - Build output generated by compilers or bundlers (
dist/,build/,*.o,*.pyc) - Environment and secret files containing API keys or database credentials (
.env,credentials.json) - OS files that your operating system creates automatically (
.DS_Store,Thumbs.db) - Editor and IDE folders with personal workspace settings (
.vscode/,.idea/,*.swp) - Logs and error reports generated at runtime (
*.log,npm-debug.log*)
You could just avoid staging these files, but that's error-prone and they'll still clutter git status output. A better approach is to tell Git to ignore them automatically.
Creating a .gitignore file
Create a file called .gitignore in the root of your repository. Inside it, list patterns — one per line — that describe which files and directories Git should ignore.
# Create the file
touch .gitignore
# Open it in your editor and add patterns
Once you've saved your .gitignore, commit it to the repository so the rules are shared with everyone who works on the project:
git add .gitignore
git commit -m "Add .gitignore"
Tip: You should commit your .gitignore early — ideally in the first commit of a new project, before you accidentally commit files you don't want tracked.
Pattern syntax
Each line in a .gitignore file is a pattern. Here are the rules Git uses to match them:
Basic patterns
| Pattern | What it matches |
|---|---|
config.yml |
Any file or directory named config.yml, at any depth |
*.log |
Any file ending in .log |
debug?.log |
debug0.log, debuga.log, etc. (? matches one character) |
debug[0-9].log |
debug0.log through debug9.log |
Directory patterns
Append a trailing slash to match directories only:
| Pattern | What it matches |
|---|---|
logs/ |
Any directory named logs, at any depth |
build/ |
Any directory named build, at any depth |
Anchoring with a leading slash
Without a leading slash, a pattern matches at any level in the directory tree. Adding a leading / anchors the pattern to the directory where the .gitignore file lives:
| Pattern | What it matches |
|---|---|
config.yml |
config.yml, src/config.yml, app/config.yml |
/config.yml |
Only config.yml in the project root |
/build/ |
Only the build/ directory in the project root |
Double asterisk (**)
The ** wildcard matches across directory boundaries:
| Pattern | What it matches |
|---|---|
**/logs |
logs, src/logs, app/src/logs |
**/logs/*.log |
logs/debug.log, src/logs/error.log |
src/** |
Everything inside src/, at any depth |
a/**/b |
a/b, a/x/b, a/x/y/b |
Negation (exceptions)
Prefix a pattern with ! to re-include a file that would otherwise be ignored:
# Ignore all .env files
*.env
# But keep the example file
!.env.example
Important caveat: You cannot re-include a file if its parent directory is already ignored. Git skips ignored directories entirely for performance, so it never looks inside them:
# This will NOT work as expected
build/
!build/important.js
To work around this, ignore the directory's contents instead of the directory itself:
# Ignore everything in build/ but allow exceptions
build/*
!build/important.js
Comments and blank lines
Lines starting with # are comments. Blank lines are ignored and can be used to organise your patterns into logical groups:
# Dependencies
node_modules/
vendor/
# Build output
dist/
build/
# Environment
.env
.env.local
Real-world .gitignore examples
Node.js
node_modules/
dist/
build/
.env
.env.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.yarn/cache
.yarn/unplugged
coverage/
Python
__pycache__/
*.py[cod]
*.so
venv/
.env
*.egg-info/
dist/
build/
.pytest_cache/
htmlcov/
.coverage
Ruby
vendor/bundle/
.bundle/
*.gem
Gemfile.lock
log/
tmp/
.env
coverage/
PHP / Laravel
vendor/
node_modules/
.env
storage/*.key
public/hot
public/storage
*.cache
WordPress
wp-content/uploads/
wp-content/cache/
wp-content/upgrade/
wp-config.php
.htaccess
*.log
General patterns (add to any project)
# OS files
.DS_Store
Thumbs.db
Desktop.ini
# Editors and IDEs
.vscode/
.idea/
*.swp
*.swo
*~
.project
.classpath
.settings/
# Misc
*.bak
*.tmp
Tip: The github/gitignore repository maintains a comprehensive collection of .gitignore templates for virtually every language and framework. It's a great starting point when setting up a new project.
Ignoring files that are already tracked
This is the most common gotcha with .gitignore: it only affects untracked files. If you've already committed a file, adding it to .gitignore won't make Git forget about it.
To stop tracking a file that's already been committed:
# Remove from Git's index but keep the file on disk
git rm --cached .env
# For an entire directory
git rm -r --cached node_modules/
# Then commit the removal
git commit -m "Stop tracking .env"
After this, the file stays on your local machine but Git treats it as untracked, and your .gitignore pattern will now take effect.
Warning: When other team members pull this commit, the file will be deleted from their working directory. If it's something like .env, make sure everyone knows to back up their copy first, or provide a .env.example template in the repo.
Global .gitignore
Some files — like .DS_Store on macOS or Thumbs.db on Windows — should be ignored everywhere, not just in one project. Instead of adding them to every repository's .gitignore, configure a global ignore file:
# Create the global ignore file
touch ~/.gitignore_global
# Tell Git to use it
git config --global core.excludesFile ~/.gitignore_global
A typical global .gitignore contains patterns for your OS and editor:
# macOS
.DS_Store
.AppleDouble
.LSOverride
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
# Linux
*~
.directory
# Editors
.vscode/
.idea/
*.swp
*.swo
*.sublime-workspace
*.sublime-project
The global file applies to every repository on your machine, but it is not committed anywhere — it only affects you. Project-specific patterns should still go in the repository's .gitignore so they're shared with collaborators.
Local-only rules with .git/info/exclude
If you need to ignore files in a specific repository but don't want to modify the shared .gitignore, you can add patterns to .git/info/exclude. This file works exactly like .gitignore but is never committed.
This is useful for personal tooling or editor files that only affect your workflow:
# Add patterns directly
echo "my-scratch-notes.txt" >> .git/info/exclude
echo ".env.local.bak" >> .git/info/exclude
Nested .gitignore files
You can place a .gitignore file in any subdirectory of your repository, not just the root. Patterns in a nested .gitignore are relative to the directory where the file lives, and they only apply from that directory downward.
project/
├── .gitignore # Project-wide rules
├── docs/
│ └── .gitignore # Rules specific to docs/
└── src/
└── .gitignore # Rules specific to src/
When multiple .gitignore files exist, the rules from the most specific (deepest) file take precedence. In practice, most projects only need a single root-level .gitignore. Use nested files when a subdirectory has meaningfully different ignore requirements — for example, a docs/ folder that generates HTML output you don't want tracked.
Debugging with git check-ignore
When a file is being ignored and you can't figure out why — or it's not being ignored and you think it should be — use git check-ignore to debug:
# Check if a file is ignored and show which rule matched
git check-ignore -v path/to/file
Output example:
.gitignore:3:*.log debug.log
This tells you that line 3 of .gitignore (the pattern *.log) is causing debug.log to be ignored.
To check multiple files at once:
git check-ignore -v *.log src/*.tmp
If the command produces no output, the file is not being ignored by any rule.
Keeping empty directories
Git only tracks files, not directories. If you need an empty directory to exist in the repository — for example, a logs/ folder that should be present but always empty — the convention is to add a .gitkeep file inside it:
mkdir logs
touch logs/.gitkeep
Then in your .gitignore:
# Ignore log files but keep the directory
logs/*
!logs/.gitkeep
Note: .gitkeep is not a Git feature — it's a community convention. The file name doesn't matter; it could be called anything. .gitkeep is simply the most widely recognised name for this purpose.
Quick reference
| I want to... | Pattern |
|---|---|
Ignore all .log files |
*.log |
Ignore node_modules/ everywhere |
node_modules/ |
Ignore .env only in the root |
/.env |
Ignore everything in build/ except one file |
build/* then !build/keep.js |
Ignore all .tmp files in any cache/ folder |
**/cache/*.tmp |
| Ignore a directory but keep it in the repo | Add dir/* + !dir/.gitkeep |
| Stop tracking a file already committed | git rm --cached <file> |
| Find out why a file is ignored | git check-ignore -v <file> |
| Ignore files globally (all repos) | git config --global core.excludesFile ~/.gitignore_global |
Once your .gitignore is set up and your repository is clean, DeployHQ can deploy your code automatically — only the files that matter reach your server, while ignored files stay out of the deployment.