.envをGitから除外していても、Viteでブラウザ向けに使った値は、公開するJavaScriptに入ります。「GitHubへアップロードしないこと」と「サイトの利用者へ渡さないこと」は、別々に確認する必要があります。
AIにコードを書いてもらったとき、APIキーを.envへ移す修正だけで終わっていませんか。この記事では、完全なダミー値を使って、Gitの追跡対象・ビルドしたファイル・ブラウザの表示を順番に確認します。読み終わると、秘密を置く場所と、公開してよい設定値を分けて考えられるようになります。
対象は、Viteで作ったブラウザ側のアプリです。Reactを使わない小さなHTML・JavaScriptの例で試すので、Viteの挙動に絞って確認できます。サーバー側で画面を作るSSRなど、別の仕組みが加わる場合は、そのフレームワークの資料も確認してください。
先に結果:.gitignoreは、ビルド後の公開範囲を決めない
今回、同じサンプルで次の結果になりました。Viteの公開対象を変える設定は、基本の実験では追加していません。
| 調べたもの | Gitへの追加 | ビルド後の結果 |
|---|---|---|
| .envなどの設定ファイル | 除外された | ファイル自体をGitへ入れなくても、Viteは読み込める |
| 画面で使ったVITE_付きの値 | 元の.envは除外された | JavaScriptとブラウザに値が出た |
| VITE_が付かない値 | 元の.envは除外された | 今回の標準設定ではundefinedになり、値は出なかった |
| コードで使わなかったVITE_付きの値 | 元の.envは除外された | 今回の出力には値がなかった。ただし秘密の保存先には使わない |
「見つからなかったから、どのコードでも秘密を守れる」とは判断できません。後半では、公開する接頭辞の設定を変えた場合と、環境変数を変更してから再ビルドする場合も確認します。
1.新しいフォルダに、ダミー値だけのサンプルを作る
必要なものはNode.jsとnpm、Git、コードを編集するエディター、ブラウザです。今回は2026年10月13日に、Node.js 24.19.0、npm 11.9.0、Git 2.52.0、Vite 8.3.4、Chromium 151で検証しました。Node.js 24系を使うと、今回と条件をそろえやすくなります。
既存のアプリとは別に、新しいフォルダを作ってください。以下のコマンドは、そのフォルダで実行します。キーを発行する必要はなく、実際のAPIも呼び出しません。
mkdir vite-env-lab
cd vite-env-lab
node --version
npm --version
git --version
package.json:使うViteのバージョンとコマンドを決める
package.jsonを作り、次の内容を保存します。
{
"name": "vite-env-lab",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 127.0.0.1 --port 5178 --strictPort",
"build": "vite build",
"preview": "vite preview --host 127.0.0.1 --port 4178 --strictPort",
"inspect": "node inspect-build.mjs"
},
"devDependencies": { "vite": "8.3.4" }
}
続いてインストールします。生成されたpackage-lock.jsonは、依存関係を記録するために残します。同じプロジェクトを別の環境で準備するときは、このファイルも渡し、npm ciを使います。
npm install --ignore-scripts
.gitignore:設定ファイルと生成物を除外する
同じフォルダに.gitignoreを作ります。末尾に.txtが付かないようにしてください。この例では.env系をまとめて除外し、値を空にした見本として使う.env.exampleだけを例外にしています。
node_modules/
dist/
.env
.env.*
!.env.example
.env:比較用の値を入れる
次の3行を.envへ保存します。すべてこの記事専用のダミー文字列です。1行目は画面で使い、2行目は接頭辞がない場合を調べ、3行目は使わない値がどうなるかを調べます。
VITE_DEMO_LABEL=PUBLIC_BASE_DUMMY
PRIVATE_DEMO_VALUE=SERVER_ONLY_DUMMY
VITE_UNUSED_DEMO_VALUE=UNUSED_DUMMY
.env.productionには、次の1行を保存します。
VITE_DEMO_LABEL=PUBLIC_PRODUCTION_DUMMY
.env.stagingには、次の1行を保存します。
VITE_DEMO_LABEL=PUBLIC_STAGING_DUMMY
3つともpackage.jsonと同じ階層です。srcフォルダや、配信用のpublicフォルダには入れません。Viteの標準設定はプロジェクトのルートから.envを読み込みます。
index.htmlとmain.js:読み取った値を画面へ出す
index.htmlを次の内容で保存します。
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Viteの環境変数を確認するサンプル</title>
<style>
body { margin: 0; background: #eef4f0; color: #173b30;
font: 18px/1.8 system-ui, sans-serif; }
main { box-sizing: border-box; max-width: 850px; margin: 8vh auto;
padding: 32px; background: white; border-radius: 16px; }
h1 { font-size: clamp(24px, 4vw, 34px); line-height: 1.5; }
pre { padding: 20px; background: #edf2f0; white-space: pre-wrap;
overflow-wrap: anywhere; font-size: 16px; }
small { color: #49675d; }
</style>
</head>
<body>
<main>
<small>完全なダミー値を使ったローカル検証</small>
<h1>ブラウザに届いた値</h1>
<pre id="result"></pre>
<p>VITE_付きの値・接頭辞のない値・ビルドのモードを確認します。</p>
</main>
<script type="module" src="/main.js"></script>
</body>
</html>
続いて、同じフォルダにmain.jsを保存します。画面に出す3項目だけを指定しています。
const lines = [
`MODE: ${import.meta.env.MODE}`,
`VITE_DEMO_LABEL: ${import.meta.env.VITE_DEMO_LABEL}`,
`PRIVATE_DEMO_VALUE: ${String(import.meta.env.PRIVATE_DEMO_VALUE)}`,
];
document.querySelector('#result').textContent = lines.join('\n');
ここまでの構成は次のようになります。node_modulesはnpmが作ったフォルダです。
vite-env-lab/
├─ .gitignore
├─ .env
├─ .env.production
├─ .env.staging
├─ package.json
├─ package-lock.json
├─ index.html
├─ main.js
└─ node_modules/
2.開発サーバーで、VITE_付きと接頭辞なしを比べる
npm run dev
ブラウザでhttp://127.0.0.1:5178/を開きます。次の3行が出れば、この段階は成功です。
MODE: development
VITE_DEMO_LABEL: PUBLIC_BASE_DUMMY
PRIVATE_DEMO_VALUE: undefined

undefinedは、ここで値が取得できないことを表しています。本物の秘密の値が取得できないときに、動かすためだけにVITE_を付け足すと、公開対象へ変えてしまいます。
Vite公式の環境変数の説明でも、標準の公開対象はVITE_付きであり、秘密のAPIキーを入れないよう案内されています。この接頭辞は、値を暗号化する機能ではありません。
確認したらターミナルでCtrl+Cを押し、開発サーバーを止めます。
3.Gitから.envが除外されたか確認する
今回作った空の練習用プロジェクトをGitで管理します。次の操作は、既存のアプリではなくvite-env-lab内で実行してください。
git init -b main
git check-ignore -v .env .env.production .env.staging
この.gitignoreを使った場合、次のようにファイル名と除外ルールが出ます。
.gitignore:3:.env .env
.gitignore:4:.env.* .env.production
.gitignore:4:.env.* .env.staging
続いて、記事で用意したソースと設定をステージへ追加し、対象ファイルを確認します。GitHubへのpushは、この実験では行いません。
git add .gitignore package.json package-lock.json index.html main.js
git ls-files
一覧に.env、.env.production、.env.stagingがないことを確認します。ここまでで分かるのはGitの追跡対象だけです。ブラウザへ渡す内容は、次のビルドで調べます。
なお、Git公式の.gitignoreの説明にあるとおり、すでに追跡されているファイルには除外ルールを追加しても効きません。既存プロジェクトでgit ls-filesに.envが出る場合は、今回の新規サンプルとは状況が異なります。秘密を過去に公開した可能性があれば、後述するキーの失効も含めて対応します。
4.本番用にビルドし、distのJavaScriptを調べる
npm run build
dist/index.htmlとdist/assets/内のJavaScriptが生成されます。これが静的サイトとして配信するファイルです。元の.envをGitへ入れなくても、ビルド時にはその値を利用できます。
値の確認をWindowsとmacOS・Linuxで同じ方法にするため、inspect-build.mjsを同じフォルダへ追加します。これは今回のダミー文字列がdistのJavaScriptに含まれるか調べるだけのスクリプトです。
import { readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
function filesIn(dir) {
return readdirSync(dir, { withFileTypes: true }).flatMap((item) => {
const path = join(dir, item.name);
return item.isDirectory() ? filesIn(path) : [path];
});
}
const javascript = filesIn('dist').filter((path) => path.endsWith('.js'));
const values = [
'PUBLIC_BASE_DUMMY',
'PUBLIC_PRODUCTION_DUMMY',
'PUBLIC_STAGING_DUMMY',
'PUBLIC_CHANGED_DUMMY',
'SERVER_ONLY_DUMMY',
'UNUSED_DUMMY',
];
for (const value of values) {
const found = javascript.filter((path) => readFileSync(path, 'utf8').includes(value));
console.log(`${value}: ${found.length ? found.join(', ') : '見つかりません'}`);
}
npm run inspect
今回の結果は次のとおりでした。JavaScriptのファイル名の末尾は、ビルド内容やバージョンによって変わります。
PUBLIC_BASE_DUMMY: 見つかりません
PUBLIC_PRODUCTION_DUMMY: dist/assets/index-BttlQtT0.js
PUBLIC_STAGING_DUMMY: 見つかりません
PUBLIC_CHANGED_DUMMY: 見つかりません
SERVER_ONLY_DUMMY: 見つかりません
UNUSED_DUMMY: 見つかりません
Gitから.envを除外したままでも、PUBLIC_PRODUCTION_DUMMYという値はJavaScriptに入りました。ビルドが通常のproductionモードなので、.env.productionに書いた値が使われています。
使っていない値が見つからない場合も、公開用の変数に秘密を置かない
今回はVITE_UNUSED_DEMO_VALUEをコードから参照していないため、その値は生成されたJavaScriptにありませんでした。環境変数の名前にVITE_があれば、どの未使用値も必ずすべてのファイルへ残る、とまでは言えません。
ただし、コードで使い始めたり、環境変数のオブジェクトをまとめて出力したりすると、結果は変わり得ます。VITE_付きには公開してよい値だけを置くという判断は変わりません。また、この検索は指定したダミー文字列だけを探します。見つからなかったことを、アプリ全体の秘密情報が漏れない証明にはできません。
5.ブラウザが受け取るJavaScriptにも値がある
npm run preview
http://127.0.0.1:4178/を開くと、ビルド済みのdistを確認できます。開発サーバーの5178番とは別です。画面のMODEがproduction、表示する値がPUBLIC_PRODUCTION_DUMMYになっていることを確認します。

画面へ表示しないコードの場合も、ブラウザが取得したファイルを確認できます。
- 開発者ツールを開き、Networkタブを選びます。
- ページを再読み込みします。
- 種類をJSで絞り、
index-で始まるJavaScriptを選びます。 - Responseで、
PUBLIC_PRODUCTION_DUMMYを探します。
また、リクエストのURLを新しいタブで開いても、そのJavaScriptを読めます。今回のファイルを実際に開くと、次のようになりました。

長い1行へ短縮されていても、秘密として隠せるわけではありません。変数名そのものが消えている場合もあるため、この実験ではダミーの値を検索しています。
Networkの使い方から確認したい場合は、Chrome DevToolsで読み込みと表示を調べる手順も参考にしてください。
6.モードの違いと、再ビルドが必要な場面を確かめる
stagingを指定すると、使う値も変わる
previewをCtrl+Cで止め、次を実行します。
npm run build -- --mode staging
npm run inspect
npm run preview
4178番のページを再読み込みすると、次の表示になります。
MODE: staging
VITE_DEMO_LABEL: PUBLIC_STAGING_DUMMY
PRIVATE_DEMO_VALUE: undefined
.env.stagingの値が選ばれ、JavaScriptの検索でもPUBLIC_STAGING_DUMMYが見つかりました。stagingというモード名を付けても、それ自体でサイトへのアクセスが制限されるわけではありません。
| コマンド | MODE | 今回使われた値 |
|---|---|---|
npm run dev |
development | .envの基本値 |
npm run build |
production | .env.productionの値 |
npm run build -- --mode staging |
staging | .env.stagingの値 |
.envを書き換えるだけでは、作成済みのdistは変わらない
ここでは次の順番で試します。
- previewを止め、
npm run buildでproductionのdistを作り直します。 npm run previewを起動し、PUBLIC_PRODUCTION_DUMMYの表示を確認します。- .env.productionの値だけを
PUBLIC_CHANGED_DUMMYへ変えて保存します。 - 再ビルドせずにページを再読み込みします。表示はPUBLIC_PRODUCTION_DUMMYのままです。
- previewを止め、
npm run buildを実行してから、再びnpm run previewを起動します。 - ページを再読み込みすると、PUBLIC_CHANGED_DUMMYに変わります。
今回の確認では、手順3の前後でdistの内容は変わらず、手順5のビルド後に値が変わりました。ビルド時に値が埋め込まれるためです。練習後は.env.productionをPUBLIC_PRODUCTION_DUMMYへ戻して、再ビルドしておくと最初の結果に戻せます。
公開サービスの設定画面で値を変更した場合も、既存のビルドへ反映されるか、再ビルド・再デプロイが必要かを確認します。Vercelでの設定と公開の流れは、Vercelの環境変数・再デプロイの手順にまとめています。
秘密のAPIキーを使うなら、ブラウザへ渡さない構成にする
外部APIの利用料金が発生する秘密のキーなどは、ブラウザが実行するコードへ入れず、自分のサーバーやサーバー側の関数で扱います。ブラウザからは自分のサーバーへ必要な入力を送り、サーバーがキーを使って外部APIを呼び出し、必要な結果をブラウザへ返します。
ブラウザ → 自分のサーバー → 外部API
秘密のキーを使う
ブラウザ ← 必要な結果だけ ← 応答
このときも、キーを応答やエラー文へ含めたり、クライアントへ渡す設定として出力したりすると漏れてしまいます。また、自分のAPIを誰でも無制限に呼べる状態なら、キーを隠していても利用枠を使われる可能性があります。アプリに応じた利用者の認証・権限、入力の検査、回数や費用の制限も必要です。
APIキーと呼ばれるものの中には、サービスがブラウザへの配置を想定する公開用のものもあります。名前だけで判断せず、そのサービスの公式資料で、キーの種類・許可される用途・必要な制限を確認してください。
「VITE_を外したから安全」とも断定しない
Viteには、公開する接頭辞を変えるenvPrefixがあります。追加検証でPRIVATE_も公開対象に含めると、先ほどは取得できなかったSERVER_ONLY_DUMMYが、生成されたJavaScriptに入りました。
さらにdefineなどで値を明示的に埋め込む実装もあります。既存コードでは.envだけでなく、vite.config.jsやvite.config.ts、プラグイン、ブラウザへ返す処理まで確認します。設定の意味はVite公式のenvPrefixで確認できます。
今回のような静的なViteアプリで接頭辞を外すと、秘密をブラウザから使えるようにしたまま隠せるのではなく、標準ではその値をブラウザから取得できなくなります。秘密を使う処理の場所も、合わせて変える必要があります。
同じ結果にならないときの確認場所
- VITE_DEMO_LABELまでundefinedになる:ファイル名が.env.txtになっていないか、package.jsonと同じ階層か、変数名の大文字・小文字が一致するかを確認します。開発サーバーは.env変更後に再起動します。
- productionとstagingで値が変わらない:buildへ
-- --mode stagingを渡したか、.env.stagingを保存したかを確認します。ビルドのログと画面のMODEも見比べます。 - .envより別の値が使われる:モード別のファイルや.localファイル、実行環境に同名の変数がないか確認します。起動前から環境に設定されている変数は、.envより優先されます。調査のために環境変数を全部ログへ出す必要はありません。
- distがないと言われる:先にnpm run buildを実行します。途中で失敗した場合は、ビルドのエラーを解消してからinspectやpreviewを実行します。
- Port 4178や5178が使用中と出る:前に起動した同じサンプルをCtrl+Cで止めます。別の作業で使っている場合は、package.jsonのportを空いている番号へ変え、ブラウザで開く番号も合わせます。
本物のキーが公開されていた場合
ファイルからキーを消すだけでは、すでに配信・取得された値を無効にはできません。提供元で古いキーを失効・再発行し、利用履歴や請求を確認します。そのうえで公開するコードから秘密を除き、正しい保管先へ設定して再ビルド・再デプロイします。
GitHubへも保存していた場合は、現在のファイルだけでなく履歴や共有先も関係します。GitHub公式の機密情報を削除する案内は、最初に秘密を失効・交換することと、履歴の書き換えが共同作業へ与える影響を説明しています。
公開前は「Git・生成物・ブラウザ」でそれぞれ確認する
今回のサンプルでは、Gitに入れなかった値が、ビルドを通してブラウザへ届くことを確認できました。公開前には、次の順番で確認すると、何を調べ終えたかが分かります。
- Git:.envなどが追跡対象になっていないか。
- 生成物:公開するdistなどへ、秘密として扱う値を入れていないか。
- ブラウザ:配信ファイル、通信のURL・ヘッダー・応答に、秘密を渡していないか。
GitHubへ保存する準備は初回pushまでの手順へ、公開の準備ができたらVercelでの公開手順へ進めます。公開URLを自分のドメインへ変えるか迷っている場合は、無料URL・独自ドメイン・サブドメインの選び方で、追加購入が必要かから確認できます。
確認した公式資料
公式資料の確認日と、このサンプルの検証日は2026年10月13日です。
