{ config, pkgs, lib, ... }: with lib; let # Piped domains domain = "piped.cloonar.com"; apiDomain = "pipedapi.cloonar.com"; # Port configuration backendPort = 8082; proxyPort = 8081; bgHelperPort = 3000; # Database configuration dbName = "piped"; dbUser = "piped"; # Piped backend configuration file backendConfig = pkgs.writeText "config.properties" '' # Database configuration # 10.88.0.1 is the default Podman bridge gateway IP hibernate.connection.url=jdbc:postgresql://10.89.0.1:5432/${dbName} hibernate.connection.driver_class=org.postgresql.Driver hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect hibernate.connection.username=${dbUser} hibernate.connection.password=PLACEHOLDER_DB_PASSWORD # Server configuration PORT=${toString backendPort} HTTP_WORKERS=2 # Proxy configuration PROXY_PART=https://${apiDomain}/proxy # API URL API_URL=https://${apiDomain} # Frontend URL FRONTEND_URL=https://${domain} # Disable registration (private instance) DISABLE_REGISTRATION=false # ReCaptcha (disabled for private instance) CAPTCHA_ENABLED=false # Matrix support (optional) MATRIX_SERVER= # Sentry (disabled) SENTRY_DSN= # Compromised password check (optional) COMPROMISED_PASSWORD_CHECK=true # Feed retention (days) FEED_RETENTION=30 # Background helper for iOS compatibility (generates PoTokens) BG_HELPER_URL=http://piped-bg-helper:${toString bgHelperPort} ''; # Piped frontend configuration # Note: Piped requires the config in a specific format as a JSON file served at /config/config.json frontendConfig = pkgs.writeText "config.json" (builtins.toJSON { apiUrl = "https://${apiDomain}"; disableRegistration = false; instanceName = "Piped (Private)"; }); in { sops.secrets = { # Database password for postgres user (used by piped-db-init) piped-db-password-postgres = { key = "piped-db-password"; owner = "postgres"; }; # Database password for piped user (used by piped-config-generate) piped-db-password-piped = { key = "piped-db-password"; owner = "piped"; }; }; # Create system user for Piped users.users.piped = { isSystemUser = true; group = "piped"; home = "/var/lib/piped"; createHome = true; }; users.groups.piped = { }; # Note: piped user doesn't need special group membership for Podman # Create piped config directory structure systemd.tmpfiles.rules = [ "d /var/lib/piped 0700 piped piped - -" "d /var/lib/piped/config 0700 piped piped - -" ]; # PostgreSQL database setup services.postgresql = { enable = true; ensureDatabases = [ dbName ]; ensureUsers = [{ name = dbUser; ensureDBOwnership = true; }]; # Allow connections from Podman containers settings = { listen_addresses = mkForce "*"; }; authentication = pkgs.lib.mkOverride 10 '' # Allow local connections local all all trust # Allow connections from localhost host all all 127.0.0.1/32 trust host all all ::1/128 trust # Allow connections from Podman network (typically 10.88.0.0/16) host ${dbName} ${dbUser} 10.88.0.0/16 scram-sha-256 host ${dbName} ${dbUser} 10.89.0.0/16 scram-sha-256 ''; }; # PostgreSQL backup services.postgresqlBackup.databases = [ dbName ]; # Allow Podman containers to connect to PostgreSQL networking.firewall.interfaces."podman1".allowedTCPPorts = [ 5432 ]; networking.firewall.interfaces."podman1".allowedUDPPorts = [ 53 5432 ]; # Setup database password (runs before containers start) systemd.services.piped-db-init = { description = "Initialize Piped database password"; wantedBy = [ "multi-user.target" ]; after = [ "postgresql.service" ]; requires = [ "postgresql.service" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; User = "postgres"; Group = "postgres"; }; script = '' password=$(cat ${config.sops.secrets.piped-db-password-postgres.path}) ${config.services.postgresql.package}/bin/psql -c "ALTER USER ${dbUser} WITH PASSWORD '$password';" ''; }; # Create Piped backend config with actual password systemd.services.piped-config-generate = { description = "Generate Piped backend configuration"; wantedBy = [ "multi-user.target" ]; before = [ "podman-piped-backend.service" ]; after = [ "piped-db-init.service" ]; requires = [ "piped-db-init.service" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; User = "piped"; Group = "piped"; }; script = '' mkdir -p /var/lib/piped/config password=$(cat ${config.sops.secrets.piped-db-password-piped.path}) sed "s|PLACEHOLDER_DB_PASSWORD|$password|" ${backendConfig} > /var/lib/piped/config/config.properties chmod 600 /var/lib/piped/config/config.properties ''; }; # Use Podman for OCI containers virtualisation.oci-containers.backend = "podman"; # Create Piped network for container-to-container communication systemd.services.init-piped-network = { description = "Create Podman network for Piped services"; wantedBy = [ "multi-user.target" ]; before = [ "podman-piped-backend.service" "podman-piped-bg-helper.service" "podman-piped-proxy.service" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; script = '' ${pkgs.podman}/bin/podman network exists piped-net || \ ${pkgs.podman}/bin/podman network create --interface-name=podman1 --subnet=10.89.0.0/24 piped-net ''; }; # Piped Backend Podman container (using custom image with iOS compatibility fixes) virtualisation.oci-containers.containers.piped-backend = { image = "git.cloonar.com/infrastructure/piped-backend:latest"; ports = [ "127.0.0.1:${toString backendPort}:${toString backendPort}" ]; volumes = [ "/var/lib/piped/config/config.properties:/app/config.properties:ro" ]; extraOptions = [ "--pull=newer" "--network=podman" # Default bridge for PostgreSQL access at 10.88.0.1 "--network=piped-net" # Custom network for DNS resolution to bg-helper ]; }; # Ensure config is generated before backend container starts systemd.services."podman-piped-backend" = { after = mkAfter [ "piped-config-generate.service" ]; requires = mkAfter [ "piped-config-generate.service" ]; }; # Piped Background Helper (generates PoTokens for iOS compatibility) virtualisation.oci-containers.containers.piped-bg-helper = { image = "1337kavin/bg-helper-server:latest"; ports = [ "127.0.0.1:${toString bgHelperPort}:3000" ]; extraOptions = [ "--pull=newer" "--network=piped-net" ]; }; # Piped Proxy Podman container virtualisation.oci-containers.containers.piped-proxy = { image = "1337kavin/piped-proxy:latest"; ports = [ "127.0.0.1:${toString proxyPort}:8080" ]; environment = { UDS = "0"; # Disable Unix domain sockets }; extraOptions = [ "--pull=newer" "--network=piped-net" ]; }; # Nginx configuration services.nginx.virtualHosts.${domain} = { forceSSL = true; enableACME = true; acmeRoot = null; # Serve Piped frontend static files root = "${pkgs.piped}"; # Frontend (root) locations."/" = { extraConfig = '' try_files $uri $uri/ /index.html; ''; }; # Serve custom frontend config locations."= /config/config.json" = { alias = frontendConfig; extraConfig = '' add_header Content-Type application/json; add_header Access-Control-Allow-Origin *; ''; }; }; # API and Proxy domain services.nginx.virtualHosts.${apiDomain} = { forceSSL = true; enableACME = true; acmeRoot = null; # Backend API (served at root) locations."/" = { proxyPass = "http://127.0.0.1:${toString backendPort}/"; proxyWebsockets = true; extraConfig = '' # Increase timeouts for long-running requests proxy_connect_timeout 600s; proxy_send_timeout 600s; proxy_read_timeout 600s; ''; }; # YouTube Proxy locations."/proxy/" = { proxyPass = "http://127.0.0.1:${toString proxyPort}/"; extraConfig = '' # CORS headers for video streaming (restricted to own frontend) # add_header Access-Control-Allow-Origin https://${domain} always; # add_header Access-Control-Allow-Credentials "true" always; proxy_buffering on; # Increase buffer sizes for video streaming proxy_buffer_size 128k; proxy_buffers 256 16k; proxy_busy_buffers_size 256k; # Increase timeouts for video streaming proxy_connect_timeout 600s; proxy_send_timeout 600s; proxy_read_timeout 600s; ''; }; }; }