From 52cc29060bf1d335d8d5d9627594a9effd5a461c Mon Sep 17 00:00:00 2001 From: Louis Chmn Date: Fri, 11 Sep 2026 15:39:32 +0200 Subject: [PATCH 01/37] feat(start): Print the database location on start Signed-off-by: Louis Chmn --- src/database.rs | 34 +++++++++++++++++++++++++++++++++- src/main.rs | 24 ++++++++++++++---------- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/database.rs b/src/database.rs index 0bc31d7..7f943bd 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,3 +1,4 @@ +use crate::config::HazeConfig; use crate::exec::{exec, exec_tty, ExitCode}; use crate::image::pull_image; use bollard::config::ContainerCreateBody; @@ -7,7 +8,7 @@ use bollard::Docker; use maplit::hashmap; use miette::{IntoDiagnostic, Report, Result, WrapErr}; use std::io::{stdout, Stdout}; -use std::net::IpAddr; +use std::net::{IpAddr, Ipv4Addr}; use std::str::FromStr; use std::time::Duration; use strum::{Display, EnumIter, EnumProperty, IntoStaticStr}; @@ -414,6 +415,37 @@ impl Database { } } + pub async fn location( + &self, + docker: &Docker, + cloud_id: &str, + config: &HazeConfig, + ) -> Result { + let ip = match self.family() { + DatabaseFamily::Mysql + | DatabaseFamily::MariaDB + | DatabaseFamily::Postgres + | DatabaseFamily::Oracle => self + .ip(docker, cloud_id) + .await + .ok_or_else(|| Report::msg("Failed to get the IP of the database container"))?, + DatabaseFamily::Sqlite => IpAddr::V4(Ipv4Addr::LOCALHOST), + }; + + match self.family() { + DatabaseFamily::Mysql | DatabaseFamily::MariaDB => { + Ok(format!("mysql://haze:haze@{ip}/haze")) + } + DatabaseFamily::Postgres => Ok(format!("postgresql://haze:haze@{ip}/haze")), + DatabaseFamily::Oracle => Ok(format!("oracle://system:haze@{ip}:1521/XE")), + DatabaseFamily::Sqlite => Ok(config + .work_dir + .join(cloud_id) + .join("data/haze.db") + .to_string()), + } + } + pub async fn is_healthy(&self, docker: &Docker, cloud_id: &str, postfix: &str) -> Result { match self.family() { DatabaseFamily::Sqlite => Ok(true), diff --git a/src/main.rs b/src/main.rs index bcfd7e5..049b56b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -422,21 +422,15 @@ async fn main() -> Result { let ip = cloud .ip .ok_or_else(|| Report::msg(format!("{} is not running", cloud.id)))?; - let db_type = match cloud.db().family() { + match cloud.db().family() { DatabaseFamily::Sqlite => { return Err(Report::msg("sqlite is not supported with `haze env`")); } DatabaseFamily::Oracle => { return Err(Report::msg("oracle is not supported with `haze env`")); } - DatabaseFamily::Mysql | DatabaseFamily::MariaDB => "mysql", - DatabaseFamily::Postgres => "postgresql", - }; - let db_ip = cloud - .db() - .ip(&docker, &cloud.id) - .await - .ok_or_else(|| Report::msg(format!("{}-db is not running", cloud.id)))?; + _ => {} + } let mut command = Command::new(command); command @@ -445,7 +439,7 @@ async fn main() -> Result { .env("NEXTCLOUD_URL", &cloud.address) .env( "DATABASE_URL", - format!("{}://haze:haze@{}/haze", db_type, db_ip), + cloud.db().location(&docker, &cloud.id, &config).await?, ); if cloud.services().contains(&Service::RedisTls(RedisTls)) { @@ -552,6 +546,16 @@ async fn setup( let cloud = Cloud::create(docker, options, config).await?; println!("{}", cloud.address); let host = cloud.address.split_once("://").expect("no address?").1; + + match cloud.db().location(docker, &cloud.id, config).await { + Ok(value) => { + println!("Database: {}", value); + } + Err(e) => { + println!("Failed to print DB location: {}", e); + } + } + if always_setup || config.auto_setup.enabled { println!("Waiting for servers to start"); cloud.wait_for_start(docker).await?; From 048bbe8de585ad6399562a09653000c85156e126 Mon Sep 17 00:00:00 2001 From: Louis Chmn Date: Wed, 16 Sep 2026 22:31:16 +0200 Subject: [PATCH 02/37] feat(proxy): Support direct TLS connection This allows runing haze proxy without a reverse proxy while still having the possiblity to use HTTPS. Signed-off-by: Louis Chmn --- Cargo.lock | 2 ++ Cargo.toml | 1 + README.md | 29 +++++++++++++++++++----- src/config.rs | 4 ++++ src/proxy.rs | 62 ++++++++++++++++++++++++++++++++++++++++++++++----- 5 files changed, 87 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9a62e09..32ae3ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -933,6 +933,7 @@ dependencies = [ "tar", "termion", "tokio", + "tokio-rustls", "tokio-stream", "toml", "tracing", @@ -2016,6 +2017,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "aws-lc-rs", + "log", "once_cell", "rustls-pki-types", "rustls-webpki", diff --git a/Cargo.toml b/Cargo.toml index 324dd21..298e0c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ tokio = { version = "1.53.1", features = [ "rt-multi-thread", "signal" ] } +tokio-rustls = "0.26" tokio-stream = { version = "0.1.19", features = ["net"] } toml = "1.1.4" tracing = "0.1.44" diff --git a/README.md b/README.md index d5a3f9b..6feae7b 100644 --- a/README.md +++ b/README.md @@ -299,14 +299,16 @@ By default, instances can be accessed by their IP. In order to get more memorable URLs and allow supporting https, haze comes with a builtin reverse proxy to allow using a wildcard domain. -### Requirements +### DNS Setup + +#### Requirements - A domain name you can set wildcard DNS records for - A reverse proxy like Nginx or Apache - (optionally) a wildcard ssl certificate (can be acquiring using letsencrypt and dns verification) -### DNS Setup +#### Steps - Set a DNS record for `*.haze.example.com` and `haze.example.com` pointing to your development machine. @@ -334,8 +336,23 @@ mkcert -cert-file haze.test.crt -key-file , + #[serde(default)] + pub key: Option, } impl ProxyConfig { diff --git a/src/proxy.rs b/src/proxy.rs index ffc6e4c..2223299 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -32,6 +32,10 @@ use tokio::net::UnixListener; use tokio::signal::ctrl_c; use tokio::spawn; use tokio::time::sleep; +use tokio_rustls::TlsAcceptor; +use tokio_rustls::rustls::ServerConfig; +use tokio_rustls::rustls::pki_types::pem::PemObject; +use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer}; use tokio_stream::wrappers::{TcpListenerStream, UnixListenerStream}; use tracing::{debug, error, info}; @@ -147,9 +151,34 @@ pub async fn proxy(docker: Docker, config: HazeConfig) -> Result<()> { } let listen = config.proxy.listen.clone(); + let acceptor = match (&config.proxy.cert, &config.proxy.key) { + (None, None) => None, + (Some(_), None) => return Err(miette!("`cert` is set without `key`")), + (None, Some(_)) => return Err(miette!("`key` is set without `cert`")), + (Some(cert), Some(key)) => Some(tls_acceptor(cert, key)?), + }; + let base_address = config.proxy.address.clone(); let instances = ActiveInstances::new(docker, config); - serve(instances, listen, base_address).await + serve(instances, listen, base_address, acceptor).await +} + +/// Build a TLS acceptor from a PEM encoded certificate chain and private key on disk +fn tls_acceptor(cert: &str, key: &str) -> Result { + let certs = CertificateDer::pem_file_iter(cert) + .map_err(|e| miette!("failed to load certificate from {cert}: {e}"))? + .collect::, _>>() + .map_err(|e| miette!("failed to load certificate from {cert}: {e}"))?; + let key = PrivateKeyDer::from_pem_file(key) + .map_err(|e| miette!("failed to load private key from {key}: {e}"))?; + + let mut server_config = ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(certs, key) + .into_diagnostic()?; + server_config.alpn_protocols = vec![b"http/1.1".to_vec()]; + + Ok(TlsAcceptor::from(Arc::new(server_config))) } #[derive(Clone)] @@ -159,7 +188,12 @@ struct AppState { proxy_client: Arc, } -async fn serve(instances: ActiveInstances, listen: String, base_address: String) -> Result<()> { +async fn serve( + instances: ActiveInstances, + listen: String, + base_address: String, + acceptor: Option, +) -> Result<()> { let instances = Arc::new(instances); let base_address = Arc::new(base_address); let last_instances = instances.clone(); @@ -188,12 +222,13 @@ async fn serve(instances: ActiveInstances, listen: String, base_address: String) if !listen.starts_with('/') { let addr: SocketAddr = listen.parse().into_diagnostic()?; let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); - println!("listening on {}", listener.local_addr().unwrap()); + let scheme = if acceptor.is_some() { "https" } else { "http" }; + println!("Listening on {scheme}://{}", listener.local_addr().unwrap()); let mut connections = pin!(TcpListenerStream::new(listener).take_until(cancel)); while let Some(stream) = connections.next().await { match stream { - Ok(stream) => handle_connection(state.clone(), stream), + Ok(stream) => handle_connection(state.clone(), stream, acceptor.clone()).await, Err(error) => { error!(%error, "connection failed"); } @@ -216,7 +251,7 @@ async fn serve(instances: ActiveInstances, listen: String, base_address: String) while let Some(stream) = connections.next().await { match stream { - Ok(stream) => handle_connection(state.clone(), stream), + Ok(stream) => handle_connection(state.clone(), stream, None).await, Err(error) => { error!(%error, "connection failed"); } @@ -227,7 +262,22 @@ async fn serve(instances: ActiveInstances, listen: String, base_address: String) Ok(()) } -fn handle_connection( +async fn handle_connection( + state: AppState, + stream: I, + acceptor: Option, +) { + // Spawn a tokio task to serve multiple connections concurrently + match acceptor { + Some(acceptor) => match acceptor.accept(stream).await { + Ok(stream) => serve_connection(state, stream).await, + Err(error) => error!(%error, "tls handshake failed"), + }, + None => serve_connection(state, stream).await, + } +} + +async fn serve_connection( state: AppState, stream: I, ) { From 8c411d3b44a3657a4c8cd62c568cefe54fbc64df Mon Sep 17 00:00:00 2001 From: Louis Chmn Date: Thu, 17 Sep 2026 14:56:44 +0200 Subject: [PATCH 03/37] fix(docs): Change to a safe suggested default Signed-off-by: Louis Chmn --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6feae7b..8f9cc4e 100644 --- a/README.md +++ b/README.md @@ -342,7 +342,7 @@ mkcert -cert-file haze.test.crt -key-file Date: Thu, 17 Sep 2026 17:42:05 +0200 Subject: [PATCH 04/37] fix(php): Version name for v8.5 Signed-off-by: Louis Chmn --- src/php.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/php.rs b/src/php.rs index bb4764c..f0878a5 100644 --- a/src/php.rs +++ b/src/php.rs @@ -71,7 +71,7 @@ impl PhpVersion { PhpVersion::Php82 => "8.2", PhpVersion::Php83 => "8.3", PhpVersion::Php84 => "8.4", - PhpVersion::Php85 => "8.4", + PhpVersion::Php85 => "8.5", } } From ef83b9dd52a08bfa5e282c9af930116b50ea0e4a Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Sat, 19 Sep 2026 12:12:38 +0200 Subject: [PATCH 05/37] remove blackfire hash workaround --- flake.nix | 8 -------- 1 file changed, 8 deletions(-) diff --git a/flake.nix b/flake.nix index a3b6ebd..99f9772 100644 --- a/flake.nix +++ b/flake.nix @@ -45,14 +45,6 @@ (final: prev: { inherit (phps.packages.${prev.system}) php81 php80; inherit (nix2container.packages.x86_64-linux) nix2container; - blackfire = prev.blackfire.overrideAttrs ( - oldAttrs: finalAttrs: { - src = final.fetchurl { - url = "https://packages.blackfire.io/debian/pool/any/main/b/blackfire/blackfire_${finalAttrs.version}_amd64.deb"; - sha256 = "sha256-1fswfZEElLyXWqvNW76BpKTpBomK9cglJtitZgcpxhM="; - }; - } - ); }) ]; From 9def5ef5207446db0b7f2be010badb0c102da493 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Sat, 19 Sep 2026 12:12:38 +0200 Subject: [PATCH 06/37] flake update --- flake.lock | 86 +++++++++++++++++------------------------------------- 1 file changed, 26 insertions(+), 60 deletions(-) diff --git a/flake.lock b/flake.lock index 3b2be4f..1603da3 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1780099841, - "narHash": "sha256-EVZd2RsbpreRUDSi9rBwPY+ZxoyMaiEBbZxxhljbaS4=", + "lastModified": 1788465171, + "narHash": "sha256-Y1/TTVXjYXGF068IThQH9fPSZ0SIE74PABlUxnWTUH0=", "owner": "ipetkov", "repo": "crane", - "rev": "0532eb17955225173906d671fb36306bdeb1e2dc", + "rev": "eb35abda9f232cc6610b1d1e3200d15c49b7ac54", "type": "github" }, "original": { @@ -38,11 +38,11 @@ ] }, "locked": { - "lastModified": 1781543995, - "narHash": "sha256-wsSyxNWOSMofu4F5xGYGMrB1d8cepvBNhxhm9bGGmXg=", + "lastModified": 1789408620, + "narHash": "sha256-ZymN+wNsIs0rMw9ydP5Yp5GIUKZoA8b8IDsCnz30VfE=", "owner": "nix-community", "repo": "flakelight", - "rev": "fd1d557721e07b5a9d319442e4bcb5d286600011", + "rev": "1d9163b9abf315129e912f3157e03b265f104838", "type": "github" }, "original": { @@ -60,11 +60,11 @@ "rust-overlay": "rust-overlay" }, "locked": { - "lastModified": 1780231986, - "narHash": "sha256-OyafczPtzE0Xa2zl3j/KvV2+ZVYGhYQHt0MOVWtDXlY=", + "lastModified": 1789480598, + "narHash": "sha256-G1jy1kz2dLJCkVjjY2PnCXEL1B2iqMlSfz01YFCBY+c=", "ref": "refs/heads/main", - "rev": "f5cbda29b945df03256bf63c22fa4cd5fa429e67", - "revCount": 72, + "rev": "c1e026caf1750f226a20dab66594dc1ef3a34ec4", + "revCount": 77, "type": "git", "url": "https://codeberg.org/icewind/mill-scale.git" }, @@ -80,11 +80,11 @@ ] }, "locked": { - "lastModified": 1775487831, - "narHash": "sha256-2lguQpLPQaxpQCJjXhmEEAfabwsAhkP29Z7fgLzHARA=", + "lastModified": 1788758950, + "narHash": "sha256-b3zONUcYXZHeoeYwDZdSDCzMj0s6ubmD6NT3D7sS+jU=", "owner": "nlewo", "repo": "nix2container", - "rev": "76be9608a7f4d6c985d28b0e7be903ae2547df3e", + "rev": "b6ac40ef110c12ab1651fce5ea563f7837236439", "type": "github" }, "original": { @@ -95,11 +95,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1781216227, - "narHash": "sha256-9mUW6gNwoN2SWc/l0fW4svPNOulXLl8ijqKyeSOGgJE=", + "lastModified": 1789749394, + "narHash": "sha256-cFTsMQz8Hzn8MT49oaeLSHg62tE86NykugCWkeM2ypk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a0374025a863d007d98e3297f6aa46cc3141c2f0", + "rev": "cf9d2fb3e50fa1cd5114c47505ea9177f7ff5f49", "type": "github" }, "original": { @@ -110,11 +110,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1781268102, - "narHash": "sha256-Zn5KTggEmUB3lXn/ccERNcBdddE6IaOFber9dWViWDg=", + "lastModified": 1788953386, + "narHash": "sha256-dIqD4NX3Uldk29CBqKt9dRdSxh/+Ba8lVPu2xEsLTJo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "49a4bd0573c376468dd7996ddb6f9fa31d8c4d97", + "rev": "19a27817106f9449970edb3a398ab5349666466f", "type": "github" }, "original": { @@ -127,15 +127,14 @@ "phps": { "inputs": { "flake-compat": "flake-compat", - "nixpkgs": "nixpkgs_2", - "utils": "utils" + "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1781456983, - "narHash": "sha256-z3SNuQpkSeIRTS6Y/Q5FgGAuGSY5+YJBXtqWPXw0f0k=", + "lastModified": 1789262846, + "narHash": "sha256-IrgEYbj5LY4EskP8lvYRexPp0jI2KBy3NJZBBgSqrpg=", "owner": "fossar", "repo": "nix-phps", - "rev": "6458735dece7e48f27d52ac68eee2d2cfe55b79a", + "rev": "af15d3f84460bfa8cd15278795347e4661e0a199", "type": "github" }, "original": { @@ -162,11 +161,11 @@ ] }, "locked": { - "lastModified": 1780197589, - "narHash": "sha256-FVCr2Ij/jKf59a4LW481eeOF6rJRreOBrVgW/aUBTrw=", + "lastModified": 1789457514, + "narHash": "sha256-Aggle++fTyAifBy+QBPxjM+obO5iepKW/8MDxQtgGvI=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "21632e942d89bf1cce4e5a63d7e58a215a0cbfcc", + "rev": "89e99bf0778a8f2cd18c9360c3f19c1ee47fc739", "type": "github" }, "original": { @@ -174,39 +173,6 @@ "repo": "rust-overlay", "type": "github" } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } } }, "root": "root", From ed824d05776bb8f627f5c87db6d0aa78e18d1556 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Sat, 19 Sep 2026 12:50:46 +0200 Subject: [PATCH 07/37] rebuilder docker images when flake changes --- .forgejo/workflows/docker.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.forgejo/workflows/docker.yaml b/.forgejo/workflows/docker.yaml index 244919b..8791fbc 100644 --- a/.forgejo/workflows/docker.yaml +++ b/.forgejo/workflows/docker.yaml @@ -4,6 +4,7 @@ on: push: branches: ["main"] paths: + - "flake.*" - "Cargo.toml" - ".forgejo/workflows/docker.yaml" - "nix/image/**" From cf2184bc6b12501a6847e1ebb83ac1a1906dad01 Mon Sep 17 00:00:00 2001 From: Louis Chmn Date: Sat, 19 Sep 2026 15:23:49 +0200 Subject: [PATCH 08/37] fix(franken): Overide SERVER_NAME env var To listen to any domain names on port 80 Signed-off-by: Louis Chmn --- nix/image/configs/supervisor/frankenphp.conf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nix/image/configs/supervisor/frankenphp.conf b/nix/image/configs/supervisor/frankenphp.conf index 0ee52b1..54bf787 100644 --- a/nix/image/configs/supervisor/frankenphp.conf +++ b/nix/image/configs/supervisor/frankenphp.conf @@ -1,3 +1,5 @@ [program:frankenphp] +environment = SERVER_NAME=":80" command = /bin/frankenphp run directory = /var/www/html +user=haze \ No newline at end of file From 4ed4f1638a633c2f8415e1ad6141b7045763f681 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Sat, 19 Sep 2026 18:10:18 +0200 Subject: [PATCH 09/37] clippy fixes --- src/service/authentik/saml.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service/authentik/saml.rs b/src/service/authentik/saml.rs index b64dc7b..7ade8db 100644 --- a/src/service/authentik/saml.rs +++ b/src/service/authentik/saml.rs @@ -76,7 +76,7 @@ impl ServiceTrait for AuthentikSaml { "--silent".into(), "1".into(), "--general-idp0_display_name=Authentik SAML".into(), - format!("--general-uid_mapping=http://haze.test/nextcloud/uid"), + "--general-uid_mapping=http://haze.test/nextcloud/uid".to_string(), format!("--idp-entityId={authentik_url}/application/saml/nextcloud-saml/metadata/"), format!("--idp-singleSignOnService.url={authentik_url}/application/saml/nextcloud-saml/"), format!("--idp-singleLogoutService.url={authentik_url}/application/saml/nextcloud-saml/"), From a130ebcad16550bde59e73d5cfb5d8dd6211ecd2 Mon Sep 17 00:00:00 2001 From: Louis Chmn Date: Sat, 19 Sep 2026 18:39:11 +0200 Subject: [PATCH 10/37] feat(franken): Expose Caddy admin endpoints through haze proxy Available through: `ember --addr http://-franken-php.haze.test` Signed-off-by: Louis Chmn --- README.md | 11 +++++++++++ nix/image/configs/supervisor/frankenphp.conf | 2 +- src/service.rs | 8 ++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6feae7b..a1768f0 100644 --- a/README.md +++ b/README.md @@ -496,3 +496,14 @@ name = "groupfolders" # name of the preset apps = ["groupfolders"] # app to enable commands = ["occ groupfolders:create gf", "occ groupfolders:group 1 admin read write share delete"] # commands to run post-setup ``` + +## FrankenPHP (experimental) + +You can have Nextcloud run on FrankenPHP. + +Caddy's admin metrics are then accessible through +`http://-franken-php.haze.test`. With ember you can run: + +```bash +ember --addr http://-franken-php.haze.test +``` diff --git a/nix/image/configs/supervisor/frankenphp.conf b/nix/image/configs/supervisor/frankenphp.conf index 54bf787..cc88f7d 100644 --- a/nix/image/configs/supervisor/frankenphp.conf +++ b/nix/image/configs/supervisor/frankenphp.conf @@ -1,5 +1,5 @@ [program:frankenphp] -environment = SERVER_NAME=":80" +environment = SERVER_NAME=":80",CADDY_ADMIN=":2019" command = /bin/frankenphp run directory = /var/www/html user=haze \ No newline at end of file diff --git a/src/service.rs b/src/service.rs index 233e88f..42646fb 100644 --- a/src/service.rs +++ b/src/service.rs @@ -236,6 +236,14 @@ impl ServiceTrait for FrankenPhp { "franken-php" } + fn container_name(&self, cloud_id: &str) -> Option { + Some(cloud_id.to_string()) + } + + fn proxy_port(&self) -> u16 { + 2019 + } + fn env(&self) -> &[&str] { &["FRANKENPHP=1"] } From 28baa842625eddc6644c83162966ee876a54277d Mon Sep 17 00:00:00 2001 From: Louis Chmn Date: Sun, 20 Sep 2026 11:16:10 +0200 Subject: [PATCH 11/37] feat(clean): Add more context to errors Signed-off-by: Louis Chmn --- src/main.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index bcfd7e5..6f4dcda 100644 --- a/src/main.rs +++ b/src/main.rs @@ -93,8 +93,15 @@ async fn main() -> Result { clear_networks(&docker, &retain).await?; - for cache_dir in config.work_dir.read_dir().into_diagnostic()? { - let cache_dir = cache_dir.into_diagnostic()?; + for cache_dir in config + .work_dir + .read_dir() + .into_diagnostic() + .wrap_err_with(|| format!("Failed to read dir {}", config.work_dir))? + { + let cache_dir = cache_dir + .into_diagnostic() + .wrap_err_with(|| "Failed to unwrap cache_dir")?; if let Some(id) = cache_dir.file_name().to_str() && id.starts_with("haze-") && !retain.iter().any(|cloud| cloud.id == id) @@ -117,7 +124,7 @@ async fn main() -> Result { } } - prune_worktrees(&config)?; + prune_worktrees(&config).wrap_err_with(|| "Failed to prune worktree")?; } HazeArgs::List { filter } => { let list = Cloud::list(&docker, filter, &config).await?; From c947d645a92d00265b7193a88e23cf634942d795 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Sun, 20 Sep 2026 11:24:20 +0200 Subject: [PATCH 12/37] use `supervisorctl restart` for `haze reload` --- src/main.rs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/main.rs b/src/main.rs index 049b56b..f5ad412 100644 --- a/src/main.rs +++ b/src/main.rs @@ -513,16 +513,7 @@ async fn main() -> Result { &docker, &cloud.id, "root", - vec!["pkill", "php-fpm"], - Vec::::new(), - Some(stdout()), - ) - .await?; - exec( - &docker, - &cloud.id, - "root", - vec!["sh", "-c", "php-fpm --fpm-config /etc/php-fpm.conf&"], + vec!["supervisorctl", "restart", "php-fpm"], Vec::::new(), Some(stdout()), ) From 459e467cf216a9413869ee5b430fb6df04f6e31f Mon Sep 17 00:00:00 2001 From: Louis Chmn Date: Sun, 20 Sep 2026 11:12:24 +0200 Subject: [PATCH 13/37] feat(README): Document Xdebug Signed-off-by: Louis Chmn --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 3090995..d66688b 100644 --- a/README.md +++ b/README.md @@ -497,6 +497,31 @@ apps = ["groupfolders"] # app to enable commands = ["occ groupfolders:create gf", "occ groupfolders:group 1 admin read write share delete"] # commands to run post-setup ``` +## Xdebug + +Haze Xdebug is running in a docker container, you need to tell you IDE how to +properly map the path. The IDE debugger config usually looks like: + +```json + { + "label": "PHP: Debug server within docker", + "adapter": "Xdebug", + "request": "launch", + "port": 9003, + "pathMappings": { + "/var/www/html": "", + "/var/www/html/apps-extra": "", + }, + }, +``` + +This would have to be adapted for detached instances. + +To enable Xdebug for all requests: + +- Uncomment the lines in `haze edit /config/php.ini` +- Then run `haze reload ` + ## FrankenPHP (experimental) You can have Nextcloud run on FrankenPHP. From fd3c5f47087cf33fd575ab84124d0a956cf4538b Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Sun, 20 Sep 2026 12:52:24 +0200 Subject: [PATCH 14/37] setup cron for background jobs --- nix/image/bootstrap | 3 ++- nix/image/configs.nix | 1 - nix/image/configs/cron.d/nc | 2 ++ nix/image/configs/oc-cron.conf | 2 -- nix/image/configs/supervisor/enabled/cron.conf | 2 ++ nix/image/scripts/nc-auto-config | 2 +- src/main.rs | 14 ++++++++++++++ 7 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 nix/image/configs/cron.d/nc delete mode 100644 nix/image/configs/oc-cron.conf create mode 100644 nix/image/configs/supervisor/enabled/cron.conf diff --git a/nix/image/bootstrap b/nix/image/bootstrap index 4e7e0f3..80a741b 100755 --- a/nix/image/bootstrap +++ b/nix/image/bootstrap @@ -2,7 +2,6 @@ touch /var/log/nginx/access.log touch /var/log/nginx/error.log -touch /var/log/cron/owncloud.log mkdir /config if not ("/config/php.ini" | path exists) { @@ -35,6 +34,7 @@ let dirs = [ let files = [ ["condition", "path"]; ["/", "/var/www/html/build/integration/composer.lock"], + ["/", "/var/log/cron/haze.log"] ] $dirs | each { |dir| @@ -88,6 +88,7 @@ if ("REDIS_TLS" in $env) { cp /etc/supervisor/redis-tls.conf /etc/supervisor/enabled/ } else { cp /etc/supervisor/redis-plain.conf /etc/supervisor/enabled/ + cp /etc/supervisor/redis-plain.conf /etc/supervisor/enabled/ } if ("BLACKFIRE_SERVER_ID" in $env) { diff --git a/nix/image/configs.nix b/nix/image/configs.nix index afa06c7..e563aa0 100644 --- a/nix/image/configs.nix +++ b/nix/image/configs.nix @@ -3,5 +3,4 @@ runCommand "configs" {} '' mkdir -p $out cp -r ${./configs} $out/etc chmod -R +w $out/etc - mkdir $out/etc/supervisor/enabled/ '' diff --git a/nix/image/configs/cron.d/nc b/nix/image/configs/cron.d/nc new file mode 100644 index 0000000..b9fb836 --- /dev/null +++ b/nix/image/configs/cron.d/nc @@ -0,0 +1,2 @@ +# m h dom mon dow user command +* * * * * haze php -f /var/www/html/cron.php -- -v >> /var/log/cron/haze.log 2>&1 diff --git a/nix/image/configs/oc-cron.conf b/nix/image/configs/oc-cron.conf deleted file mode 100644 index 81c6020..0000000 --- a/nix/image/configs/oc-cron.conf +++ /dev/null @@ -1,2 +0,0 @@ -# m h dom mon dow command -*/5 * * * * sudo -u haze php -f /var/www/html/cron.php >> /var/log/cron/haze.log 2>&1 diff --git a/nix/image/configs/supervisor/enabled/cron.conf b/nix/image/configs/supervisor/enabled/cron.conf new file mode 100644 index 0000000..9646598 --- /dev/null +++ b/nix/image/configs/supervisor/enabled/cron.conf @@ -0,0 +1,2 @@ +[program:cron] +command = crond -x sch -f diff --git a/nix/image/scripts/nc-auto-config b/nix/image/scripts/nc-auto-config index 674baaa..0cf9554 100755 --- a/nix/image/scripts/nc-auto-config +++ b/nix/image/scripts/nc-auto-config @@ -2,7 +2,7 @@ touch /var/log/nginx/access.log touch /var/log/nginx/error.log -touch /var/log/cron/owncloud.log +touch /var/log/cron/haze.log if ("/var/www/html/config/config.php" | path exists) { exit 0 diff --git a/src/main.rs b/src/main.rs index 54dbf97..2c01ce4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -616,6 +616,20 @@ async fn setup( ) .await?; } + cloud + .occ( + docker, + vec![ + "config:app:set", + "core", + "backgroundjobs_mode", + "--value", + "cron", + ], + None, + Vec::::default(), + ) + .await?; let domains = [ip_str.as_str(), "cloud", &cloud.id, host]; for (i, domain) in domains.iter().enumerate() { From 50dd87298d38977e2d5c113560c460b36a4c940c Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Sun, 20 Sep 2026 15:04:31 +0200 Subject: [PATCH 15/37] cleanup workflow --- .forgejo/workflows/docker.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.forgejo/workflows/docker.yaml b/.forgejo/workflows/docker.yaml index 8791fbc..4cbab3d 100644 --- a/.forgejo/workflows/docker.yaml +++ b/.forgejo/workflows/docker.yaml @@ -9,9 +9,6 @@ on: - ".forgejo/workflows/docker.yaml" - "nix/image/**" -permissions: - contents: read - jobs: build-images: runs-on: nix From 54e748b57904f98787fa742333d4fdb179879745 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Sun, 20 Sep 2026 15:50:31 +0200 Subject: [PATCH 16/37] 2.4.1 --- CHANGELOG.md | 8 ++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d158b83..8c1b845 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.4.1 + +- Show database location on start +- Direct TLS listening support for the proxy +- Set `SERVER_NAME` for frankenphp +- Expose Caddy admin endpoints when using frankenphp +- Use cron for background jobs + ## 2.4.0 - Show instance details in shell prompt diff --git a/Cargo.lock b/Cargo.lock index 32ae3ea..97e3e78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -899,7 +899,7 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "haze" -version = "2.4.0" +version = "2.4.1" dependencies = [ "async-trait", "atty", diff --git a/Cargo.toml b/Cargo.toml index 298e0c3..c21a0a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "haze" -version = "2.4.0" +version = "2.4.1" edition = "2024" description = "Easy setup and management of Nextcloud test instances using docker" repository = "https://codeberg.org/icewind/haze" From d398b9f9e107ab692f4b465ea4d953c132e9ae8b Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Sun, 20 Sep 2026 16:21:57 +0200 Subject: [PATCH 17/37] allow specifying #! inside shell fixes #51 --- CHANGELOG.md | 4 ++++ README.md | 15 +++++++++++-- example-script.sh | 1 + src/script.rs | 57 ++++++++++++++++++++++++++++++++++------------- 4 files changed, 60 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c1b845..d3e4fd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased + +- Support having a `#!` inside a haze script to specify the interperter to use + ## 2.4.1 - Show database location on start diff --git a/README.md b/README.md index d66688b..b05dfe4 100644 --- a/README.md +++ b/README.md @@ -447,8 +447,19 @@ script can be overriden by running the script with ## Nushell scripts -By default, the script contents are executed as a `bash` script, you can instead -execute the script as a `nushell` script by using `.nu` as the file extention. +By default, the script contents are executed as either `bash` script, or `nu` script based on the extension. + +You can overwrite the interpreter used to execute the script by adding an extra `#!` line to the script, for example: + +```bash +#! /usr/bin/env -S haze script +#! haze shell pgsql s3 +#! php -f + shell, + (None, Some("nu")) => vec!["nu".into()], + _ => vec!["bash".into()], }; options.mappings.push( @@ -61,7 +63,10 @@ pub async fn run_script( cloud .exec( docker, - vec![shell.to_string(), target_path.into_string()], + shell + .into_iter() + .chain(once(target_path.into_string())) + .collect(), true, get_forward_env(), ) @@ -74,14 +79,22 @@ pub async fn run_script( Ok(()) } -fn parse_script(script: &str, config: &HazeConfig) -> Result<(CloudOptions, ScriptMode)> { - let (options, mode) = script +#[derive(Debug)] +struct Script { + options: CloudOptions, + mode: ScriptMode, + shell: Option>, +} + +fn parse_script(script: &str, config: &HazeConfig) -> Result