ASP.NET Core で Entity Framework Core(EF Core)と SQLite を使うとき、「UseSqlite("Data Source=db.db") と書いたらファイルはどこに作られるのか?」は、開発・本番で動く場所が変わるたびに必ず議題になります。本記事では “作業ディレクトリ(ワーキングディレクトリ)” をキーに、IIS、Windows サービス、Docker、コンソール/Worker、Azure など代表的なホスティング形態での保存場所を具体例と実装パターン付きで徹底解説します。
結論と最短回答
接続文字列が Data Source=db.db のように相対パス(ファイル名のみを含む)なら、SQLite ファイルは アプリの「作業ディレクトリ」 に作成されます。 開発時は多くの場合「プロジェクトのルート(.csproj が置かれたフォルダ)」、シングルファイル配布や一部のホスティングでは「実行ファイルが置かれたフォルダ」やサービスのカレントディレクトリになります。確実に場所を制御したい場合は、絶対パスを使うか、IHostEnvironment.ContentRootPath などで相対パスを自前で解決してください。
// 例:最短で「Data」サブフォルダに明示保存(推奨パターン)
builder.Services.AddDbContext<AppDbContext>(options =>
{
var env = builder.Environment; // IHostEnvironment
var dbPath = Path.Combine(env.ContentRootPath, "Data", "db.db");
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
options.UseSqlite($"Data Source={dbPath}");
});
「作業ディレクトリ」とは何か ― 3つの基準パスを整理
パスの混乱は「基準点」を混同することから始まります。ASP.NET Core では次の 3 つを押さえましょう。
Environment.CurrentDirectory:プロセスの作業ディレクトリ。相対パスのData Source=相対パスはここから解決されます。IHostEnvironment.ContentRootPath(旧IWebHostEnvironmentにも同名):アプリの「コンテンツルート」。appsettings.jsonなどの標準ファイルや Razor コンパイルの基準。多くのテンプレートでCurrentDirectoryと一致しますが、必ず一致するとは限りません。AppContext.BaseDirectory:実行アセンブリの配置ディレクトリ。シングルファイル配布や Windows サービスで頼れることが多い基準点。
相対パスの接続文字列は、原則として Environment.CurrentDirectory から解決されます。ホスティングによりここが変わるため、「ローカルでは動くのに本番で見失う」現象が起こります。
代表的なホスティングごとの保存先の実際
次の表は、UseSqlite("Data Source=db.db") と書いたときに どこに作られがちか をまとめたものです。環境準備やテンプレートの違いでブレるので、後述の「実際の解決結果をログする」テクニックで必ず確認しましょう。
| ホスティング形態 | 作業ディレクトリの傾向 | 生成される想定パス(例) | 備考/注意 |
|---|---|---|---|
Visual Studio/dotnet run(開発) | プロジェクトのルート | C:\MyProject\db.db | builder.Environment.ContentRootPath ≒ プロジェクトルート。 |
| IIS(In-Process) | アプリの配置フォルダ | C:\inetpub\wwwroot\MyApp\db.db | アプリプールの書き込み権限が必要。 |
| IIS(Out-Of-Process / Kestrel背後) | 配置フォルダまたは System32 ではない設定 | C:\sites\MyApp\db.db | 近年のテンプレートでは System32 にはならないが、明示解決が安全。 |
| Windows サービス(Worker/Web) | サービスの実行フォルダまたは AppContext.BaseDirectory | C:\Services\MyApp\db.db | 起動方法により CurrentDirectory がズレることがある。 |
| Docker(Linuxコンテナ) | コンテナの WORKDIR | /app/db.db | 永続化は VOLUME や bind mount を使う。 |
| Azure App Service(Linux/Windows) | アプリのホーム/サイトディレクトリ | D:\home\site\wwwroot\db.db 等 | 書き込み可能領域を選ぶ。ローカル一時領域は揮発性に注意。 |
相対パス/絶対パスの指定とその効果
絶対パスを使う(もっとも明確)
options.UseSqlite(@"Data Source=C:\Data\db.db"); // Windows
options.UseSqlite("Data Source=/var/lib/myapp/db.db"); // Linux
- 保存先を完全に固定できます。
- デプロイ先のフォルダ権限を事前に設定しておく必要があります。
相対パスを使う(作業ディレクトリ基準)
options.UseSqlite("Data Source=Data/db.db"); // or Data\\db.db on Windows literal
- ディレクトリは自動では作成されないので、
Directory.CreateDirectoryが必要です。 - ホスティングにより基準(作業ディレクトリ)がずれます。
安全確実:パスを自分で生成してから渡す(推奨)
運用でのトラブルを根本から減らすには、「相対 → 絶対」を自分で解決してから接続文字列へ埋め込みます。以下は最小かつ堅牢なパターンです。
// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
{
string root = builder.Environment.ContentRootPath; // プロジェクト/アプリの基準
string dbDir = Path.Combine(root, "Data");
Directory.CreateDirectory(dbDir); // 無ければ作る
string dbPath = Path.Combine(dbDir, "db.db");
options.UseSqlite(new SqliteConnectionStringBuilder
{
DataSource = dbPath,
Mode = SqliteOpenMode.ReadWriteCreate, // 必要に応じて
Cache = SqliteCacheMode.Shared // 複数接続時の安定化に寄与
}.ToString());
});
文字列連結ではなく Path.Combine を使う理由は、OS ごとの区切り文字の違いや、末尾スラッシュの有無を意識せずに済むからです。
appsettings.json 管理と「再解決」テクニック
接続文字列を構成ファイルで管理するのは定番です。ただし Data Source=Data/db.db のような相対パスをそのまま使うと本番で迷子になりやすいので、起動時に再解決しましょう。
{
"ConnectionStrings": {
"Default": "Data Source=Data/db.db"
}
}
// Program.cs
using Microsoft.Data.Sqlite;
var cs = builder.Configuration.GetConnectionString("Default")!;
var b = new SqliteConnectionStringBuilder(cs);
// Data Source が相対なら ContentRootPath から絶対化
if (!Path.IsPathRooted(b.DataSource))
{
var rooted = Path.Combine(builder.Environment.ContentRootPath,
b.DataSource.Replace('/', Path.DirectorySeparatorChar));
Directory.CreateDirectory(Path.GetDirectoryName(rooted)!);
b.DataSource = rooted;
}
builder.Services.AddDbContext(opt => opt.UseSqlite(b.ToString()));
このやり方なら、appsettings.Development.json と本番で同じ値を持ちながら、実行時に安全な場所へ収めることができます。
フォルダ権限と運用上の注意
Windows(IIS/サービス)
- IIS の場合はアプリプールの実行ユーザー(既定では
IIS AppPool\{AppPool名})に書き込み権限を付与。 - サービスの場合はサービス実行ユーザー(ローカルサービス/仮想アカウントなど)に付与。
- 権限はフォルダ単位で。ファイル単体にだけ権限を付けても、新規作成時に失敗します。
Linux/コンテナ
chownとchmodで実行ユーザーにrwx(少なくともrw)を付与。- Docker は
WORKDIRとVOLUME(または bind mount)で永続化を設計。コンテナ内部だけに置くとコンテナ破棄で消えます。
SQLite 特有の運用 Tips(WAL・ReadOnly・ネットワーク共有)
- WAL モード(ジャーナル):同時アクセスが多いなら
PRAGMA journal_mode=WAL;を初期化時に実行すると読み取り同時性が向上します。 - ReadOnly:読み取り専用で開くなら
Mode=ReadOnlyを併用。誤更新の事故を防げます。 - ネットワーク共有:SQLite はローカルディスク前提のロック機構です。SMB/NFS 越しの更新はロック競合やパフォーマンス低下の原因になるため避けます。
// 例:WAL を使う初期化(アプリ起動時に一度だけ)
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.OpenConnection();
db.Database.ExecuteSqlRaw("PRAGMA journal_mode=WAL;");
}
マイグレーションとファイル作成タイミング
EF Core では dotnet ef database update(または Database.Migrate())を実行した その時点の接続文字列 を基にファイルが作成されます。つまり、マイグレーション実行時の作業ディレクトリが最初の配置場所になります。実運用では、Database.Migrate() をアプリ起動時に行うか、CI/CD のデプロイスクリプトで 期待する作業ディレクトリ に切り替えてからコマンドを実行しましょう。
// 起動時に自動マイグレーションする例(開発〜小規模向け)
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.Migrate();
}
相対パスの落とし穴と対策チェックリスト
- ディレクトリ未作成:
Directory.CreateDirectoryを忘れない。 - バックスラッシュとエスケープ:C# の文字列リテラルは
"C:\\Data\\db.db"または逐語的文字列@"C:\Data\db.db"を使う。 - CurrentDirectory の変動:サービス/IIS/ジョブスケジューラでは変わり得る。自前解決で固定化。
- 相対パスの二重解決:
appsettings.jsonに相対パスを書いたら、必ず起動時に自分で絶対化してから渡す。 - |DataDirectory| トークン:ADO.NET の一部プロバイダだけの慣習です。Microsoft.Data.Sqlite を使うなら安易に頼らず、上記の自前解決を採用。
実際の解決結果(どこに作られたか)をログで可視化する
「どこに作られたか」を確信する最短の方法は、実行時に最終的な接続文字列をログに出すことです。
// DbContext.OnConfiguring などで
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
optionsBuilder.UseLoggerFactory(loggerFactory);
}
}
// 実際の Data Source を取り出して確認
var conn = context.Database.GetDbConnection();
var builder = new SqliteConnectionStringBuilder(conn.ConnectionString);
logger.LogInformation("SQLite DataSource => {Path}", builder.DataSource);
具体的なコードパターン集
パターンA:ContentRoot 配下の「Data」フォルダに固める
var root = builder.Environment.ContentRootPath;
var dbPath = Path.Combine(root, "Data", "db.db");
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
builder.Services.AddDbContext(o => o.UseSqlite($"Data Source={dbPath}"));
パターンB:環境変数で上書き可能にする(12-factor)
// appsettings.json : "ConnectionStrings:Default": "Data Source=Data/db.db"
var raw = builder.Configuration.GetConnectionString("Default")!;
var sb = new SqliteConnectionStringBuilder(raw);
var envPath = Environment.GetEnvironmentVariable("APP_DB_PATH");
if (!string.IsNullOrWhiteSpace(envPath)) sb.DataSource = envPath;
if (!Path.IsPathRooted(sb.DataSource))
sb.DataSource = Path.Combine(builder.Environment.ContentRootPath, sb.DataSource);
Directory.CreateDirectory(Path.GetDirectoryName(sb.DataSource)!);
builder.Services.AddDbContext(o => o.UseSqlite(sb.ToString()));
パターンC:Docker でボリューム永続化
# Dockerfile(抜粋)
WORKDIR /app
# 最終イメージに app を配置済みとして…
VOLUME ["/data"] # 永続データ用
// コンテナ内では /data 配下に作る
var dbPath = Path.Combine("/data", "db.db");
Directory.CreateDirectory("/data");
builder.Services.AddDbContext<AppDbContext>(o => o.UseSqlite($"Data Source={dbPath}"));
# 実行時にホストへ永続化
docker run -d -p 8080:8080 -v C:\MyAppData:/data myimage:latest
パターンD:Windows サービスで AppContext.BaseDirectory 起点
var baseDir = AppContext.BaseDirectory;
var dbPath = Path.Combine(baseDir, "db.db");
builder.Services.AddDbContext<AppDbContext>(o => o.UseSqlite($"Data Source={dbPath}"));
トラブルシューティング(エラー別の原因と対策)
| 症状 | 主な原因 | 対策 |
|---|---|---|
SQL logic error: unable to open database file | フォルダが無い/権限が無い/相対パスが誤基準 | CreateDirectory を追加/権限付与/絶対化して固定 |
attempt to write a readonly database | ファイル/フォルダが読み取り専用、または Mode=ReadOnly | ACL/パーミッションを修正/接続文字列のモードを見直し |
database is locked | 長時間の書き込みトランザクション/同時更新 | トランザクション粒度を小さく/WAL 有効化/リトライポリシー |
| 開発では動くが本番で「見つからない」 | 作業ディレクトリが異なる | ContentRootPath 起点に自前で絶対化して渡す |
ベストプラクティス要点
- 保存場所はコードで決める:相対のまま EF に委ねない。
Path.Combine + Directory.CreateDirectoryをセットで。 - 構成は 1 箇所:
appsettings.jsonの相対値は起動時に絶対化し、アプリ全体で同じインスタンスを使う。 - 書き込み権限を先に設計:IIS/サービス/コンテナで「誰が書くのか」を決め、デプロイ手順に含める。
- 永続化戦略:コンテナはボリューム前提。PaaS は書き込み可能なホーム領域を使用。
- 運用ログ:起動時に
DataSourceを INFO ログ出力しておく。
よくある質問(FAQ)
Q. UseSqlite("Data Source=db.db") で作られる場所は常に「プロジェクトのルート」ですか?
A. いいえ。作業ディレクトリに作られます。開発では多くの場合プロジェクトルートに一致しますが、IIS、サービス、コンテナ、クラウドでは異なることがあります。
Q. |DataDirectory| トークンは使えますか?
A. 一部の ADO.NET プロバイダの文化です。EF Core の標準的な Microsoft.Data.Sqlite を使う前提では、自前で絶対化する設計を強く推奨します。
Q. EnsureCreated() と Migrate() はどちらを使えば?
A. スキーマ進化を伴う運用では Migrate() を推奨。EnsureCreated() は最初期のプロトタイプ向けです。
Q. ファイルをアプリと同じ場所に置くのは安全ですか?
A. 小規模・単体インスタンスなら実務上問題ないケースが多いですが、バックアップやローテーション、権限境界を考えると C:\Data のように データ専用のフォルダ を切り出す方が管理しやすくなります。
Q. 本番でパスを切り替えたい
A. 環境変数や KeyVault で接続文字列を上書きし、それが相対なら起動時に絶対化するフローを組み込みます(前掲のパターンB)。
サンプル:最小の ASP.NET Core + EF Core + SQLite
// Program.cs(.NET 8 以降の最小ホスト)
var builder = WebApplication.CreateBuilder(args);
// DB パスを明示
var dbPath = Path.Combine(builder.Environment.ContentRootPath, "Data", "db.db");
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
builder.Services.AddSqlite($"Data Source={dbPath}");
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
// 最初にマイグレーション
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService();
db.Database.Migrate();
}
app.MapGet("/", (AppDbContext db) => db.Items.ToList());
app.Run();
// DbContext
public class AppDbContext : DbContext
{
public DbSet Items => Set();
public AppDbContext(DbContextOptions options) : base(options) { }
}
public class TodoItem
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
}
まとめ
- 相対パスは作業ディレクトリ基準で解決される。環境により変わる。
- 絶対パス or 自前絶対化で保存場所を統一する。
- フォルダ作成と権限付与をデプロイ手順に含める。
- Docker/クラウドでは永続化ストレージを設計から組み込む。
- ログで DataSource を常に確認。問題の切り分けが速くなる。
この原則を守れば、「どこに作られた?」で悩む時間はゼロになります。SQLite の強みである「単一ファイル」を味方に、配布・バックアップ・移行をシンプルに保ちましょう。

コメント