diff --git a/Cargo.toml b/Cargo.toml
index 8f95f1a..12fe2d1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,7 +10,7 @@ anyhow = { version = "1.0.97", features = ["backtrace"] }
 axum = { version = "0.8.1", features = ["http2", "original-uri", "tracing"] }
 cached = "0.55.1"
 config = "0.15.11"
-notify = "8.0.0"
+notify = { version = "8.0.0", optional = true }
 pulldown-cmark = "0.13.0"
 regex = "1.11.1"
 serde = "1.0.219"
@@ -30,3 +30,7 @@ tracing-subscriber = { version = "0.3.19", features = [
     "json",
     "tracing-log",
 ] }
+
+[features]
+default = ["watch"]
+watch = ["dep:notify"]
diff --git a/Containerfile b/Containerfile
index 05e5e38..c3323fd 100644
--- a/Containerfile
+++ b/Containerfile
@@ -40,14 +40,14 @@ WORKDIR /app
 
 COPY --from=planner /app/recipe.json ./
 
-RUN cargo chef cook --target x86_64-unknown-linux-musl --release --recipe-path recipe.json
+RUN cargo chef cook --target x86_64-unknown-linux-musl --release --no-default--features --recipe-path recipe.json
 
 COPY ./Cargo.lock ./
 COPY ./Cargo.toml ./
 
 COPY ./src ./src
 
-RUN cargo build --target x86_64-unknown-linux-musl --release
+RUN cargo build --target x86_64-unknown-linux-musl --release --no-default--features
 
 ####################################################################################################
 ## Final image
diff --git a/src/main.rs b/src/main.rs
index 6087d6e..13c1415 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,19 +1,9 @@
 #![warn(clippy::pedantic)]
 use anyhow::Result;
-use notify::Watcher;
-use std::{
-    path::Path,
-    sync::Arc,
-    time::{Duration, Instant},
-};
-use time::OffsetDateTime;
-use tokio::{
-    net::TcpListener,
-    signal,
-    sync::{RwLock, mpsc::Receiver},
-};
+use std::sync::Arc;
+use tokio::{net::TcpListener, signal, sync::RwLock};
 use tower_http::{compression::CompressionLayer, cors::CorsLayer, trace::TraceLayer};
-use tracing::{debug, error, instrument, level_filters::LevelFilter, log::info, warn};
+use tracing::{instrument, level_filters::LevelFilter, log::info, warn};
 use tracing_subscriber::EnvFilter;
 
 mod error;
@@ -25,6 +15,8 @@ mod rendering;
 mod settings;
 mod state;
 mod tag;
+#[cfg(feature = "watch")]
+mod watch;
 
 use settings::Settings;
 use state::AppState;
