Skip to main content

Nix Deployment

Deploy Aya using Nix for fully reproducible builds and declarative infrastructure configuration.

Why Nix?

Reproducible

Same input = same output, always

Declarative

Infrastructure as code

Atomic

Rollbacks are instant and safe

Development Environment

Aya includes a flake.nix that provides all development tools:
flake.nix
{
  description = "aya.is development environment";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs { inherit system; };
        go = pkgs.go_1_25;
        nodejs = pkgs.nodejs_20;
      in {
        devShells.default = pkgs.mkShell {
          packages = [
            # Go toolchain
            go
            pkgs.golangci-lint
            pkgs.govulncheck
            pkgs.air
            pkgs.sqlc

            # Frontend
            nodejs
            pkgs.deno
            pkgs.pnpm

            # Database
            pkgs.postgresql_16

            # Tools
            pkgs.git
            pkgs.gnumake
            pkgs.docker
            pkgs.docker-compose
            pkgs.pre-commit
          ];

          shellHook = ''
            pre-commit install --install-hooks > /dev/null 2>&1
            echo "AYA dev shell ready."
          '';
        };
      }
    );
}

Entering the Shell

# Enter development shell
nix develop

# Or use direnv for automatic activation
echo "use flake" > .envrc
direnv allow
Benefits:
  • Exact versions of all tools (Go 1.25, Node 20, Deno, etc.)
  • Same environment on macOS, Linux, NixOS
  • No global installation pollution
  • Instant rollback to previous versions

NixOS Module

For NixOS servers, create a module for Aya:
/etc/nixos/modules/aya.nix
{ config, lib, pkgs, ... }:

with lib;

let
  cfg = config.services.aya;
  ayaPkg = pkgs.callPackage ./aya-package.nix {};
in {
  options.services.aya = {
    enable = mkEnableOption "Aya community platform";

    domain = mkOption {
      type = types.str;
      default = "aya.is";
      description = "Domain name for the Aya instance";
    };

    jwtSecret = mkOption {
      type = types.str;
      description = "JWT secret for authentication";
    };

    database = {
      host = mkOption {
        type = types.str;
        default = "localhost";
      };

      port = mkOption {
        type = types.port;
        default = 5432;
      };

      name = mkOption {
        type = types.str;
        default = "aya";
      };

      user = mkOption {
        type = types.str;
        default = "aya";
      };

      passwordFile = mkOption {
        type = types.path;
        description = "Path to file containing database password";
      };
    };
  };

  config = mkIf cfg.enable {
    # PostgreSQL
    services.postgresql = {
      enable = true;
      package = pkgs.postgresql_16;
      ensureDatabases = [ cfg.database.name ];
      ensureUsers = [{
        name = cfg.database.user;
        ensureDBOwnership = true;
      }];
    };

    # Aya backend service
    systemd.services.aya-backend = {
      description = "Aya Backend API";
      after = [ "network.target" "postgresql.service" ];
      wantedBy = [ "multi-user.target" ];

      environment = {
        ENV = "production";
        PORT = "8080";
        AUTH__JWT_SECRET = cfg.jwtSecret;
        CONN__targets__default__protocol = "postgres";
        CONN__targets__default__dsn = "postgres://${cfg.database.user}:$(cat ${cfg.database.passwordFile})@${cfg.database.host}:${toString cfg.database.port}/${cfg.database.name}";
      };

      serviceConfig = {
        Type = "simple";
        User = "aya";
        ExecStart = "${ayaPkg}/bin/aya-server";
        Restart = "on-failure";
        RestartSec = "10s";
      };
    };

    # Aya frontend service
    systemd.services.aya-frontend = {
      description = "Aya Frontend";
      after = [ "network.target" ];
      wantedBy = [ "multi-user.target" ];

      environment = {
        BACKEND_URI = "http://localhost:8080";
      };

      serviceConfig = {
        Type = "simple";
        User = "aya";
        ExecStart = "${ayaPkg}/bin/aya-frontend";
        Restart = "on-failure";
      };
    };

    # Nginx reverse proxy
    services.nginx = {
      enable = true;
      recommendedProxySettings = true;
      recommendedTlsSettings = true;

      virtualHosts."${cfg.domain}" = {
        enableACME = true;
        forceSSL = true;

        locations."/api/" = {
          proxyPass = "http://localhost:8080/";
        };

        locations."/" = {
          proxyPass = "http://localhost:3000";
          proxyWebsockets = true;
        };
      };
    };

    # User and group
    users.users.aya = {
      isSystemUser = true;
      group = "aya";
      home = "/var/lib/aya";
      createHome = true;
    };

    users.groups.aya = {};

    # Firewall
    networking.firewall.allowedTCPPorts = [ 80 443 ];
  };
}

