Setup config folder to be a mounted volume

This commit is contained in:
Arnaud_Cayrol 2026-02-13 22:56:33 +01:00
parent 55a3ff7378
commit fbafb2c1e1
4 changed files with 59 additions and 13 deletions

View file

@ -60,7 +60,7 @@ COPY --from=frontend-build /app/frontend/dist/ ./frontend/dist/
# Create non-root user
RUN useradd --create-home --shell /bin/bash timelapse && \
mkdir -p output && \
mkdir -p output config && \
chown -R timelapse:timelapse /app
USER timelapse

View file

@ -10,6 +10,9 @@ use std::path::PathBuf;
use crate::error::{Error, Result};
/// Default path to the config file, relative to the working directory.
pub const CONFIG_PATH: &str = "config/config.toml";
/// Main configuration struct.
///
/// This is the single source of truth for all configuration.
@ -735,8 +738,30 @@ impl Config {
// Write to a temp file then atomically rename to avoid partial writes
let tmp_path = path.with_extension("toml.tmp");
std::fs::write(&tmp_path, content)?;
std::fs::rename(&tmp_path, path)?;
std::fs::write(&tmp_path, &content).map_err(|e| {
if e.kind() == std::io::ErrorKind::PermissionDenied {
Error::Config(format!(
"Permission denied writing config to '{}'. \
If running in Docker, mount a writable volume at the config directory \
(e.g. -v /host/config:/app/config).",
tmp_path.display()
))
} else {
Error::Io(e)
}
})?;
std::fs::rename(&tmp_path, path).map_err(|e| {
if e.kind() == std::io::ErrorKind::PermissionDenied {
Error::Config(format!(
"Permission denied saving config to '{}'. \
If running in Docker, mount a writable volume at the config directory \
(e.g. -v /host/config:/app/config).",
path.display()
))
} else {
Error::Io(e)
}
})?;
Ok(())
}
}

View file

@ -2,7 +2,7 @@
//!
//! Web server for creating selfie timelapses from Immich.
use immich_timelapse::{config::Config, models::DlibLandmarks, web};
use immich_timelapse::{config::{Config, CONFIG_PATH}, models::DlibLandmarks, web};
use std::net::SocketAddr;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
@ -61,13 +61,34 @@ async fn main() -> anyhow::Result<()> {
}
fn load_config() -> anyhow::Result<Config> {
// Try loading from config.toml first
let config = if std::path::Path::new("config.toml").exists() {
tracing::info!("Loading configuration from config.toml");
Config::from_file("config.toml")?.with_env()
let config_path = std::path::Path::new(CONFIG_PATH);
// Try loading from config/config.toml, or create a default one
let config = if config_path.exists() {
tracing::info!("Loading configuration from {}", CONFIG_PATH);
Config::from_file(config_path)?.with_env()
} else {
tracing::info!("No config.toml found, using environment variables");
Config::from_env()
tracing::info!("No {} found, creating default config file", CONFIG_PATH);
let default_config = Config::from_env();
// Write default config so it can be customized via volume mount
match default_config.save_to_file(config_path) {
Ok(()) => tracing::info!("Default config written to {}", CONFIG_PATH),
Err(e) => {
let msg = e.to_string();
if msg.contains("permission denied") || msg.contains("Permission denied") {
tracing::warn!(
"Could not write default config to {}: permission denied. \
If running in Docker, ensure the config directory is writable \
(e.g. mount a volume with the correct permissions: \
-v /host/config:/app/config). Continuing with in-memory defaults.",
CONFIG_PATH
);
} else {
tracing::warn!("Could not write default config to {}: {}", CONFIG_PATH, e);
}
}
}
default_config
};
// Warn but don't abort if config is incomplete - the server can still start

View file

@ -3,7 +3,7 @@
use crate::config::{
AlignmentConfig, BlurConfig, BrightnessConfig, EyeFilterConfig, FaceResolutionConfig,
HeadPoseConfig, OutputConfig, ProcessingConfig, TimeIntervalConfig, TimestampConfig,
VideoConfig,
VideoConfig, CONFIG_PATH,
};
use crate::web::state::AppState;
use axum::{extract::State, http::StatusCode, response::Json};
@ -314,10 +314,10 @@ pub async fn update_config(
// Persist to file outside the lock using spawn_blocking for the fs write
let save_result =
tokio::task::spawn_blocking(move || config_snapshot.save_to_file("config.toml")).await;
tokio::task::spawn_blocking(move || config_snapshot.save_to_file(CONFIG_PATH)).await;
match save_result {
Ok(Ok(())) => tracing::info!("Configuration updated and persisted to config.toml"),
Ok(Ok(())) => tracing::info!("Configuration updated and persisted to {}", CONFIG_PATH),
Ok(Err(e)) => tracing::warn!("Failed to persist config to file: {}", e),
Err(e) => tracing::warn!("Config save task panicked: {}", e),
}