{ config, ... }: let nixosModules = config.flake.modules.nixos; in { flake.modules.nixos.wardrowbe = { config, lib, ... }: let service = config.facts.services.wardrowbe; auth = config.facts.services.wardrowbe-auth; podman = lib.getExe config.virtualisation.podman.package; # Multi-architecture manifest digests; updating a tag alone cannot change an image. images = { backend = "ghcr.io/anyesh/wardrowbe:backend-1.10.3@sha256:75fd6f85fb7b91ccbb227b52d616bab11b5130c4e22858579f45736419fb592d"; frontend = "ghcr.io/anyesh/wardrowbe:frontend-1.10.3@sha256:730cf52bd8a03b94473b129e9a988b3dae9818e9377fd88917029156ee7a2e17"; auth = "ghcr.io/tinyauthapp/tinyauth:v5.2.0@sha256:220aa235c76393a73fece831d7ba967b8698d3907fd8e3c4c952f699c7b142a5"; postgres = "docker.io/library/postgres:15-alpine@sha256:f7d23353e1b15400d22ebe31189f4d314b87a4c129cc400c8c2d8d4ca127bf81"; redis = "docker.io/library/redis:7-alpine@sha256:858f009f9709ce576febc734aa78b8f6d624b82571f9ddb6bda4377c833b3499"; }; healthOptions = [ "--health-interval=10s" "--health-timeout=5s" "--health-start-period=30s" "--health-retries=6" "--health-on-failure=kill" ]; backendContainer = { image = images.backend; environment = { # Profile selection is available only behind the password gateway. DEBUG = "true"; AI_INTERNAL_ENABLED = "false"; AI_VISION_ENABLED = "false"; AI_TEXT_ENABLED = "false"; APP_URL = service.origin; CORS_ORIGINS = builtins.toJSON [ service.origin ]; REDIS_URL = "redis://wardrowbe-redis:6379/0"; STORAGE_PATH = "/data/uploads"; LOG_LEVEL = "INFO"; }; environmentFiles = [ config.sops.templates."wardrowbe-backend.env".path ]; volumes = [ "wardrowbe-uploads:/data/uploads" ]; }; backendUnits = [ "podman-wardrowbe-backend.service" "podman-wardrowbe-worker.service" "podman-wardrowbe-image-worker.service" ]; containers = { wardrowbe-postgres = { image = images.postgres; environment = { POSTGRES_USER = "wardrobe"; POSTGRES_DB = "wardrobe"; }; environmentFiles = [ config.sops.templates."wardrowbe-postgres.env".path ]; volumes = [ "wardrowbe-postgres:/var/lib/postgresql/data" ]; podman.sdnotify = "healthy"; extraOptions = healthOptions ++ [ "--health-cmd=pg_isready -h 127.0.0.1 -U wardrobe -d wardrobe" ]; }; wardrowbe-redis = { image = images.redis; cmd = [ "redis-server" "--appendonly" "yes" ]; volumes = [ "wardrowbe-redis:/data" ]; podman.sdnotify = "healthy"; extraOptions = healthOptions ++ [ "--health-cmd=redis-cli ping" ]; }; wardrowbe-backend = backendContainer // { dependsOn = [ "wardrowbe-postgres" "wardrowbe-redis" ]; ports = [ "127.0.0.1:8002:8000" ]; cmd = [ "sh" "-c" "alembic upgrade head && exec uvicorn app.main:app --host 0.0.0.0 --port 8000" ]; podman.sdnotify = "healthy"; extraOptions = healthOptions ++ [ "--health-cmd=curl --fail --silent http://127.0.0.1:8000/api/v1/health" ]; }; wardrowbe-worker = backendContainer // { dependsOn = [ "wardrowbe-backend" ]; cmd = [ "arq" "app.workers.worker.WorkerSettings" ]; }; wardrowbe-image-worker = backendContainer // { dependsOn = [ "wardrowbe-backend" ]; cmd = [ "arq" "app.workers.image_worker.ImageWorkerSettings" ]; }; wardrowbe-frontend = { image = images.frontend; dependsOn = [ "wardrowbe-backend" ]; environment = { BACKEND_URL = "http://wardrowbe-backend:8000"; NEXTAUTH_URL = service.origin; DEV_MODE = "true"; }; environmentFiles = [ config.sops.templates."wardrowbe-frontend.env".path ]; ports = [ "127.0.0.1:3002:3000" ]; podman.sdnotify = "healthy"; extraOptions = healthOptions ++ [ "--health-cmd=node -e \"fetch('http://127.0.0.1:3000').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))\"" ]; }; wardrowbe-auth = { image = images.auth; environment = { TINYAUTH_APPURL = auth.origin; TINYAUTH_AUTH_SECURECOOKIE = "true"; TINYAUTH_AUTH_TRUSTEDPROXIES = "10.89.30.1"; TINYAUTH_LABELPROVIDER = "none"; TINYAUTH_ANALYTICS_ENABLED = "false"; TINYAUTH_DATABASE_PATH = "/data/tinyauth.db"; TINYAUTH_UI_TITLE = "Wardrobe login"; }; environmentFiles = [ config.sops.templates."wardrowbe-auth.env".path ]; volumes = [ "wardrowbe-auth:/data" ]; ports = [ "127.0.0.1:3003:3000" ]; podman.sdnotify = "healthy"; extraOptions = healthOptions ++ [ "--health-cmd=tinyauth healthcheck" ]; }; }; in { imports = [ nixosModules.caddy nixosModules.sops ]; sops.secrets = { wardrowbe-postgres-password = { }; wardrowbe-secret-key = { }; wardrowbe-nextauth-secret = { }; wardrowbe-auth-users = { }; }; sops.templates = { "wardrowbe-postgres.env" = { content = '' POSTGRES_PASSWORD=${config.sops.placeholder.wardrowbe-postgres-password} ''; restartUnits = [ "podman-wardrowbe-postgres.service" ]; }; "wardrowbe-backend.env" = { content = '' DATABASE_URL=postgresql+asyncpg://wardrobe:${config.sops.placeholder.wardrowbe-postgres-password}@wardrowbe-postgres:5432/wardrobe SECRET_KEY=${config.sops.placeholder.wardrowbe-secret-key} ''; restartUnits = backendUnits; }; "wardrowbe-frontend.env" = { content = '' NEXTAUTH_SECRET=${config.sops.placeholder.wardrowbe-nextauth-secret} ''; restartUnits = [ "podman-wardrowbe-frontend.service" ]; }; "wardrowbe-auth.env" = { content = '' TINYAUTH_AUTH_USERS=${config.sops.placeholder.wardrowbe-auth-users} ''; restartUnits = [ "podman-wardrowbe-auth.service" ]; }; }; virtualisation.oci-containers = { backend = "podman"; containers = lib.mapAttrs ( _: container: { networks = [ "wardrowbe" ]; } // container ) containers; }; systemd.services = { wardrowbe-network = { description = "Wardrowbe container network"; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; script = '' ${podman} network exists wardrowbe || \ ${podman} network create --subnet 10.89.30.0/24 --gateway 10.89.30.1 wardrowbe ''; }; } // lib.mapAttrs' ( name: _: lib.nameValuePair "podman-${name}" { requires = [ "wardrowbe-network.service" ]; after = [ "wardrowbe-network.service" ]; serviceConfig.TimeoutStartSec = lib.mkForce 900; } ) containers; services.caddy.virtualHosts = { ${auth.domain}.extraConfig = "reverse_proxy 127.0.0.1:3003"; ${service.domain}.extraConfig = '' request_body { max_size 50MB } route { forward_auth 127.0.0.1:3003 { uri /api/auth/caddy } handle /api/auth/* { reverse_proxy 127.0.0.1:3002 } handle /api/* { reverse_proxy 127.0.0.1:8002 } handle { reverse_proxy 127.0.0.1:3002 } } ''; }; }; }