Aya Package Definition

/etc/nixos/modules/aya-package.nix
{ lib, buildGoModule, buildNpmPackage, fetchFromGitHub }:

let
  version = "1.0.0";
  src = fetchFromGitHub {
    owner = "eser";
    repo = "aya.is";
    rev = "v${version}";
    sha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
  };

  backend = buildGoModule {
    pname = "aya-backend";
    inherit version src;

    sourceRoot = "source/apps/services";

    vendorHash = "sha256-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=";

    buildPhase = ''
      go build -o aya-server ./cmd/serve
    '';

    installPhase = ''
      mkdir -p $out/bin
      cp aya-server $out/bin/
      cp -r etc $out/
      cp config.json $out/
    '';
  };

  frontend = buildNpmPackage {
    pname = "aya-frontend";
    inherit version src;

    sourceRoot = "source/apps/webclient";

    npmDepsHash = "sha256-CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=";

    buildPhase = ''
      npm run build
    '';

    installPhase = ''
      mkdir -p $out/bin
      cp -r .output $out/
      cat > $out/bin/aya-frontend <<EOF
      #!/bin/sh
      exec node $out/.output/server/index.mjs
      EOF
      chmod +x $out/bin/aya-frontend
    '';
  };
in
{
  inherit backend frontend;
  bin = lib.symlinkJoin {
    name = "aya";
    paths = [ backend frontend ];
  };
}

NixOS Configuration

/etc/nixos/configuration.nix
{ config, pkgs, ... }:

{
  imports = [
    ./modules/aya.nix
  ];

  services.aya = {
    enable = true;
    domain = "aya.is";
    jwtSecret = "your-jwt-secret";  # Use secrets management in production!

    database = {
      passwordFile = "/run/secrets/aya-db-password";
    };
  };

  # Automatic backups
  services.postgresqlBackup = {
    enable = true;
    databases = [ "aya" ];
    startAt = "daily";
    location = "/var/backup/postgresql";
  };
}
Apply:
sudo nixos-rebuild switch

Docker with Nix

Build Docker images using Nix for reproducibility:
flake.nix (extended)
{
  outputs = { self, nixpkgs, ... }:
    let
      pkgs = import nixpkgs { system = "x86_64-linux"; };
      ayaPkg = pkgs.callPackage ./aya-package.nix {};
    in {
      packages.x86_64-linux = {
        default = ayaPkg.bin;

        # Docker image for backend
        dockerBackend = pkgs.dockerTools.buildLayeredImage {
          name = "aya-backend";
          tag = "latest";
          contents = [ ayaPkg.backend pkgs.cacert ];

          config = {
            Cmd = [ "${ayaPkg.backend}/bin/aya-server" ];
            ExposedPorts = { "8080/tcp" = {}; };
          };
        };

        # Docker image for frontend
        dockerFrontend = pkgs.dockerTools.buildLayeredImage {
          name = "aya-frontend";
          tag = "latest";
          contents = [ ayaPkg.frontend pkgs.nodejs_20 ];

          config = {
            Cmd = [ "${ayaPkg.frontend}/bin/aya-frontend" ];
            ExposedPorts = { "3000/tcp" = {}; };
          };
        };
      };
    };
}
Build and load:
# Build Docker image
nix build .#dockerBackend

# Load into Docker
docker load < result

# Run
docker run -p 8080:8080 aya-backend:latest
Benefits:
  • Bit-for-bit reproducible images
  • Layer caching optimized by Nix
  • No Dockerfile needed
  • Smaller images (only runtime dependencies)

Secrets Management

Use agenix for encrypted secrets:
{ config, pkgs, ... }:

let
  agenix = builtins.fetchTarball "https://github.com/ryantm/agenix/archive/main.tar.gz";
in {
  imports = [ "${agenix}/modules/age.nix" ];

  age.secrets.aya-jwt-secret = {
    file = ./secrets/aya-jwt-secret.age;
    owner = "aya";
  };

  age.secrets.aya-db-password = {
    file = ./secrets/aya-db-password.age;
    owner = "aya";
  };

  services.aya = {
    jwtSecretFile = config.age.secrets.aya-jwt-secret.path;
    database.passwordFile = config.age.secrets.aya-db-password.path;
  };
}
Create secrets:
# Install age
nix-shell -p age

