diff --git a/modules/nixos/services/tabby-web/README.md b/modules/nixos/services/tabby-web/README.md new file mode 100644 index 0000000..143d2b7 --- /dev/null +++ b/modules/nixos/services/tabby-web/README.md @@ -0,0 +1,196 @@ +# Tabby Web Service Module + +This module provides a NixOS service for running the Tabby Web terminal application server. + +## Features + +- Systemd service with automatic startup +- User and group management +- Database migration on startup +- Configurable environment variables +- Security hardening +- Firewall integration +- Support for PostgreSQL and SQLite databases +- Social authentication configuration + +## Basic Usage + +```nix +{ + mjallen.services.tabby-web = { + enable = true; + port = 9000; + openFirewall = true; + }; +} +``` + +## Advanced Configuration + +```nix +{ + mjallen.services.tabby-web = { + enable = true; + port = 8080; + openFirewall = true; + + # Use PostgreSQL instead of SQLite + databaseUrl = "postgresql://tabby:password@localhost:5432/tabby"; + + # Use S3 for app distribution storage + appDistStorage = "s3://my-bucket/tabby-dist"; + + # Configure social authentication + socialAuth = { + github = { + key = "your-github-oauth-key"; + secret = "your-github-oauth-secret"; + }; + gitlab = { + key = "your-gitlab-oauth-key"; + secret = "your-gitlab-oauth-secret"; + }; + }; + + # Performance tuning + workers = 8; + timeout = 300; + + # Additional environment variables + extraEnvironment = { + DEBUG = "0"; + LOG_LEVEL = "info"; + }; + }; +} +``` + +## Configuration Options + +### Basic Options + +- `enable`: Enable the tabby-web service +- `port`: Port to run the server on (default: 9000) +- `openFirewall`: Whether to open the firewall port (default: false) +- `user`: User to run the service as (default: "tabby-web") +- `group`: Group to run the service as (default: "tabby-web") +- `dataDir`: Data directory (default: "/var/lib/tabby-web") + +### Database Configuration + +- `databaseUrl`: Database connection URL + - SQLite: `"sqlite:///var/lib/tabby-web/tabby.db"` (default) + - PostgreSQL: `"postgresql://user:password@host:port/database"` + +### Storage Configuration + +- `appDistStorage`: Storage URL for app distributions + - Local: `"file:///var/lib/tabby-web/dist"` (default) + - S3: `"s3://bucket-name/path"` + - GCS: `"gcs://bucket-name/path"` + +### Social Authentication + +Configure OAuth providers: + +```nix +socialAuth = { + github = { + key = "oauth-key"; + secret = "oauth-secret"; + }; + gitlab = { + key = "oauth-key"; + secret = "oauth-secret"; + }; + microsoftGraph = { + key = "oauth-key"; + secret = "oauth-secret"; + }; + googleOauth2 = { + key = "oauth-key"; + secret = "oauth-secret"; + }; +}; +``` + +### Performance Options + +- `workers`: Number of gunicorn worker processes (default: 4) +- `timeout`: Worker timeout in seconds (default: 120) + +### Additional Configuration + +- `extraEnvironment`: Additional environment variables as an attribute set + +## Service Management + +```bash +# Start the service +sudo systemctl start tabby-web + +# Enable automatic startup +sudo systemctl enable tabby-web + +# Check service status +sudo systemctl status tabby-web + +# View logs +sudo journalctl -u tabby-web -f + +# Run management commands +sudo -u tabby-web tabby-web-manage migrate +sudo -u tabby-web tabby-web-manage add_version 1.0.156-nightly.2 +``` + +## Security + +The service runs with extensive security hardening: + +- Dedicated user and group +- Restricted filesystem access +- No new privileges +- Protected system directories +- Private temporary directory +- Memory execution protection +- Namespace restrictions + +## Database Setup + +### PostgreSQL + +If using PostgreSQL, ensure the database and user exist: + +```sql +CREATE USER tabby WITH PASSWORD 'your-password'; +CREATE DATABASE tabby OWNER tabby; +``` + +### SQLite + +SQLite databases are created automatically in the data directory. + +## Troubleshooting + +1. **Service fails to start**: Check logs with `journalctl -u tabby-web` +2. **Database connection issues**: Verify database URL and credentials +3. **Permission errors**: Ensure data directory has correct ownership +4. **Port conflicts**: Check if another service is using the configured port + +## Integration with Reverse Proxy + +Example Nginx configuration: + +```nginx +server { + listen 80; + server_name tabby.example.com; + + location / { + proxy_pass http://localhost:9000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/modules/nixos/services/tabby-web/default.nix b/modules/nixos/services/tabby-web/default.nix new file mode 100644 index 0000000..622e0ac --- /dev/null +++ b/modules/nixos/services/tabby-web/default.nix @@ -0,0 +1,121 @@ +{ + config, + lib, + pkgs, + namespace, + ... +}: +with lib; +let + cfg = config.${namespace}.services.tabby-web; + + # Build environment variables from configuration + environmentVars = { + DATABASE_URL = cfg.databaseUrl; + APP_DIST_STORAGE = cfg.appDistStorage; + PORT = toString cfg.port; + } + // optionalAttrs (cfg.socialAuth.github.key != null) { + SOCIAL_AUTH_GITHUB_KEY = cfg.socialAuth.github.key; + } + // optionalAttrs (cfg.socialAuth.github.secret != null) { + SOCIAL_AUTH_GITHUB_SECRET = cfg.socialAuth.github.secret; + } + // optionalAttrs (cfg.socialAuth.gitlab.key != null) { + SOCIAL_AUTH_GITLAB_KEY = cfg.socialAuth.gitlab.key; + } + // optionalAttrs (cfg.socialAuth.gitlab.secret != null) { + SOCIAL_AUTH_GITLAB_SECRET = cfg.socialAuth.gitlab.secret; + } + // optionalAttrs (cfg.socialAuth.microsoftGraph.key != null) { + SOCIAL_AUTH_MICROSOFT_GRAPH_KEY = cfg.socialAuth.microsoftGraph.key; + } + // optionalAttrs (cfg.socialAuth.microsoftGraph.secret != null) { + SOCIAL_AUTH_MICROSOFT_GRAPH_SECRET = cfg.socialAuth.microsoftGraph.secret; + } + // optionalAttrs (cfg.socialAuth.googleOauth2.key != null) { + SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = cfg.socialAuth.googleOauth2.key; + } + // optionalAttrs (cfg.socialAuth.googleOauth2.secret != null) { + SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = cfg.socialAuth.googleOauth2.secret; + } + // cfg.extraEnvironment; + +in +{ + imports = [ ./options.nix ]; + + config = mkIf cfg.enable { + # Create user and group + users.users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + home = cfg.dataDir; + createHome = true; + description = "Tabby Web service user"; + }; + + users.groups.${cfg.group} = { }; + + # Ensure data directory exists with correct permissions + systemd.tmpfiles.rules = [ + "d '${cfg.dataDir}' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.dataDir}/dist' 0750 ${cfg.user} ${cfg.group} - -" + ]; + + # Create the systemd service + systemd.services.tabby-web = { + description = "Tabby Web Terminal Application Server"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ] ++ optional (hasPrefix "postgresql://" cfg.databaseUrl) "postgresql.service"; + + environment = environmentVars; + + serviceConfig = { + Type = "exec"; + User = cfg.user; + Group = cfg.group; + WorkingDirectory = cfg.dataDir; + + # Use the tabby-web package from our custom packages + ExecStart = "${pkgs.${namespace}.tabby-web}/bin/tabby-web --workers ${toString cfg.workers} --timeout ${toString cfg.timeout}"; + + # Run database migrations before starting the service + ExecStartPre = "${pkgs.${namespace}.tabby-web}/bin/tabby-web-manage migrate"; + + # Security settings + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = true; + ReadWritePaths = [ cfg.dataDir ]; + PrivateTmp = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictSUIDSGID = true; + RestrictRealtime = true; + RestrictNamespaces = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + + # Restart policy + Restart = "always"; + RestartSec = "10s"; + + # Resource limits + LimitNOFILE = "65536"; + }; + + # Ensure the service starts after database if using PostgreSQL + requisite = mkIf (hasPrefix "postgresql://" cfg.databaseUrl) [ "postgresql.service" ]; + }; + + # Open firewall if requested + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = [ cfg.port ]; + }; + + # Add the tabby-web package to system packages + environment.systemPackages = [ pkgs.${namespace}.tabby-web ]; + }; +} diff --git a/modules/nixos/services/tabby-web/example.nix b/modules/nixos/services/tabby-web/example.nix new file mode 100644 index 0000000..b15b81e --- /dev/null +++ b/modules/nixos/services/tabby-web/example.nix @@ -0,0 +1,45 @@ +# Example configuration for Tabby Web service +# Add this to your NixOS configuration to enable tabby-web + +{ + # Basic configuration - SQLite database, local storage + mjallen.services.tabby-web = { + enable = true; + port = 9000; + openFirewall = true; + }; + + # Advanced configuration example (commented out) + /* + mjallen.services.tabby-web = { + enable = true; + port = 8080; + openFirewall = true; + + # Use PostgreSQL database + databaseUrl = "postgresql://tabby:password@localhost:5432/tabby"; + + # Use S3 for app distribution storage + appDistStorage = "s3://my-bucket/tabby-dist"; + + # Configure GitHub OAuth + socialAuth.github = { + key = "your-github-oauth-key"; + secret = "your-github-oauth-secret"; + }; + + # Performance tuning + workers = 8; + timeout = 300; + + # Custom data directory + dataDir = "/srv/tabby-web"; + + # Additional environment variables + extraEnvironment = { + DEBUG = "0"; + LOG_LEVEL = "info"; + }; + }; + */ +} diff --git a/modules/nixos/services/tabby-web/options.nix b/modules/nixos/services/tabby-web/options.nix new file mode 100644 index 0000000..1a62974 --- /dev/null +++ b/modules/nixos/services/tabby-web/options.nix @@ -0,0 +1,127 @@ +{ lib, namespace, ... }: +with lib; +{ + options.${namespace}.services.tabby-web = { + enable = mkEnableOption "Tabby Web terminal application server"; + + port = mkOption { + type = types.port; + default = 9000; + description = "Port for tabby-web server"; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Whether to open firewall for tabby-web"; + }; + + user = mkOption { + type = types.str; + default = "tabby-web"; + description = "User to run tabby-web as"; + }; + + group = mkOption { + type = types.str; + default = "tabby-web"; + description = "Group to run tabby-web as"; + }; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/tabby-web"; + description = "Directory to store tabby-web data"; + }; + + databaseUrl = mkOption { + type = types.str; + default = "sqlite:///var/lib/tabby-web/tabby.db"; + description = "Database connection URL"; + example = "postgresql://user:password@localhost:5432/tabby"; + }; + + appDistStorage = mkOption { + type = types.str; + default = "file:///var/lib/tabby-web/dist"; + description = "Storage URL for app distributions"; + example = "s3://my-bucket/tabby-dist"; + }; + + socialAuth = { + github = { + key = mkOption { + type = types.nullOr types.str; + default = null; + description = "GitHub OAuth key"; + }; + secret = mkOption { + type = types.nullOr types.str; + default = null; + description = "GitHub OAuth secret"; + }; + }; + + gitlab = { + key = mkOption { + type = types.nullOr types.str; + default = null; + description = "GitLab OAuth key"; + }; + secret = mkOption { + type = types.nullOr types.str; + default = null; + description = "GitLab OAuth secret"; + }; + }; + + microsoftGraph = { + key = mkOption { + type = types.nullOr types.str; + default = null; + description = "Microsoft Graph OAuth key"; + }; + secret = mkOption { + type = types.nullOr types.str; + default = null; + description = "Microsoft Graph OAuth secret"; + }; + }; + + googleOauth2 = { + key = mkOption { + type = types.nullOr types.str; + default = null; + description = "Google OAuth2 key"; + }; + secret = mkOption { + type = types.nullOr types.str; + default = null; + description = "Google OAuth2 secret"; + }; + }; + }; + + extraEnvironment = mkOption { + type = types.attrsOf types.str; + default = { }; + description = "Extra environment variables for tabby-web"; + example = { + DEBUG = "1"; + LOG_LEVEL = "info"; + }; + }; + + workers = mkOption { + type = types.ints.positive; + default = 4; + description = "Number of gunicorn worker processes"; + }; + + timeout = mkOption { + type = types.ints.positive; + default = 120; + description = "Worker timeout in seconds"; + }; + }; +} diff --git a/packages/tabby-web/default.nix b/packages/tabby-web/default.nix new file mode 100644 index 0000000..8bfa033 --- /dev/null +++ b/packages/tabby-web/default.nix @@ -0,0 +1,186 @@ +{ + lib, + stdenv, + fetchFromGitHub, + nodejs, + yarn, + python3, + poetry, + makeWrapper, + fetchYarnDeps, + fixup-yarn-lock, + ... +}: + +stdenv.mkDerivation rec { + pname = "tabby-web"; + version = "0.0.1"; + + src = fetchFromGitHub { + owner = "Eugeny"; + repo = "tabby-web"; + rev = "16847cea93f730814c1855241d8ebdea20b1ff6e"; + sha256 = "sha256-FaVJdizSQq600awY9HAwMNv6vpcjLVAVqdWnn+sYAxk="; + }; + + # Fetch yarn dependencies separately for reproducibility + yarnDeps = fetchYarnDeps { + yarnLock = "${src}/frontend/yarn.lock"; + hash = "sha256-NInsyKgp2+ppHJZLFn3qKW08rvSSIShhh2JbR91WgOk="; + }; + + nativeBuildInputs = [ + nodejs + yarn + python3 + poetry + makeWrapper + fixup-yarn-lock + ]; + + buildInputs = [ + python3 + ]; + + propagatedBuildInputs = with python3.pkgs; [ + gunicorn + django + ]; + + configurePhase = '' + runHook preConfigure + + # Set up yarn + export HOME=$TMPDIR + cd frontend + + # Fix up yarn.lock and set up offline cache + fixup-yarn-lock yarn.lock + yarn config --offline set yarn-offline-mirror ${yarnDeps} + yarn install --offline --frozen-lockfile --ignore-platform --ignore-scripts --no-progress --non-interactive + patchShebangs node_modules/ + cd .. + + # Set up poetry + export POETRY_CACHE_DIR=$TMPDIR/poetry-cache + export POETRY_VENV_IN_PROJECT=1 + + runHook postConfigure + ''; + + buildPhase = '' + runHook preBuild + + echo "Building frontend..." + cd frontend + yarn run build + cd .. + + echo "Backend is ready (dependencies will be handled by Nix)" + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + # Create output directories + mkdir -p $out/lib/tabby-web + mkdir -p $out/bin + mkdir -p $out/share/tabby-web + + # Install backend + cp -r backend/* $out/lib/tabby-web/ + + # Install frontend build output + if [ -d frontend/dist ]; then + cp -r frontend/dist/* $out/share/tabby-web/ + elif [ -d frontend/build ]; then + cp -r frontend/build/* $out/share/tabby-web/ + fi + + # Create main executable wrapper + makeWrapper ${python3.withPackages (ps: with ps; [ gunicorn django ])}/bin/python $out/bin/tabby-web \ + --add-flags "-m gunicorn tabby_web.wsgi:application" \ + --set PYTHONPATH "$out/lib/tabby-web" \ + --set DJANGO_SETTINGS_MODULE "tabby_web.settings" \ + --set STATIC_ROOT "$out/share/tabby-web" \ + --run "cd $out/lib/tabby-web" \ + --run 'export DATABASE_URL="''${DATABASE_URL:-sqlite:///tmp/tabby-web.db}"' \ + --run 'export APP_DIST_STORAGE="''${APP_DIST_STORAGE:-file:///tmp/tabby-web-dist}"' \ + --run 'export PORT="''${PORT:-9000}"' \ + --add-flags '--bind "0.0.0.0:$PORT"' \ + --add-flags "--workers 4" \ + --add-flags "--timeout 120" + + # Create Django management wrapper + makeWrapper ${python3.withPackages (ps: with ps; [ django ])}/bin/python $out/bin/tabby-web-manage \ + --add-flags "manage.py" \ + --set PYTHONPATH "$out/lib/tabby-web" \ + --set DJANGO_SETTINGS_MODULE "tabby_web.settings" \ + --set STATIC_ROOT "$out/share/tabby-web" \ + --run "cd $out/lib/tabby-web" \ + --run 'export DATABASE_URL="''${DATABASE_URL:-sqlite:///tmp/tabby-web.db}"' \ + --run 'export APP_DIST_STORAGE="''${APP_DIST_STORAGE:-file:///tmp/tabby-web-dist}"' + + # Create a help script + cat > $out/bin/tabby-web-help << 'HELP_EOF' +#!/bin/bash +cat << 'HELP' +Tabby Web - Terminal application server + +Usage: + tabby-web Start the server + tabby-web-manage Run Django management commands + tabby-web-help Show this help + +Environment Variables: + DATABASE_URL Database connection URL + Examples: sqlite:///path/to/db.sqlite + postgresql://user:pass@host:5432/dbname + + APP_DIST_STORAGE Storage URL for app distributions + Examples: file:///path/to/storage + s3://bucket-name/path + gcs://bucket-name/path + + PORT Server port (default: 9000) + + Social Authentication (optional): + SOCIAL_AUTH_GITHUB_KEY GitHub OAuth key + SOCIAL_AUTH_GITHUB_SECRET GitHub OAuth secret + SOCIAL_AUTH_GITLAB_KEY GitLab OAuth key + SOCIAL_AUTH_GITLAB_SECRET GitLab OAuth secret + SOCIAL_AUTH_MICROSOFT_GRAPH_KEY Microsoft Graph OAuth key + SOCIAL_AUTH_MICROSOFT_GRAPH_SECRET Microsoft Graph OAuth secret + SOCIAL_AUTH_GOOGLE_OAUTH2_KEY Google OAuth2 key + SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET Google OAuth2 secret + +Examples: + # Development with defaults + tabby-web + + # Production with PostgreSQL + DATABASE_URL="postgresql://user:pass@localhost:5432/tabby" tabby-web + + # Run migrations + tabby-web-manage migrate + + # Add app version + tabby-web-manage add_version 1.0.156-nightly.2 +HELP +HELP_EOF + + chmod +x $out/bin/tabby-web-help + + runHook postInstall + ''; + + meta = with lib; { + description = "Web-based terminal application"; + homepage = "https://github.com/Eugeny/tabby-web"; + license = licenses.mit; + maintainers = [ ]; + platforms = platforms.linux ++ platforms.darwin; + }; +} diff --git a/systems/x86_64-linux/jallen-nas/apps.nix b/systems/x86_64-linux/jallen-nas/apps.nix index 0a21513..3cbb670 100755 --- a/systems/x86_64-linux/jallen-nas/apps.nix +++ b/systems/x86_64-linux/jallen-nas/apps.nix @@ -151,6 +151,12 @@ htpasswdFile = "/media/nas/main/backup/restic/.htpasswd"; extraFlags = [ "--no-auth" ]; }; + + tabby-web = { + enable = true; + port = 9000; + openFirewall = true; + }; }; }; }