@@ -72,12 +64,15 @@ fn setup_tracing(cfg: &Settings) {
 
 #[instrument(skip(cfg))]
 async fn init_app(cfg: Settings) -> Result<axum::routing::Router> {
+    #[cfg(feature = "watch")]
     let watch = cfg.watch;
+
     let state = AppState::load(cfg)?;
     let state = Arc::new(RwLock::new(state));
 
+    #[cfg(feature = "watch")]
     if watch {
-        tokio::spawn(start_file_watcher(state.clone()));
+        tokio::spawn(watch::start_file_watcher(state.clone()));
     }
 
     Ok(handlers::routes()
@@ -87,108 +82,6 @@ async fn init_app(cfg: Settings) -> Result<axum::routing::Router> {
         .with_state(state))
 }
 
-async fn start_file_watcher(state: Arc<RwLock<AppState>>) {
-    fn event_filter(event: &notify::Event) -> bool {
-        event.kind.is_modify() || event.kind.is_remove()
-    }
-
-    let (page_tx, page_rx) = tokio::sync::mpsc::channel::<notify::Event>(8);
-
-    let mut page_watcher =
-        notify::recommended_watcher(move |event: Result<notify::Event, notify::Error>| {
-            let Ok(event) = event.inspect_err(|e| error!("File watcher error: {}", e)) else {
-                return;
-            };
-            if !event_filter(&event) {
-                return;
-            }
-            _ = page_tx
-                .blocking_send(event)
-                .inspect_err(|e| error!("Failed to add watch event to channel: {}", e));
-        })
-        .expect("create page file watcher");
-
-    page_watcher
-        .watch(Path::new("pages/"), notify::RecursiveMode::Recursive)
-        .expect("add pages dir to watcher");
-
-    let (template_tx, template_rx) = tokio::sync::mpsc::channel::<notify::Event>(8);
-
-    let mut template_watcher =
-        notify::recommended_watcher(move |event: Result<notify::Event, notify::Error>| {
-            let Ok(event) = event.inspect_err(|e| error!("File watcher error: {}", e)) else {
-                return;
-            };
-            if !event_filter(&event) {
-                return;
-            }
-            _ = template_tx
-                .blocking_send(event)
-                .inspect_err(|e| error!("Failed to add watch event to channel: {}", e));
-        })
-        .expect("create template file watcher");
-
-    template_watcher
-        .watch(Path::new("templates/"), notify::RecursiveMode::Recursive)
-        .expect("add templates dir to watcher");
-
-    tokio::join!(
-        page_watch_loop(state.clone(), page_rx),
-        template_watch_loop(state.clone(), template_rx)
-    );
-}
-
-const WATCHER_DEBOUNCE_MILLIS: u64 = 100;
-
-async fn page_watch_loop(state: Arc<RwLock<AppState>>, mut rx: Receiver<notify::Event>) {
-    let mut last_reload = Instant::now();
-    debug!("Now watching pages");
-    while let Some(_event) = rx.recv().await {
-        if last_reload.elapsed() < Duration::from_millis(WATCHER_DEBOUNCE_MILLIS) {
-            continue;
-        }
-
-        let pages = {
-            let state = state.read().await;
-
-            info!("Reloading pages");
-            let root_path = Path::new("pages/");
-            page::load_all(&state, root_path, root_path)
-                .inspect_err(|err| error!("Error reloading pages: {}", err))
-                .ok()
-        };
-
-        if let Some(pages) = pages {
-            let mut state = state.write().await;
-            state.pages = pages;
-            state.last_modified = OffsetDateTime::now_utc();
-            last_reload = Instant::now();
-        }
-    }
-    warn!("Page watch loop stopped");
-}
-
-async fn template_watch_loop(state: Arc<RwLock<AppState>>, mut rx: Receiver<notify::Event>) {
-    let mut last_reload = Instant::now();
-    debug!("Now watching templates");
-    while let Some(_event) = rx.recv().await {
-        if last_reload.elapsed() < Duration::from_millis(WATCHER_DEBOUNCE_MILLIS) {
-            continue;
-        }
-
-        let mut state = state.write().await;
-
-        info!("Reloading templates");
-        _ = state
-            .tera
-            .full_reload()
-            .inspect_err(|err| error!("Error reloading templates: {}", err));
-        state.last_modified = OffsetDateTime::now_utc();
-        last_reload = Instant::now();
-    }
-    warn!("Template watch loop stopped");
-}
-
 async fn shutdown_signal() {
     let ctrl_c = async {
         signal::ctrl_c()
diff --git a/src/settings.rs b/src/settings.rs
index 11b675f..9f30552 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -10,6 +10,7 @@ pub struct Settings {
     pub logging: String,
     pub log_format: String,
     pub drafts: bool,
+    #[cfg(feature = "watch")]
     pub watch: bool,
 }
 
diff --git a/src/watch.rs b/src/watch.rs
new file mode 100644
index 0000000..34b78d4
--- /dev/null
+++ b/src/watch.rs
@@ -0,0 +1,114 @@
+use std::{
+    path::Path,
+    sync::Arc,
+    time::{Duration, Instant},
+};
+
+use notify::Watcher;
+use time::OffsetDateTime;
+use tokio::sync::{RwLock, mpsc::Receiver};
+use tracing::{debug, error, info, warn};
+
+use crate::{page, state::AppState};
+
+pub async fn start_file_watcher(state: Arc<RwLock<AppState>>) {
+    fn event_filter(event: &notify::Event) -> bool {
+        event.kind.is_modify() || event.kind.is_remove()
+    }
+
+    let (page_tx, page_rx) = tokio::sync::mpsc::channel::<notify::Event>(8);
+
+    let mut page_watcher =
+        notify::recommended_watcher(move |event: Result<notify::Event, notify::Error>| {
+            let Ok(event) = event.inspect_err(|e| error!("File watcher error: {}", e)) else {
+                return;
+            };
+            if !event_filter(&event) {
+                return;
+            }
+            _ = page_tx
+                .blocking_send(event)
+                .inspect_err(|e| error!("Failed to add watch event to channel: {}", e));
+        })
+        .expect("create page file watcher");
+
+    page_watcher
+        .watch(Path::new("pages/"), notify::RecursiveMode::Recursive)
+        .expect("add pages dir to watcher");
+
+    let (template_tx, template_rx) = tokio::sync::mpsc::channel::<notify::Event>(8);
+
+    let mut template_watcher =
+        notify::recommended_watcher(move |event: Result<notify::Event, notify::Error>| {
+            let Ok(event) = event.inspect_err(|e| error!("File watcher error: {}", e)) else {
+                return;
+            };
+            if !event_filter(&event) {
+                return;
+            }
+            _ = template_tx
+                .blocking_send(event)
+                .inspect_err(|e| error!("Failed to add watch event to channel: {}", e));
+        })
+        .expect("create template file watcher");
+
+    template_watcher
+        .watch(Path::new("templates/"), notify::RecursiveMode::Recursive)
+        .expect("add templates dir to watcher");
+
+    tokio::join!(
+        page_watch_loop(state.clone(), page_rx),
+        template_watch_loop(state.clone(), template_rx)
+    );
+}
+
+const WATCHER_DEBOUNCE_MILLIS: u64 = 100;
+
+async fn page_watch_loop(state: Arc<RwLock<AppState>>, mut rx: Receiver<notify::Event>) {
+    let mut last_reload = Instant::now();
+    debug!("Now watching pages");
+    while let Some(_event) = rx.recv().await {
+        if last_reload.elapsed() < Duration::from_millis(WATCHER_DEBOUNCE_MILLIS) {
+            continue;
+        }
+
+        let pages = {
+            let state = state.read().await;
+
+            info!("Reloading pages");
+            let root_path = Path::new("pages/");
+            page::load_all(&state, root_path, root_path)
+                .inspect_err(|err| error!("Error reloading pages: {}", err))
+                .ok()
+        };
+
+        if let Some(pages) = pages {
+            let mut state = state.write().await;
+            state.pages = pages;
+            state.last_modified = OffsetDateTime::now_utc();
+            last_reload = Instant::now();
+        }
+    }
+    warn!("Page watch loop stopped");
+}
+
+async fn template_watch_loop(state: Arc<RwLock<AppState>>, mut rx: Receiver<notify::Event>) {
+    let mut last_reload = Instant::now();
+    debug!("Now watching templates");
+    while let Some(_event) = rx.recv().await {
+        if last_reload.elapsed() < Duration::from_millis(WATCHER_DEBOUNCE_MILLIS) {
+            continue;
+        }
+
+        let mut state = state.write().await;
+
+        info!("Reloading templates");
+        _ = state
+            .tera
+            .full_reload()
+            .inspect_err(|err| error!("Error reloading templates: {}", err));
+        state.last_modified = OffsetDateTime::now_utc();
+        last_reload = Instant::now();
+    }
+    warn!("Template watch loop stopped");
+}