# Generate key
age-keygen -o ~/.config/age/key.txt

# Encrypt secret
echo "my-jwt-secret" | age -r $(cat ~/.config/age/key.txt.pub) > secrets/aya-jwt-secret.age

sops-nix (Alternative)

Use sops-nix for YAML-based secrets:
secrets.yaml
aya:
  jwt_secret: ENC[AES256_GCM,data:...,iv:...,tag:...]
  db_password: ENC[AES256_GCM,data:...,iv:...,tag:...]
{
  imports = [ <sops-nix/modules/sops> ];

  sops.defaultSopsFile = ./secrets.yaml;
  sops.secrets."aya/jwt_secret" = {
    owner = "aya";
  };

  services.aya.jwtSecretFile = config.sops.secrets."aya/jwt_secret".path;
}

Deployment Workflow

Using deploy-rs

deploy-rs enables declarative deployments:
flake.nix (extended)
{
  inputs = {
    deploy-rs.url = "github:serokell/deploy-rs";
  };

  outputs = { self, nixpkgs, deploy-rs, ... }:
    {
      deploy.nodes.aya-production = {
        hostname = "aya.is";
        profiles.system = {
          user = "root";
          path = deploy-rs.lib.x86_64-linux.activate.nixos self.nixosConfigurations.aya-production;
        };
      };

      nixosConfigurations.aya-production = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          ./configuration.nix
          ./modules/aya.nix
        ];
      };
    };
}
Deploy:
# Deploy to production
nix run github:serokell/deploy-rs -- .#aya-production

# Dry run
nix run github:serokell/deploy-rs -- --dry-activate .#aya-production

# Rollback
nix run github:serokell/deploy-rs -- --rollback .#aya-production

CI/CD with Nix

GitHub Actions

.github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: cachix/install-nix-action@v26
        with:
          nix_path: nixpkgs=channel:nixos-unstable

      - uses: cachix/cachix-action@v14
        with:
          name: aya
          authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'

      # Build and push to Cachix
      - name: Build
        run: |
          nix build .#packages.x86_64-linux.default
          nix build .#dockerBackend
          nix build .#dockerFrontend

      # Deploy to production
      - name: Deploy
        env:
          SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_KEY" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          nix run github:serokell/deploy-rs -- .#aya-production

Monitoring with Nix

Prometheus + Grafana

/etc/nixos/monitoring.nix
{ config, ... }:

{
  services.prometheus = {
    enable = true;
    exporters = {
      node = {
        enable = true;
        enabledCollectors = [ "systemd" ];
      };

      postgres = {
        enable = true;
        dataSourceName = "user=prometheus database=aya";
      };
    };

    scrapeConfigs = [
      {
        job_name = "node";
        static_configs = [{
          targets = [ "localhost:9100" ];
        }];
      }
      {
        job_name = "postgres";
        static_configs = [{
          targets = [ "localhost:9187" ];
        }];
      }
      {
        job_name = "aya-backend";
        static_configs = [{
          targets = [ "localhost:8080" ];
        }];
      }
    ];
  };

  services.grafana = {
    enable = true;
    settings = {
      server.http_port = 3001;
      server.domain = "metrics.aya.is";
    };
  };
}

Advantages of Nix Deployment

Reproducibility

Same flake.lock = identical builds across all environments

Atomic Rollbacks

Instant rollback to any previous version

No Drift

Declarative config prevents configuration drift

Efficient Updates

Binary cache eliminates rebuild times

Troubleshooting

Update hashes after changing dependencies:
# Get new hash
nix-prefetch-url --unpack https://github.com/eser/aya.is/archive/v1.0.0.tar.gz

# Or use lib.fakeSha256 temporarily
sha256 = lib.fakeSha256;
# Build will fail with correct hash
Check systemd logs:
journalctl -u aya-backend -f
journalctl -u aya-frontend -f
Garbage collect old generations:
# Delete generations older than 30 days
sudo nix-collect-garbage --delete-older-than 30d

# Optimize store
sudo nix-store --optimise

Next Steps

Docker Deployment

Alternative deployment with Docker Compose

Environment Variables

Configuration reference

Development Setup

Local development with Nix

Architecture

Understand the system design

Build docs developers (and LLMs) love