設定#
conf/application.conf に書きます。書式は HOCON です。
置き場所#
設定は conf/ に置きます。
conf/application.conf 設定
conf/application.local.conf 環境別
conf/migration/<スキーマ名>/ マイグレーション
build.gradle.kts で conf をリソースに足します。
sourceSets {
main {
resources {
srcDir("conf")
}
}
}
こうすると conf/ の中身はそのまま jar に入ります。
codegen / migrate / jimbleRun / テストも、同じファイルをクラスパスから見ます。
jimble が読むのはクラスパスだけです。 jar の外は見ません。 設定を変えたらビルドし直します。動いている jar と設定が1対1になります。
読むのは1ファイルだけです。
application.<env>.confがあればそれ- 無ければ
application.conf
どのファイルを読んだかは起動ログに出ます。
設定: jar:file:/opt/app/app.jar!/application.prod.conf
環境で切り替える#
共通を読み込むのは環境別ファイルの include です。
# application.prod.conf
include "application.conf"
db {
main {
url = ${?DB_URL}
}
}
application.conf 共通
application.prod.conf 本番。1行目で共通を読み、続きで上書きする
環境は -Djimble.env=prod で決まります。既定は local です。
jimble は裏で共通を足しません。 足すと、include が効いているのか
フレームワークが足しているのかがファイルを見ても分からなくなるからです。
読むファイルは1つ、続きは書いてあるとおりです。
include を書き忘れたら#
共通の設定が丸ごと落ちます。落ちたら起動時に名指しで言います。
設定: application.conf にしかないキーが読まれていません: cipher, session, server
/ application.prod.conf の先頭に include "application.conf" を書いてください
環境は変えずに、読むファイルだけ替える#
-Djimble.conf=application.batch を渡すと、環境(env)に関係なく application.batch.conf だけを読みます(2.5.3 から)。
環境はそのままなので、env = local なら isLocal() も true のままです。
同じアプリをバッチとして動かすときなど、環境は変えずに設定だけ替えたいときに使います。
java -Djimble.conf=application.batch -jar app.jar
- 拡張子は書いても書かなくてもかまいません
- 置き場所は環境別ファイルと同じ
conf/(クラスパスの中)です。jar の外は指せません - 見つからなければ起動時に落ちます(黙って
application.confを読みません) - 共通を読み込むのは、ここでもファイルの
include "application.conf"です
秘密はファイルに書かない#
db {
main_db {
url = "jdbc:mariadb://127.0.0.1:3306/app"
url = ${?DB_URL}
password = ""
password = ${?DB_PASSWORD}
}
}
${?ENV_NAME} は「環境変数があれば上書き、無ければ前の行のまま」です。
同じキーを2回書くのが正しい書き方です。
コメントは # か //#
HOCON に /* */ はありません。 書くとこうなります。
Key '/' may not be followed by token: '*'
設定キー#
ここに出ているのが、jimble が読むキーの全部です。 出ていないキーは読みません(打ち間違いは黙って無視されます)。 値は既定値なので、変えないものは書かなくて構いません。
補足
時間と大きさは、単位を値に書きます(30m / 200ms / 10MiB)。
素の数値は起動時に落ちます——assets.max_age = 3600000 のような取り違えを、
黙って通さないためです。使えるのは
ns / us / ms / s / m / h / d と、B / KiB / MiB / GiB です。
落とし穴
MB は 1000 の3乗、MiB は 1024 の3乗です(HOCON の決まり)。
10MB は 10,000,000 バイトで、10MiB は 10,485,760 バイトです。
書いたとおりに解釈します——どちらでも構いませんが、
既定値と比べるときは違うものだと思ってください。
server {
host = "" # 待ち受けるアドレス。空なら全部
port = 9000
max_request_size = 10MiB # リクエスト本文の上限
max_header_size = 16KiB # ヘッダ全体の上限
idle_timeout = 60s # 何もしない接続を閉じるまで
compression = true # 文字の応答(1KB 以上)を gzip で返すか
trust_proxy = false # ロードバランサの後ろに置くまで false(下を参照)
access_log = true # 切ると速くなるが、何が起きたか残らない
bot_access_log = true # ボットのアクセスログを分けるか
access_log_mask = [] # アクセスログのクエリで値を伏せる名前(token などは書かなくても伏せる)
strict_routes = false # 一生呼ばれないルートを例外にする(CI では true に)
shutdown_grace = 0s # 止め始めてから新規を断つまで
shutdown_timeout = 15s # 処理中を待つ上限
backlog = 1024 # 受け付け待ちの接続を OS に持たせる数
write_queue_length = 0 # 応答を書き出す列の長さ。0/1 は「列を作らない」
smart_async_writes = false # 列があるとき、空いていればその場で書く
tcp_no_delay = true # TCP_NODELAY(Nagle を切り、小さく書いたものもすぐ送る)
}
metrics {
enabled = true # メトリクスを数えるか
}
router {
ignore_case = false # パスの大文字小文字を区別しないか
redirect_to_canonical = false # 正規の URL へ 301 で寄せるか
}
cookie {
secure = true # HTTPS のみ。ローカルでは false(下を参照)
http_only = true # JavaScript から読めない
same_site = "lax" # none | strict | lax
domain = "" # 空なら発行元ドメイン
max_age = 365d # 0 以下でセッション Cookie
secret = ${?COOKIE_SECRET} # 署名鍵。空なら署名しない
previous_secrets = [${?COOKIE_SECRET_OLD}] # 鍵の入れ替え中だけ
accept_unsigned = false # 署名を入れる移行期間だけ true にする
accept_legacy_signature = false # 名前に結びついていない 2.2.2 までの署名も読むか(2.2.2 以前から上げる間だけ true)
}
csrf {
max_age = 1d # トークンの寿命。cookie.max_age とは別
bind_session = false # true でトークンをセッションに置き、ログインで作り直す(session.store が要る)
}
session {
store = "none" # none | db | redis | cookie
timeout = 30m
absolute_timeout = 1d # 発行からの上限(使い続けても延びない)。cookie は既定 1d。db / redis は書いたときだけ効く
cookie_name = "sid"
table = "session" # store = db のとき
secret = ${?SESSION_SECRET} # store = cookie のとき必須
previous_secrets = [${?SESSION_SECRET_OLD}] # 鍵の入れ替え中だけ
}
upload {
max_file_size = 10MiB # 1ファイル
max_total_size = 10MiB # 1リクエスト合計(server.max_request_size と揃える)
max_files = 20 # 1リクエストのファイル数
temp_dir = "" # 空なら java.io.tmpdir
}
assets {
max_age = 0s # Cache-Control の max-age
immutable_max_age = 365d # 不変扱い(js / css)のファイル
if_modified_since = true # If-Modified-Since を見るか
etag = true # ETag / If-None-Match を使うか
}
template {
package = "gg.jte.generated.precompiled" # 事前コンパイルの出力先
content_type = "text/html; charset=utf-8"
json_fallback = false # view() のページに Accept: application/json で来たら、テンプレートに渡したデータを JSON で返すか
}
security_headers {
enabled = true
content_type_options = "nosniff" # 空なら付けない
frame_options = "SAMEORIGIN" # 別のサイトの iframe に入れさせない
referrer_policy = "strict-origin-when-cross-origin"
hsts = 0s # Strict-Transport-Security の max-age。0s なら付けない(https で受けたときだけ付ける)
hsts_include_subdomains = false
}
paging {
name_page = "page" # ページ番号のリクエストパラメータ名(件数ではない)
name_per = "per" # 取得件数のリクエストパラメータ名
max_per = 200 # 1ページの上限。per=all にも効く(0 で上限なし)
}
auth {
full_auth_max_age = 0s # Auth.FULL_AUTH のルートを、パスワードを入れてからこれだけの間だけ通す。0s なら切らない
lockout {
enabled = true # DB が無ければ何もしない
free_attempts = 3 # ここまでは待たされない(打ち間違い)
base = 1s # 4回目から 1 → 2 → 4 …
max = 5m # 待ち時間の上限
forget = 24h # これだけ間が空いたら数え直す
}
remember {
enabled = true # DB が無ければ何もしない
cookie_name = "remember"
sliding = 30d # 最後に使ってからこれだけ
absolute = 90d # 発行してからこれを超えたら、使っていても切れる
grace = 60s # 回した直後、古いほうも通す時間
}
revocation {
enabled = true # DB が無ければ比べない(Auth.revoke は例外)
cache_ttl = 5s # 締め出しがあったかを見に行く間隔。複数台ではほかの台で効くまでの遅れ。0s で毎回引く
}
mfa {
enabled = true # DB と secret_key が要る(下を参照)
issuer = "" # 認証アプリの一覧に出る名前
digits = 6 # 6 から変えない(多くのアプリが6桁しか出せない)
period = 30 # 秒。RFC 6238 のパラメータなので、ここだけ数値
window = 1 # 前後いくつの窓まで許すか。広げるほど当たりも増える
recovery_codes = 10 # 登録のときに出す数
pending = 5m # パスワードが通ってからコードを入れるまでの猶予
# 秘密鍵を暗号化する鍵。無ければ Mfa.enroll が断る。
# cipher.key を流用しないこと(下を参照)
secret_key = ${?MFA_SECRET_KEY}
}
passkey {
enabled = true
rp_id = "" # パスキーを結びつけるドメイン(あとから変えると登録したものが全部使えなくなる)。クライアントごとに違うなら PasskeyRp を渡す
rp_name = "" # 登録のときにブラウザが出す名前(空なら rp_id)
origins = [] # 受け付けるオリジン(空なら https:// + rp_id)
timeout = 5m # 始めてから終えるまで
}
# プロバイダごとに1ブロック。名前(google)は Oidc.callback に渡す名前
oidc {
google {
issuer = "https://accounts.google.com"
client_id = ${?GOOGLE_CLIENT_ID} # 環境変数から
client_secret = ${?GOOGLE_CLIENT_SECRET} # 設定ファイルに書かない
redirect_uri = "https://example.com/auth/google/callback"
# 省略できる。issuer から discovery で引く
# authorization_endpoint = "..."
# token_endpoint = "..."
# jwks_uri = "..."
scopes = "openid email profile"
clock_skew = 60s # 時計のずれをどこまで許すか
discovery_ttl = 1h # discovery と JWKS を持つ時間
}
}
}
cipher {
# 移送してきたアプリの、暗号化済みパスワードハッシュを読むための鍵。
# 二要素認証には使わない(下を参照)
key = ${?CIPHER_KEY} # 16 / 24 / 32 バイト
iv = ${?CIPHER_IV} # 16 バイト
}
hash {
password {
# cipher.* を書くなら、これも必ず書く(書かないと起動時に落ちる)
encrypt = false
pepper = ${?PASSWORD_PEPPER} # 1.x との互換のためだけ。守りにならない(下記)。入れ替えられない
cost = 10 # bcrypt のコスト(4〜31)。1 上げると倍の時間。72 バイトより後ろは bcrypt が読まない
}
}
rate_limit {
enabled = true
store = "db" # db | memory | redis
}
cache {
type = "db" # db | memory | redis
temp_dir = "" # ファイルを置くところ
memory.expire = 0s # memory のときだけ。0 で無期限
}
mail {
transport = "smtp" # smtp | log(送らずにログ。手元用)| memory(テスト用)
from = "" # From を書かないメールの差出人
from_name = ""
smtp {
host = "" # smtp なら必須(書かずに送ると例外)
port = 587 # 書かなければ security から(starttls 587 / tls 465 / none 25)
security = "starttls" # starttls | tls | none(none では認証できない)
username = ""
password = ${?SMTP_PASSWORD}
connect_timeout = 10s
timeout = 30s
helo = "" # EHLO で名乗る名前(空ならこの機械の名前)
envelope_from = "" # バウンスの宛先(空なら From)
}
}
sql_cache {
enabled = false
store = "memory" # memory | redis | db
ttl = 5m
max = 10000 # 件数の上限(memory のみ)
namespace = "" # 1つの Redis を複数のアプリや環境で分け合うなら、それぞれ別の値に
}
redis {
host = "" # 空なら Redis 無し
port = 6379
ssl = false
username = "" # Redis 6 の ACL の利用者。空なら送らない
password = ${?REDIS_PASSWORD} # 空なら送らない
settings {
connection_timeout = 10s
timeout = 3s # コマンドの応答を待つ上限
connection_minimum_idle = 24
connection_pool_size = 64
retry_attempts = 3
retry_minimum_interval = 500ms
retry_maximum_interval = 2s
idle_connection_timeout = 10s
subscription_connection_minimum_idle_size = 1
subscription_connection_pool_size = 50
}
}
db {
# 全部 null のテーブルを結果から落とすか。
# データソースの名前ではないので、この位置に書く
remove_all_null_table_data = false
# データソースごとに1ブロック。名前(blog)が DB クラスの名前になる
blog {
main = true # 主データソースにするか
driver = "com.mysql.cj.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/blog"
username = ${?DB_USER}
password = ${?DB_PASSWORD}
product = "mysql" # mysql | mariadb | postgresql | postgres | pgsql
schema = "" # 空ならブロックの名前。以前は scheme という綴りだった
maximum_pool_size = 10 # プールの最大
minimum_idle = 1 # 常に開けておく数
fetch_size = 100 # 一度に取り出す行数
idle_timeout = 10m # 使っていない接続を閉じるまで
max_lifetime = 30m # 1本の接続の寿命
connection_timeout = 30s # 接続を待つ上限
keepalive_time = 30s # 生存確認の間隔(Agroal は裏の定期確認と、これより長く寝ていた接続を渡す前の確認)
leak_timeout = 0s # これより長く握られた接続をリークの疑いとして警告する。0s なら見ない
validate_on_borrow = false # 取り出すたびに生存確認する(Agroal だけ。HikariCP はいつも確かめる)。速さが半分ほどになる。DB の再起動に備えるだけなら要らない(切れた接続は例外を見て捨てる)
connection_init_sql = "" # 接続直後に流す SQL
connection_test_query = "" # 生存確認の SQL
connection_pool_type = "hikari" # hikari | agroal
transaction_isolation = "" # 分離レベル
create_database_sql = "" # 無ければ作るときの SQL
long_connection_log = false # 長く握った接続をログに出すか
long_connection_time = 0s # それを「長い」とみなす時間
# 読み取り専用の接続。書かなければ上と同じところを読む
read { url = "jdbc:mysql://127.0.0.1:3307/blog" }
# シャード。中身は上と同じキー
subs {
shard1 { url = "jdbc:mysql://127.0.0.1:3308/blog" }
}
}
}
db_sticky {
use = false # 書いたあと、同じリクエストの読みを書き側に寄せるか
}
log {
db = false # SQL をログに残すか
}
async {
prefetch {
on_response = false # レスポンスを送る前に自動で先読みするか
max_depth = 5 # 先読みの周回の上限
}
}
migration {
on_startup = "auto" # auto | true | false。auto はローカル以外で当てる
down = false # down を実行するか
lock_timeout = 60s # ロック待ちの上限
resource_dir = "migration" # SQL ファイルのリソースディレクトリ
}
codegen {
package = "db" # 生成先パッケージ
exclude_tables = [] # 生成対象から外すテーブル
}
mq {
poll_min = 10ms # キューが空のときの取り出し役の待ちの最初(ワーカーの手が空いたら、待たずに取りにいく)
poll_max = 1s # キューが空のときの取り出し役の待ちの上限(だんだん伸びる)
retry_backoff = 10s # リトライの間隔(回を追うごとに倍)
retry_backoff_max = 10m
stale = 10m # これだけ running のままなら落ちたとみなす(waiting に戻し、リトライの1回と数える)
# 実行種別ごとのワーカーの数(並んで処理する数)。書かなければ種別ごとの既定。
# DB から取るのはキューごとに1本の取り出し役なので、増やしても待っているときの接続は増えない。
# 取り出し役は手の空いたワーカーの数だけまとめて取るので、増やせば秒あたりの件数も伸びる。
# プール(db.*.maximum_pool_size)は、ワーカーの数+取り出し役の1本より多くしておく
thread_count {
short_time = 2
long_time = 8
}
}
scheduler {
reload_interval = 10s # batch_master を読み直す間隔
tick_interval = 1s # cron を確かめる間隔
exit_check = 3s # 止められていないか確かめる間隔
execute_threads = 10 # バッチを走らせるスレッド数(0 で無制限)
queue_name = "mq_scheduler"
}
batch {
scheduler_id = "" # このインスタンスの識別子。空ならホスト名
heartbeat = 3s # 実行中であることを知らせる間隔
alive = 10s # これだけ更新が無ければ「実行中ではない」とみなす
cancel_check = 3s # 中断指示を見にいく間隔
progress = 5s # チャンクバッチが進み具合を履歴に書く間隔
all_stop = 1h # 全停止フラグが効く時間
}
batch_manager {
enabled = false # 利用者名かパスワードが空だと画面が生えない
path = "/batch-manager"
realm = "jimble batch manager"
username = ${?BATCH_MANAGER_USER}
password = ${?BATCH_MANAGER_PASSWORD}
}
proxy {
connect_timeout = 5s # 転送先に繋ぐまでの上限
request_timeout = 30s # 送るときに進まない待ち・応答のヘッダが届くまで(全体)・本文の1回の読み込み
body_timeout = 0s # 応答の本文の全体の上限。0 なら上限なし(ダウンロードや SSE を切らない)
}
sse {
max_duration = 5m # 1本を張っていられる上限(0 以下で無制限)
max_events = 0 # 送れる件数の上限(0 以下で無制限)
retry = 3s # 切れたクライアントがどれだけ後で繋ぎ直すか
}
mcp {
path = "/mcp"
name = "jimble" # サーバーの名前
version = "0.1.0" # サーバーの版(MCP の仕様の版ではない)
instructions = "" # モデルへの手引き(server/discover で返す)
page_size = 100 # 一覧の1ページの件数
allowed_origins = [] # 許すオリジン
}
jimble {
io.buffer_size = 64KiB # ファイル・ストリームを送るときに1回に写す大きさ/JSON を溜める上限
read_only_container = false # 書き込めないコンテナで動かすか
# server.port は -Djimble.server.port=8080 でも指定できる(システムプロパティが優先)
}
cipher.key と auth.mfa.secret_key は別のもの#
二要素認証に cipher.key を流用しないでください。
cipher.* を書いたら、hash.password.encrypt も必ず書いてください。
書いていないと起動時に落ちます。
以前は「cipher.key と cipher.iv の両方が揃っていれば true」という既定でした。
つまり cipher.key だけ足したアプリは false のままで、
あとから cipher.iv を足した瞬間に反転しました——
保存済みの BCrypt が「暗号化済み」として読まれ、全員入れなくなります。
出るのは「IDかパスワードが違います」だけなので、設定を足したことと結び付きません。
**離れたキーで決まる既定は置きません。**暗号化を使うなら、そう書いてもらいます。
cipher.key / cipher.iv |
移送してきたアプリの暗号化済みパスワードハッシュを読むためのもの。新しく暗号化するものには使いません(固定 IV で、改ざん検知もありません) |
auth.mfa.secret_key |
TOTP の秘密鍵を暗号化するためのもの。AES-256-GCM です |
cipher.* をまったく書いていないアプリは、これまでどおり何も書かなくて構いません
(暗号化なし、BCrypt だけ)。
trust_proxy は既定で false#
true にすると X-Forwarded-For を信じます。
ロードバランサの後ろに置くまでは false のままにしてください。
直接叩ける状態で true にすると、送信元 IP を誰でも偽れます。
cookie.secure は既定で true#
安全側が既定ですが、ローカルは http なのでブラウザが Cookie を返しません。 セッションも CSRF も Flash も、エラーを出さずに効かなくなります。
env=local かつ secure = true のときは、起動時に WARN が出ます。
ローカル用の application.conf には cookie { secure = false } を入れて、
本番へ出すときに消してください。
起動ログで確認する#
jimble 構成: env=local / session=none / cache=db / redis=なし / db=[blog_example]
読み込まれた設定が起動の1行目に出ます。 「設定したつもりが効いていない」はここで気づけます。