How to safely store API keys in a project: .env, GitHub Secrets, server variables
Main chat
A chat for vibe coders: news, guides, live cases, marketplace, and finding executors.
In 2024, GitGuardian discovered more than 12 million secrets in public GitHub repositories – API keys, passwords, tokens. Most of them got there by accident: the developer simply forgot to remove the key in front of the commit.
The consequences are different. Someone is charged for other people’s requests to OpenAI. Someone's getting hacked into the database. Someone gets a bill for several thousand dollars for cloud resources that someone used for mining.
The good news is, it's not hard to defend yourself. You need to build the right habit once and most problems will disappear.
Why you can’t keep your keys in the code
Let's start with the most important thing, because beginners often wonder, "So what if the key is in the code?" The repository is private.
Private repository is not protection. One random change of visibility, one hacked member account, one wrong click is enough. Public repositories are scanned automatically by bots within seconds of a commit.
Git remembers everything. Even if you deleted the key from the file and made a new commit, it remained in history. git log will show all previous versions of the file. Deleting the current code does not remove the history.
When the code says const apiKey = "sk-abc123", this key is seen by everyone who clones the repository. Even if you trust the whole team, it's bad practice: the keys are mixed, it's unclear whose key is being used, rotation turns into a nightmare.
The rule is simple: a secret in the code is no longer a secret.
Environment Variables: The Basis of Fundamentals
The right place for secrets is not code files, but environment variables. These are the name-value pairs that the operating system provides to the running process. The code reads them at launch — it doesn’t store them.
Reading environment variables is simple:
// Node.js
const apiKey = process.env.OPENAI_API_KEY;
const dbUrl = process.env.DATABASE_URL;
# Python
import os
api_key = os.getenv('OPENAI_API_KEY')
db_url = os.getenv('DATABASE_URL')
Where these variables come from depends on the environment. Locally - from the .env file. On the server - from the hosting settings. CI/CD is from GitHub Secrets. The code is the same everywhere, the secrets are different in every environment.
.env file: for local development
.env is a simple text file at the root of a project where you list environment variables for local work.
# .env
OPENAI_API_KEY=sk-proj-abc123...
DATABASE_URL=postgresql://localhost:5432/myapp
STRIPE_SECRET_KEY=sk_test_xyz789...
TELEGRAM_BOT_TOKEN=123456:ABCdef...
The dotenv library reads this file at startup and downloads variables:
# Node.js
npm install dotenv
// First line at the entry point (index.js, app.js)
require('dotenv').config();
Now process.env contains everything from .env
console.log(process.env.OPENAI API KEY) // works
# Python
pip install python-dotenv
from dotenv import load_dotenv
import os
load_dotenv() # читает .env файл
api_key = os.getenv('OPENAI_API_KEY')
Most important: .env in .gitignore
Immediately after creating .env, add it to .gitignore. This is a list of files and folders that Git ignores and does not include in commits.
# .gitignore
.env
.env.local
.env.production
.env.*.local
Make sure the file is ignored:
git status #.env should not appear in the list
git check-ignore -v .env # should show: .gitignore:1:.env
.env.example: template for the team
If .env cannot be committed, how will other developers know which variables are needed? .env.example is a file with the same variable names but no real values. You can and should commit him.
# .env.example — коммитится в репозиторий
OPENAI_API_KEY= # ключ от OpenAI, получить на platform.openai.com
DATABASE_URL= # строка подключения к PostgreSQL
STRIPE_SECRET_KEY= # тестовый ключ Stripe (sk_test_...)
TELEGRAM_BOT_TOKEN= # токен от @BotFather
The new developer clones the repository, copies the file and fills in its values:
cp .env.example.env
open .env and fill in the values
GitHub Secrets: for CI/CD and Deploy
When you need to run a demo or tests through GitHub Actions, environment variables are no longer needed on your computer, but on GitHub servers. For this purpose, use GitHub Secrets.
How to add a secret
- Open the repository on GitHub
- Settings – Secrets and Variables – Actions
- Click "New Repository Secret"
- Enter the name (e.g.
OPENAI_API_KEY) and the value - Save it
After that, the value is encrypted and does not appear even in the interface - only stars.
Use in GitHub Actions
#.github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
run-on: ubuntu-latest
steps:
- uses actions/checkout@v4
- name: Deploy to server
env:
Variables are taken from GitHub Secrets
OPENAI API KEY: ${secrets.OPENAI API KEY}
DATABASE URL: ${secrets.DATABASE URL}
run: |
# Here are your deploy teams
npm install
npm run build
npm run deploy
GitHub Secrets are automatically masked in logs – if the value accidentally gets into the output of the command, it will be replaced with ***.
Environments: Different Secrets for Dev and Prod
If you have multiple environments, you can create a separate set of secrets for each through GitHub Environments:
Settings → Environments → New Environment →
jobs:
deploy-production:
environment: production # uses secrets from the production environment
steps:
- env.
DATABASE URL: ${{secrets.DATABASE URL }} # production value
Server variables: on hosting and VPS
When the application is deployed on the server, you also need to specify environment variables there. It depends on the platform.
Timeweb Cloud / VPS with PM2
If you run Node.js through PM2:
// ecosystem.config. js
module.export = {
apps: [{{}
name: 'myapp',
script: './index.js',
env:
NODE ENV: 'production,'
Do not write secrets here - this file will go to the repository
}
env file: '.env.production', // read from a separate file
}
};
It is better to create a separate .env.production on the server right in the command line:
# On the server via SSH
nano /var/www/myapp/.env.production
# Enter values, save.
# Rights only for the owner:
chmod 600 /var/www/myapp/.env.production
Docker and docker-compose
#docker-compose.yml – Do not write secrets directly
services:
app:
image: myapp
env file:
.env.production # file on the server, not in the repository
Or pass on a separate secret file:
# Create a file with secrets on the server
echo "OPENAI API KEY=sk-.." >> /etc/myapp/secrets.env
docker run --env-file /etc/myapp/secrets.env myapp
Vercel, Railway, Render and other PaaS
All modern platforms for deploy have an interface for environment variables. This is the easiest and safest option:
Vercel:* Project → Settings → Environment Variables
Railway: Project → Variables
Render: Service → Environment
Variables are encrypted, available only in the running application and are not displayed anywhere in plain form.
What not to commit: full list
Save this section and add each project to .gitignore.
# .gitignore - secrets and confidential files
# Files of environment variables
.env
.env.local
.env.development
.env.production
.env.staging
.env.*.local
Keys and certificates
*
*
* p12
*pfx.
id rsa
id ed25519
*.cert
Configs with secrets (often forgotten)
config/secrets.yml
config/database.yml # if it contains passwords
secrets.json.
credentials. json #Google Cloud credentials
service-account.json #Firebase/GCP service account
# Cryptocurrency wallets and keys (if you work with the Web3)
*keystore.
wallet.json
In addition to the obvious .env, pay special attention to:
credentials.json and service-account.json are Google Cloud and Firebase service accounts. Very often they leak, give full access to the project.
*.pem and *.key are SSL certificate and SSH private keys. Their compromise is critical.
Database Configurations – In some frameworks (Ruby on Rails, Laravel) configuration files contain real database passwords.
How to check if the keys have already leaked
Before you consider the repository clean – it is worth checking the history of commits. The key could have been there months ago.
git log – a quick history search
# Looking for lines like secrets throughout the history of the repository
git log -p --all | grep -i "api key\|secret\|password\|token\|sk-\|Bearer"
trufflehog - automatic scanner
#Install
pip install trufflehog
# or through brew:
brew install trufflehog
Scanning the repository
trufflehog git file:// --only-verified
truffleHog knows the key formats of hundreds of providers - OpenAI, Stripe, AWS, GitHub and others. It doesn’t just look for the word “key,” it checks for a pattern of real meaning.
gitleaks is another popular scanner
# Скачать с github.com/gitleaks/gitleaks
gitleaks detect --source . -v
GitHub itself scans public repositories
If the repository is public, GitHub will automatically search for secrets. Upon detection, you receive a notification by email. Some providers (Stripe, OpenAI, GitHub) receive a notification and automatically invalidate the key.
What to do if the key is already in the repository
The bad news is that deleting a file or string by commit won’t help. The key remains in history.
The good news is that there is a right course of action.
Step 1: Revoke the key immediately. Go to the ISP panel and revoke the compromised key now. Not in an hour, now. If the repository was public for even a second, consider the key compromised.
Step 2: Check the logs for suspicious requests. Most providers show a history of key usage. See if there were any calls from unknown IP.
Step 3: generate a new key and add it to the right place. The new key is only in environment variables, not in code.
**Step 4: Clear the history of git (optional, difficult). If the repository is private and you are sure that no one has time to copy, you can rewrite the history through git filter-repo. But this is a complex operation that requires the coordination of the entire team. It is easier to consider the key leaked and simply change it.
# Install git-filter-repo
pip install git-filter-repo
# Remove all key line entry from history
git filter-repo --replace-text <(echo 'sk-abc123) ==>REMOVED'
After that, you need to make git push --force, which will rewrite the history on the server. Anyone who cloned the repository should re-pump it.
Again, the most important thing is to revoke the key, not to clear the story. Changing the key solves the security problem. Cleaning up history is an optional step for order.
Additional Level: Vault and Secret Managers
For small projects, .env and GitHub Secrets are sufficient. But as the project grows, more tools emerge.
*HashiCorp Vault is a specialized secret repository with access control, key rotation and auditing. Each application gets only the secrets it needs, with a limited expiration date.
AWS Secrets Manager/Parameter Store is the equivalent of AWS infrastructure. Secrets are stored in the cloud, rotated automatically, rights are configured through IAM.
Doppler, Infisical are simpler SaaS alternatives to Vault. Synchronize environment variables between environments, integrate with CI / CD, there are free rates.
For most Wibcoding projects, these tools are redundant – it is not worth lifting Vault for the sake of three API keys. But it's good to know they exist.
Quick Start: What to Do Right Now
If you read the article and want to put the project in order right away, here is the sequence:
#1. Create .env with real values
touch.env
#2. Add .env to .gitignore
echo ".env" >> .gitignore
echo ".env.*" >> .gitignore
#3. Create .env.example with empty values
cp .env.example
# Open .env.example and clear all values, leaving only names
#4. Check that .env is not tracked by git
git check-ignore -v.env
#5. If .env has already been added to git, remove it from tracking.
git rm --cached.env
#6. Replace all keys in the code with process.env. NAME
Was: const key = "sk-abc123"
Stall: const key = process.env. OPENAI API KEY
#7. Scan history.
git log -p --all | grep -iE "sk-|api[ -]?key|secretpassword|token" | head -50
Checklist of safe keeping secrets
Local development:
.env file created at the root of the project
.env added to .gitignore
.env.example with empty values added to the repository
● There is no real key in the code files.
Git story:
● Scanned history for secrets (git log -p or truffleHog)
● If found, the keys are withdrawn from the provider.
CI/CD:
Secrets added to GitHub Secrets (not workflow files)
● There are no real values in .yml files, only ${{secrets.NAME }}}
Production server:
● Variables are specified through a hosting interface or file outside the repository
Secret file has 600 rights (only owner)
● Different keys are used for dev and production
General:
● No one but the right people has access to production keys.
● There is an action plan for compromise: where to withdraw, where to change
Outcome
Safely keeping secrets isn’t about paranoia, it’s about habit. Once you configure .env and .gitignore, add GitHub Secrets for CI/CD and most of the problems disappear.
The main rules that are worth remembering:
Secrets live in environment variables, not code. .env is in .gitignore always and in every project. .env.example with empty values commits - the real .env never. If the key is in the repository – first withdraw, then deal with the history.
If you want to test yourself right now - open any of your project and search in the line code api_key =, secret =, password =. If you find real values, it is time to correct.
FAQ
Is it possible to store .env in a private repository? ** Technically possible, but this is bad practice. A private repository can become public by mistake. In addition, all participants in the repository will have access to production keys, which is undesirable.
How to transfer .env to a new developer in a team? ** Through a secure channel - an encrypted message in the messenger, 1Password, Bitwarden for commands. Not through email or Telegram in plain form. In the long run, he is a secret manager like Doppler or Infisical.
Do I need to hide the keys in the frontend (React, Vue)? ** Yeah, and that's a different complicated topic. Any key that enters the JavaScript band is visible to the user through DevTools. For the frontend, use only restricted keys (read only, linked to the domain). Keys with broad rights should only be on the backend.
What to do if you are not sure if the key is leaked or not? ** Acting like a leak is to recall and generate a new one. It takes 5 minutes. Testing and doubting is a losing strategy.
**The environment variable is set, but the app doesn't see it - why? **
Most likely, dotenv is loaded after code that uses a variable. Make sure that require('dotenv').config() or load_dotenv() are the first lines at the application entry point, before any other imports.
June 2026. *