Files
2026-10-10 15:58:44 +02:00

235 lines
8.1 KiB
Nix

{ 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
}
}
'';
};
};
}