この記事でわかること
- Rust の環境構築 2026最新版
- axum + tauri で作るデスクトップアプリ開発
はじめに
Webアプリケーションを開発していると、次のように考えたことがある方はいるのではないでしょうか?
このWebアプリケーション、デスクトップアプリにして配布したいな。
しかし、現実的にPython, Node.js などの実行環境を配布先の環境に入れて、、、いや、Docker?、、、というのはかなり大変です。
そこで、Rust 言語により開発した既存のWebアプリケーション資産を活かしながら、デスクトップアプリを作ることについての知見を記事にしました。
この構成、過去に Electron + Express の構成で自身が開発した個人プロジェクトの経験がベースとなっています。
しかし、インストーラも重たく、Chromeを内包するため、継続的なセキュリティアップデートが求められます。
時は流れ、Rust 製のフレームワーク axum で Webサーバを開発していた時、
あれ?これ Tauri で配布すれば、良いのでは。。。
と思いつきました。
axum の API サーバーを起動し、それを Tauri が読み込む流れ、つまり同一プロセスの中で立ち上げる構成です。
しかし、思うは易し、Tauri のセキュリティ機構は非常に厳しく(故に強い)、なかなか思うような処理が実装できませんでした。
そんな試行錯誤の手法を公開し、2026年最新のRust環境構築手順とあわせて、お送りします。
本題
さて、この記事では、axum が返す HTML を Tauri のウィンドウでそのまま表示する、最小構成のサンプルを再現できる形で紹介します。環境は Windows です。
今回の完成形は次のとおりです。
-
axumがhttp://localhost:8000/indexで HTML を返す -
Tauriがアプリ起動時にaxumを立ち上げる - サーバーの準備ができたら
Tauriのウィンドウでhttp://localhost:8000/indexを開く - DB や認証、テンプレートエンジンは使わない
SQLite を使用する場合
MarkownWiki2-SingleBin では SQLite をデータベースに使用しています。
DBの作成、マイグレーションまでを見たい方はご参照ください。
え?Macがサポートされてない?では、ぜひ、一緒に開発しましょう。
ディレクトリ構成
あらかじめ、リポジトリを用意しましたので、概要だけ把握したい方は、クローンして使用してください。
/
├─ Cargo.toml
├─ build.rs
├─ tauri.conf.json
├─ dist/
│ └─ index.html
└─ src/
└─ main.rs
1. Windows で Rust / Tauri のビルド環境を用意する
このリポジトリで cargo tauri build を実行できるようにするには、先に Windows で Rust の開発環境と Tauri Cli を用意する必要があります。
1-1. Rust の開発環境を構築
Rust 公式 より RUSTUP-INIT.EXE(x64) をダウンロードし、実行します。
rustup-init.exe を実行すると、次のようなプロンプトが現れますので、 玄人以外は黙って 1 を入力します。
インストールが少し進むと Visual Studio Installer が起動するので、これを実行します。この過程で Microsoft C++ Build Tools が導入されます。
Visual Studio Installer によるインストールが完了したら、Rust インストールのプロンプトに戻るので、 1 を入力します。
ここまでで、Rust 開発環境の構築が完了です。
導入確認:
rustc --version
cargo --version
cargo が見つからない場合は、PowerShell を再起動してからもう一度確認してください。改善しないときは %USERPROFILE%\.cargo\bin が PATH に入っているか確認します。
1-2. Tauri CLI をインストールする
cargo tauri build は tauri-cli を入れると使えるようになります。
cargo install tauri-cli --version "^2.0.0" --locked
導入確認:
cargo tauri --version
ここまで終わったら、リポジトリのルートで次を実行します。
cargo tauri build
ビルドが完了すると target/release/bundle 配下に nsis と msi それぞれのインストーラが作成されます。
補足ですが、インストーラを作成しない場合は tauri.conf.json の bundle.active を false にします。これにより、cargo tauri build を実行してもインストーラーは生成せず、バイナリのみが作成されます。
まずは cargo tauri build コマンドが最後まで通れば、Windows 上の Rust + Tauri のビルド環境は整ったと考えて大丈夫です。
2. Cargo.toml を用意する
まずは axum、tokio、tauri を追加します。今回のサンプルは HTML を 1 本返すだけなので、依存関係も最小限です。
[package]
name = "axum_tauri_example"
version = "0.1.0"
edition = "2024"
[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
tauri = { version = "2", features = [] }
[build-dependencies]
tauri-build = { version = "2", features = [] }
合わせて build.rs も作成します。
fn main() {
tauri_build::build();
}
3. Tauri の設定ファイルを書く
tauri.conf.json にはアプリ名や識別子を設定します。今回はウィンドウを Rust 側で組み立てるので、app.windows は空で問題ありません。
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "axum-tauri-example",
"version": "0.1.0",
"identifier": "com.example.axum-tauri-example",
"build": {
"frontendDist": "dist"
},
"bundle": {
"active": false,
"icon": ["./res/icon.ico"]
},
"app": {
"windows": [],
"withGlobalTauri": true,
"security": {
"csp": null
}
}
}
frontendDist は Tauri の設定上必要になるので、dist/index.html をダミーで置いています。今回の画面は axum 側の http://localhost:8000/index を直接開くため、この HTML 自体は使いません。あわせて Windows ビルド時に必要なアイコンとして、親プロジェクトの res/icon.ico を再利用しています。
4. axum と Tauri を同時に起動する
src/main.rs では、起動順序を次のようにしています。
-
axum用のRouterを作る - Tauri の
setupでバックグラウンドにaxumを起動する - サーバーの待ち受け準備が終わったら
WebviewWindowを作る - ウィンドウが閉じられたら
axumにシャットダウンを送る
ポイントは、サーバー起動前にウィンドウを開かないことです。これを守るだけで、localhost に接続できず白画面になる事故を避けられます。
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use axum::{Router, response::Html, routing::get};
use std::{
sync::{Arc, Mutex, mpsc},
time::Duration,
};
// 重要
const SERVER_ADDR: &str = "127.0.0.1:8000"; // v4 addr で起動
const WINDOW_URL: &str = "http://localhost:8000/index"; // localhost でアクセス
// axum シャットダウン信号を保持する Tauri 管理状態
struct ShutdownState(Arc<Mutex<Option<tokio::sync::oneshot::Sender<()>>>>);
async fn index_handler() -> Html<&'static str> {
let html_text = r#"
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>axum + tauri example</title>
</head>
<body>
<h1>axum + Tauri で構築するデスクトップアプリ</h1>
<p>このアプリは、Tauri と axum を使用して構築されています。</p>
<p>axum起動 -> tauri起動</p>
</body>
<style>
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
}
</style>
</html>
"#;
Html(html_text)
}
fn build_router() -> Router {
Router::new().route("/index", get(index_handler))
}
fn run_server_mode(bind_addr: String) {
println!("Server mode: binding to http://{bind_addr}");
let runtime = tokio::runtime::Runtime::new().expect("failed to create tokio runtime");
runtime.block_on(async move {
let listener = tokio::net::TcpListener::bind(&bind_addr)
.await
.expect("failed to bind tcp listener");
println!("Listening on http://{bind_addr}");
println!("Press Ctrl+C to stop the server.");
axum::serve(listener, build_router())
.with_graceful_shutdown(async {
tokio::signal::ctrl_c()
.await
.expect("failed to listen for Ctrl+C");
})
.await
.expect("axum server stopped unexpectedly");
});
}
fn main() {
let args: Vec<String> = std::env::args().collect();
if let Some(pos) = args.iter().position(|arg| arg == "-s") {
let ip_arg = args
.get(pos + 1)
.cloned()
.unwrap_or_else(|| "0.0.0.0".to_string());
let bind_addr = if ip_arg.contains(':') {
ip_arg
} else {
format!("{ip_arg}:8000")
};
run_server_mode(bind_addr);
return;
}
// ShutdownState を Arc で共有(on_window_event からも参照する)
let shutdown_inner = Arc::new(Mutex::new(None::<tokio::sync::oneshot::Sender<()>>));
let shutdown_for_event = Arc::clone(&shutdown_inner);
// tauri 構築
tauri::Builder::default()
.manage(ShutdownState(shutdown_inner))
.setup(move |app| {
// axum 起動完了通知チャネル
let (ready_tx, ready_rx) = mpsc::channel::<Result<(), String>>();
// シャットダウンチャネルを生成し State に格納
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
{
let state = app.state::<ShutdownState>();
let mut guard = state.0.lock().unwrap();
*guard = Some(shutdown_tx);
}
tauri::async_runtime::spawn(async move {
let listener = match tokio::net::TcpListener::bind(SERVER_ADDR).await {
Ok(listener) => listener,
Err(error) => {
let _ = ready_tx.send(Err(format!(
"failed to bind {SERVER_ADDR}: {error}"
)));
return;
},
};
// Tauriウィンドウ作成前にサーバ準備完了を通知
let _ = ready_tx.send(Ok(()));
// axum サーバ起動
if let Err(error) = axum::serve(listener, build_router())
.with_graceful_shutdown(async move {
shutdown_rx.await.ok();
})
.await
{
eprintln!("axum server stopped unexpectedly: {error}");
}
});
// axum が起動するまで最大30秒待機
ready_rx
.recv_timeout(Duration::from_secs(10))
.map_err(|error| std::io::Error::other(format!("server startup timed out: {error}")))?
.map_err(std::io::Error::other)?;
// メインウィンドウ作成
tauri::WebviewWindowBuilder::new(
app,
"main",
tauri::WebviewUrl::External(WINDOW_URL.parse().unwrap()), // 読み込み
)
.title("axum + tauri example")
.inner_size(900.0, 700.0)
.build()?;
Ok(())
})
.on_window_event(move |window, event| {
if window.label() != "main" {
return;
}
if let tauri::WindowEvent::Destroyed = event {
if let Ok(mut guard) = shutdown_for_event.lock() {
if let Some(tx) = guard.take() {
let _ = tx.send(());
}
}
}
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
5. 最小構成 axum + tauri
このサンプルは、
上記のアプリケーションの「先に axum を起動してから Tauri の画面を出す」という流れだけを残し、それ以外の要素を削っています。
- セットアップ画面を削除
- DB 初期化を削除
- 認証処理を削除
- Tera などのテンプレートエンジンを削除
- API は
/indexのみ
そのため、「Tauri の中に axum を内蔵する最小形」を理解する教材として扱いやすいように編集しました。
6. 実行方法
次のコマンドで axum + tauri を起動できます。
cargo run
正常に起動すると、デスクトップウィンドウが開き、次の HTML が表示されます。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>axum + tauri example</title>
</head>
<body>
<h1>axum + Tauri で構築するデスクトップアプリ</h1>
<p>このアプリは、Tauri と axum を使用して構築されています。</p>
<p>axum起動 -> tauri起動</p>
</body>
<style>
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
}
</style>
</html>
サーバー単体で動作確認したいときは、-s オプションを使います。
cargo run -- -s 127.0.0.1
この場合は http://localhost:8000/index にブラウザでアクセスして確認できます。
7. つまずきやすいポイント
- Tauri の設定上
frontendDistは必要なので、使わなくても最小のdist/index.htmlを置いておく
まとめ
axum + tauri の組み合わせは、Rust だけで API とデスクトップ画面の起動制御を完結させたいときに扱いやすい構成です。まずは今回のように localhost へ最小の HTML を返すところから始めると、起動順序や責務分離が理解しやすくなります。
この axum-tauri-example は、そのまま「DB や認証を足していく前の土台」としても使えます。まずは最小構成で動かし、そのあとで機能を 1 つずつ載せていくのがおすすめです。
宣伝
宣伝をお許してください!
実際に axum + tauri で開発した Wikiアプリケーション 👇
スライドも作れる!プレゼンもできる!マークダウンエディタ 👇