├── .gitignore ├── docker ├── shell ├── bundle ├── shell.nix ├── build_docker_image ├── build ├── update-nix-base ├── push ├── nix-base.nix ├── Dockerfile └── default.nix ├── modules ├── runtimes │ ├── default.nix │ └── ruby │ │ └── default.nix ├── templates │ ├── default.nix │ └── rails │ │ └── default.nix ├── default.nix ├── helpers.nix ├── app.nix └── container.nix ├── default.nix ├── pkgs └── ruby │ ├── 2.7.4.nix │ ├── 2.7.5.nix │ ├── 3.1.1.nix │ └── gem-config.nix ├── update-nixpkgs ├── support ├── base-layer.nix ├── keep-paths.nix ├── eval-spec.nix └── image-tools.nix ├── .github └── workflows │ └── cache.yaml ├── nixpkgs.nix ├── overlay.nix └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | result 2 | result-* 3 | 4 | -------------------------------------------------------------------------------- /docker/shell: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | nix-shell .nix/shell.nix 4 | -------------------------------------------------------------------------------- /modules/runtimes/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | imports = [ 3 | ./ruby 4 | ]; 5 | } 6 | -------------------------------------------------------------------------------- /modules/templates/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | imports = [ 3 | ./rails 4 | ]; 5 | } 6 | -------------------------------------------------------------------------------- /docker/bundle: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | nix-shell --pure -p bundix -p cacert --run "bundix --gemset .nix/gemset.nix" -------------------------------------------------------------------------------- /docker/shell.nix: -------------------------------------------------------------------------------- 1 | { nix-base ? (import ./nix-base.nix {}) }: 2 | (import ./. { inherit nix-base; }).eval.config.outputs.shell 3 | -------------------------------------------------------------------------------- /docker/build_docker_image: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TAG=${TAG:-"latest"} 4 | docker build --platform linux/amd64 -t flyio/nix-build:$TAG . 5 | docker push flyio/nix-build:$TAG 6 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? (import ./nixpkgs.nix {}) }: 2 | 3 | # The evaluated expression here is the full Nixpkgs package set given as an 4 | # input, *composed* with the provided overlay. 5 | pkgs.appendOverlays [ 6 | (import ./overlay.nix) 7 | ] 8 | -------------------------------------------------------------------------------- /modules/default.nix: -------------------------------------------------------------------------------- 1 | # Entry-point to the fly Nix base modules. 2 | 3 | { config, lib, pkgs, ... }: 4 | 5 | { 6 | imports = [ 7 | ./app.nix 8 | ./container.nix 9 | ./helpers.nix 10 | ./runtimes 11 | ./templates 12 | ]; 13 | } 14 | -------------------------------------------------------------------------------- /pkgs/ruby/2.7.4.nix: -------------------------------------------------------------------------------- 1 | { ruby_2_7, fetchFromGitHub }: 2 | 3 | ruby_2_7.overrideAttrs(_: { 4 | version = "2.7.4"; 5 | src = fetchFromGitHub { 6 | owner = "ruby"; 7 | repo = "ruby"; 8 | rev = "v2_7_4"; 9 | sha256 = "sha256-t7hcM9cjp8z1neijl4lARAJwJJikVHqVLJMoOjnOOt8="; 10 | }; 11 | }) 12 | -------------------------------------------------------------------------------- /pkgs/ruby/2.7.5.nix: -------------------------------------------------------------------------------- 1 | { ruby_2_7, fetchFromGitHub }: 2 | 3 | ruby_2_7.overrideAttrs(_: { 4 | version = "2.7.5"; 5 | src = fetchFromGitHub { 6 | owner = "ruby"; 7 | repo = "ruby"; 8 | rev = "v2_7_5"; 9 | sha256 = "sha256-cC3JV/wbmQg1U9ZFVNXb1JCvoa/kNINNhoGEQ70rppg="; 10 | }; 11 | }) 12 | -------------------------------------------------------------------------------- /pkgs/ruby/3.1.1.nix: -------------------------------------------------------------------------------- 1 | { ruby_3_1, fetchFromGitHub }: 2 | 3 | ruby_3_1.overrideAttrs(_: { 4 | version = "3.1.1"; 5 | src = fetchFromGitHub { 6 | owner = "ruby"; 7 | repo = "ruby"; 8 | rev = "v3_1_1"; 9 | sha256 = "sha256-76t/tGyK5nz7nvcRdHJTjjckU+Kv+/kbTMiNWJ93jU8="; 10 | }; 11 | }) 12 | -------------------------------------------------------------------------------- /update-nixpkgs: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | GIT_SHA=$(gh api https://api.github.com/repos/nixos/nixpkgs/commits -q '.[0].sha') 4 | NIX_SHA=$(nix-shell -p nix-universal-prefetch --command "nix-universal-prefetch builtins.fetchTarball --url https://github.com/nixos/nixpkgs/archive/${GIT_SHA}.tar.gz") 5 | 6 | echo "rev = \"$GIT_SHA\";" 7 | echo "sha256 = \"$NIX_SHA\";" 8 | -------------------------------------------------------------------------------- /support/base-layer.nix: -------------------------------------------------------------------------------- 1 | { fly 2 | , busybox 3 | , cacert 4 | }: 5 | 6 | /** 7 | * Minimum basic requirements. 8 | * 9 | * Usage of `busybox` assumes a shell is needed (it generally is for POSIX `/bin/sh`). 10 | * Usage of `cacert` is needed for SSL certificates. 11 | * 12 | * Adding to this layer affects every default Fly image templates. 13 | */ 14 | fly.imageTools.buildSpecifiedLayers { 15 | layeredContent = [ 16 | { contents = [ busybox cacert ]; } 17 | ]; 18 | } 19 | -------------------------------------------------------------------------------- /docker/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #!nix-shell -I .nix/default.nix -p flyctl skopeo dive yq jq git -i bash 3 | 4 | set -e 5 | 6 | echo "Building ${PROJECT_NAME}..." 7 | 8 | COMMAND="nix-build .nix -A eval.config.outputs.container.image" 9 | 10 | if [ -n "$NIX_BASE_DEV" ]; then 11 | COMMAND="${COMMAND} --arg nix-base '(import ../../internal/nix-base {})'" 12 | fi 13 | 14 | echo $COMMAND 15 | export ARCHIVE_PATH=$($COMMAND) 16 | echo "Image tarball: $ARCHIVE_PATH" 17 | 18 | $(dirname "$0")/push $ARCHIVE_PATH $TAG -------------------------------------------------------------------------------- /.github/workflows/cache.yaml: -------------------------------------------------------------------------------- 1 | name: "Cache Nix Builds" 2 | on: 3 | pull_request: 4 | push: 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest, macos-latest] 10 | runs-on: ${{ matrix.os }} 11 | steps: 12 | - uses: actions/checkout@v2.4.0 13 | - uses: cachix/install-nix-action@v16 14 | with: 15 | nix_path: nixpkgs=channel:nixos-unstable 16 | - uses: cachix/cachix-action@v10 17 | with: 18 | name: flyio 19 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 20 | - run: nix-build -A ruby_2_7_4 21 | 22 | -------------------------------------------------------------------------------- /docker/update-nix-base: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #!nix-shell -p dasel nix-universal-prefetch -i bash 3 | 4 | GIT_SHA=$(gh api https://api.github.com/repos/fly-apps/nix-base/commits -q '.[0].sha') 5 | NIX_SHA=$(nix-universal-prefetch builtins.fetchTarball --url https://github.com/fly-apps/nix-base/archive/${GIT_SHA}.tar.gz) 6 | 7 | if [ -f "nix.toml" ]; then 8 | echo "Updating nix.toml with rev and sha256 values." 9 | dasel put string -f nix.toml -s '.requirements.nix_base_revision' $GIT_SHA 10 | dasel put string -f nix.toml -s '.requirements.nix_base_sha256' $NIX_SHA 11 | fi 12 | 13 | echo "rev = \"$GIT_SHA\";" 14 | echo "sha256 = \"$NIX_SHA\";" 15 | -------------------------------------------------------------------------------- /docker/push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #!nix-shell -I .nix/default.nix -p flyctl skopeo dive yq jq git -i bash 3 | 4 | set -ue 5 | 6 | 7 | ARCHIVE_PATH=$1 8 | 9 | if [ -n "$2" ]; then 10 | TAG=$2 11 | fi 12 | 13 | echo "Pushing ${ARCHIVE_PATH} to $TAG..." 14 | skopeo \ 15 | copy docker-archive:"$ARCHIVE_PATH" \ 16 | docker://$TAG \ 17 | --dest-creds x:$FLY_API_TOKEN \ 18 | --insecure-policy \ 19 | --format v2s2 20 | echo "Done." 21 | 22 | SIZE=$(skopeo inspect --raw docker-archive://${ARCHIVE_PATH} | jq -r '[ .layers[].size ] | add') 23 | ((SIZE_IN_MB=$SIZE/1024/1024)) 24 | echo "Image size: ${SIZE_IN_MB}MB" 25 | echo "Deploy with: fly deploy -i ${TAG}" 26 | -------------------------------------------------------------------------------- /support/keep-paths.nix: -------------------------------------------------------------------------------- 1 | { lib, nix-filter }: 2 | 3 | { root, paths }: 4 | 5 | nix-filter { 6 | inherit root; 7 | include = 8 | # Given a list of paths, provides prefixes so that the directories are 9 | # included in the `nix-filter` lookup. 10 | lib.flatten ( 11 | map (str: 12 | let 13 | # "a/b/c" → [ "a" "b" "c" ] 14 | parts = lib.splitString "/" str; 15 | # [ "a" "b" "c" ] → [ "a" "a/b" "a/b/c" ] 16 | paths = lib.foldl (result: component: 17 | result ++ [((lib.last result) + "/" + component)] 18 | ) [ (lib.head parts) ] parts; 19 | in 20 | [ 21 | parts 22 | (nix-filter.inDirectory str) 23 | ] 24 | ) paths 25 | ) 26 | ; 27 | } 28 | -------------------------------------------------------------------------------- /nixpkgs.nix: -------------------------------------------------------------------------------- 1 | # 2 | # This can be replaced by any method of tracking inputs. 3 | # - niv https://github.com/nmattia/niv 4 | # - npins https://github.com/andir/npins 5 | # - Flakes https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake.html 6 | # 7 | # Using this simple purely-Nix shim serves as a placeholder. 8 | 9 | let 10 | # Tracking https://github.com/NixOS/nixpkgs/tree/nixos-unstable 11 | rev = "0c7c76fa9e2f5c7b446c1182c79eabac08588392"; 12 | sha256 = "sha256:0n0j1zg237pfjr1xg2cpg8fwj0fg93knwrxxjv7xwqdks7jy89yf"; 13 | tarball = builtins.fetchTarball { 14 | url = "https://github.com/NixOS/nixpkgs/archive/${rev}.tar.gz"; 15 | inherit sha256; 16 | }; 17 | in 18 | # The builtins.trace is optional; serves as a reminder. 19 | builtins.trace "Using default Nixpkgs revision '${rev}'..." 20 | (import tarball) 21 | -------------------------------------------------------------------------------- /docker/nix-base.nix: -------------------------------------------------------------------------------- 1 | # 2 | # This can be replaced by any method of tracking inputs. 3 | # - niv https://github.com/nmattia/niv 4 | # - npins https://github.com/andir/npins 5 | # - Flakes https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake.html 6 | # 7 | # Using this simple purely-Nix shim serves as a placeholder. 8 | 9 | let 10 | toml = if builtins.pathExists ../nix.toml then 11 | (builtins.fromTOML (builtins.readFile ../nix.toml)) 12 | else 13 | ""; 14 | rev = toml.build.nix_base_revision or "7f455becd1491747d88c0bdb01deb81f5d2a3024"; 15 | sha256 = toml.build.nix_base_sha256 or "sha256:1nw5l177byk2p69cdc9s5hps1qd3llgg0pppv4b9avlpw5x1wlz7"; 16 | tarball = builtins.fetchTarball { 17 | url = "https://github.com/fly-apps/nix-base/archive/${rev}.tar.gz"; 18 | inherit sha256; 19 | }; 20 | in import tarball 21 | -------------------------------------------------------------------------------- /modules/helpers.nix: -------------------------------------------------------------------------------- 1 | { config, lib , ... }: 2 | 3 | let 4 | inherit (lib) 5 | concatStringsSep 6 | filter 7 | mkOption 8 | showWarnings 9 | types 10 | ; 11 | failedAssertions = map (x: x.message) (filter (x: !x.assertion) config.assertions); 12 | applyAssertions = result: 13 | if failedAssertions != [] 14 | then throw "\nFailed assertions:\n${concatStringsSep "\n" (map (x: "- ${x}") failedAssertions)}" 15 | else showWarnings config.warnings result 16 | ; 17 | in 18 | { 19 | options.helpers = { 20 | applyAssertions = mkOption { 21 | type = with types; functionTo anything; 22 | internal = true; 23 | description = '' 24 | Helper that applies assertions before returning the argument. 25 | 26 | Use at any "end" of an evaluation, for example an image build, or an application build. 27 | ''; 28 | }; 29 | }; 30 | 31 | config = { 32 | helpers = { 33 | inherit applyAssertions; 34 | }; 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /support/eval-spec.nix: -------------------------------------------------------------------------------- 1 | { pkgs # Refers to the Nixpkgs eval 2 | , lib 3 | }: 4 | 5 | # Interface used in project default.nix 6 | { config ? {} }: 7 | 8 | let 9 | # Modules added from `pkgs.path` need to be added **before** evaluation. 10 | # Or else `pkgs.path` depends on the modules eval, making an infinite recursion. 11 | fromNixpkgs = map (module: "${toString pkgs.path}/nixos/modules/${module}"); 12 | modulesFromNixpkgs = fromNixpkgs [ 13 | "misc/assertions.nix" 14 | ]; 15 | in 16 | 17 | { 18 | eval = (lib.evalModules { 19 | modules = [ 20 | # Get `pkgs` in there 21 | { 22 | # Prevents `` from being reported. 23 | _file = ./eval-spec.nix; 24 | # Provides the `pkgs` argument for modules. 25 | _module.args.pkgs = pkgs; 26 | } 27 | ] ++ 28 | modulesFromNixpkgs ++ 29 | [ 30 | # Import the local modules 31 | ../modules 32 | # Apply the user config 33 | config 34 | ]; 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /modules/app.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | 3 | let 4 | inherit (lib) 5 | mkOption 6 | types 7 | ; 8 | in 9 | { 10 | options = { 11 | app = { 12 | source = mkOption { 13 | type = with types; oneOf [ path package ]; 14 | description = '' 15 | Source of the application. 16 | ''; 17 | }; 18 | }; 19 | 20 | outputs = { 21 | app = mkOption { 22 | type = with types; nullOr package; 23 | default = null; # to allow usage of assertions for errors. 24 | description = '' 25 | The application to run. 26 | ''; 27 | }; 28 | shell = mkOption { 29 | type = with types; nullOr package; 30 | default = null; # to allow usage of assertions for errors. 31 | description = '' 32 | The shell for the development environment. 33 | ''; 34 | }; 35 | }; 36 | }; 37 | 38 | config = { 39 | assertions = [ 40 | { 41 | assertion = config.outputs.app != null; 42 | message = "No application output for the current configuration."; 43 | } 44 | ]; 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /pkgs/ruby/gem-config.nix: -------------------------------------------------------------------------------- 1 | { defaultGemConfig }: 2 | 3 | defaultGemConfig // { 4 | 5 | # Prevent the entire postgresql dependency tree from installing along with this gem 6 | # by stripping files that keep `final.postgresql` refs in the closure. 7 | pg = attrs: (defaultGemConfig.pg attrs) // { 8 | postInstall = '' 9 | find $out/lib/ruby/gems/ -name 'pg-*.info' -delete 10 | ''; 11 | }; 12 | 13 | # Ensure newer verisons of the grpc gem can be built. 14 | # TODO: Correctly apply this at the relevant version and submit to upstream nixpkgs 15 | grpc = attrs: (defaultGemConfig.grpc attrs) // { 16 | postPatch = '' 17 | substituteInPlace Makefile \ 18 | --replace '-Wno-invalid-source-encoding' "" 19 | substituteInPlace src/ruby/ext/grpc/extconf.rb \ 20 | --replace "ENV['AR']" "ENV['NONE']" 21 | substituteInPlace src/ruby/ext/grpc/extconf.rb \ 22 | --replace "ENV['ARFLAGS']" "ENV['NONE']" 23 | ''; 24 | }; 25 | 26 | # Prevent v8 from being installed with execjs, since almost everybody 27 | # uses nodejs with this gem, and v8 does not install correctly on Darwin arm64. 28 | execjs = attrs: (defaultGemConfig.execjs attrs) // { 29 | propagatedBuildInputs = []; 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1.4 2 | FROM alpine 3 | ENV USER root 4 | 5 | RUN apk add rsync curl ca-certificates xz bash 6 | RUN mkdir -m 0755 /nix && chown root /nix 7 | 8 | RUN <> /root/.config/nix/nix.conf 17 | 18 | RUN sh <(curl -L https://nixos.org/nix/install) --no-daemon 19 | RUN ln -s /root/.nix-profile/etc/profile.d/nix.sh /etc/profile.d/nix.sh 20 | 21 | RUN mkdir /source 22 | WORKDIR /source 23 | 24 | RUN ls /nix 25 | RUN cp -pr /nix /nix-backup 26 | COPY . /nix_support 27 | 28 | SHELL ["bash", "-l", "-c"] 29 | 30 | # The following should run at build time on the client 31 | 32 | # Warm up the Nix cache 33 | ONBUILD RUN --mount=type=cache,id=nix-store,target=/nix cp -pr /nix-backup/* /nix 34 | 35 | ONBUILD COPY . . 36 | 37 | ONBUILD ARG FLY_API_TOKEN 38 | ONBUILD ARG TAG 39 | ONBUILD ENV FLY_API_TOKEN=${FLY_API_TOKEN} 40 | ONBUILD ENV TAG=${TAG} 41 | 42 | ONBUILD RUN cp -pr /nix_support .nix 43 | 44 | ONBUILD RUN --mount=type=cache,id=nix-store,target=/nix .nix/bundle 45 | ONBUILD RUN --mount=type=cache,id=nix-store,target=/nix .nix/build 46 | -------------------------------------------------------------------------------- /docker/default.nix: -------------------------------------------------------------------------------- 1 | { nix-base ? (import ./nix-base.nix {}) 2 | }: 3 | let 4 | toml = if builtins.pathExists ../nix.toml then 5 | (builtins.fromTOML (builtins.readFile ../nix.toml)) 6 | else 7 | ""; 8 | in 9 | (nix-base.fly.evalSpec) { 10 | config = { pkgs, ... }: { 11 | templates.rails.enable = true; 12 | app.source = ../.; 13 | runtimes.ruby.version = toml.runtime.ruby_version or "3.1.1"; 14 | runtimes.ruby.withJemalloc = toml.runtime.use_jemalloc or false; 15 | 16 | # Define additional paths required for asset compilation 17 | templates.rails.assetInputs = toml.build.asset_compilation_files or [""]; 18 | 19 | # Define additional files required for bundling gems 20 | templates.rails.gemInputs = toml.build.gem_inputs or [""]; 21 | 22 | # This could also be further abstracted in the nix-base modules by 23 | # providing an option that only takes packages as input, and adds the 24 | # layer. This would provide more isolation from the implementation details. 25 | container.additionalLayers = [ 26 | { 27 | contents = with pkgs; [ ffmpeg ]; 28 | } 29 | ]; 30 | 31 | # Include the base layer which contains useful tools (ca-certs, bash, etc) 32 | container.includeBaseLayer = toml.build.nix_include_tools or false; 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /overlay.nix: -------------------------------------------------------------------------------- 1 | final: super: 2 | 3 | let 4 | inherit (final) callPackage; 5 | inherit (final.lib) genAttrs; 6 | 7 | # 8 | # Simple wrapper around `import` that works like `callPackage`, but does not 9 | # shadow `override`. 10 | # When using `callPackage, `callPackage` adds its own `override` function to 11 | # override the inputs. This can be problematic if the overlaid expression is 12 | # intended to be further manipulated as-is. 13 | # 14 | # Use this when overriding versions in packages from Nixpkgs. 15 | # Do not use for newly added packages or helpers. 16 | # 17 | callPackageNotOverridable = file: args: 18 | let 19 | fn = import file; 20 | fnArgs = builtins.attrNames (builtins.functionArgs fn); 21 | selectedArgs = genAttrs fnArgs (name: final."${name}"); 22 | in 23 | fn selectedArgs 24 | ; 25 | in 26 | { 27 | ruby_2_7_4 = callPackageNotOverridable ./pkgs/ruby/2.7.4.nix { }; 28 | ruby_2_7_5 = callPackageNotOverridable ./pkgs/ruby/2.7.5.nix { }; 29 | ruby_3_1_1 = callPackageNotOverridable ./pkgs/ruby/3.1.1.nix { }; 30 | 31 | defaultGemConfig = callPackage ./pkgs/ruby/gem-config.nix { 32 | defaultGemConfig = super.defaultGemConfig; 33 | }; 34 | 35 | fly = final.lib.makeScope final.newScope (self: { 36 | evalSpec = callPackage ./support/eval-spec.nix { }; 37 | 38 | # Image tools and helpers 39 | baseLayer = callPackage ./support/base-layer.nix { }; 40 | imageTools = callPackage ./support/image-tools.nix { }; 41 | 42 | keepPaths = callPackage ./support/keep-paths.nix { 43 | nix-filter = import (builtins.fetchurl { 44 | url = "https://raw.githubusercontent.com/numtide/nix-filter/e4e8649a3b6f0d3c181955945a84e6918d3f832a/default.nix"; 45 | sha256 = "17s5q9fjwxivr0gjq9bqyias1s38wn78znk3a93q8g05jzgn94wq"; 46 | }); 47 | }; 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /modules/runtimes/ruby/default.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | 3 | let 4 | inherit (lib) 5 | mkDefault 6 | mkForce 7 | mkMerge 8 | mkOption 9 | types 10 | ; 11 | inherit (config.runtimes.ruby) 12 | rubyInterpreters 13 | ; 14 | in 15 | { 16 | options = { 17 | runtimes = { 18 | ruby = { 19 | version = mkOption { 20 | type = types.enum (builtins.attrNames rubyInterpreters); 21 | default = pkgs.ruby.version.majMinTiny; 22 | description = '' 23 | Version of the Ruby interpreter to use. 24 | ''; 25 | }; 26 | package = mkOption { 27 | type = types.package; 28 | internal = true; 29 | description = '' 30 | The selected Ruby package. 31 | 32 | Note that overriding this option with a Ruby package will not 33 | intrinsically configure it with version or features customization. 34 | 35 | When overriden, the overriden version will be used as-is. 36 | ''; 37 | }; 38 | rubyInterpreters = mkOption { 39 | type = with types; attrsOf package; 40 | description = '' 41 | Definition of known Ruby interpreters 42 | ''; 43 | internal = true; 44 | }; 45 | withJemalloc = mkOption { 46 | default = false; 47 | type = types.bool; 48 | description = '' 49 | Whether jemalloc is enabled or not for Ruby. 50 | ''; 51 | }; 52 | }; 53 | }; 54 | }; 55 | 56 | config = { 57 | runtimes.ruby = { 58 | package = mkDefault ( 59 | (rubyInterpreters."${config.runtimes.ruby.version}").override({ 60 | jemallocSupport = config.runtimes.ruby.withJemalloc; 61 | }) 62 | ); 63 | rubyInterpreters = mkMerge [ 64 | # Ruby packages maintained in the Fly overlay 65 | { 66 | "2.7.4" = pkgs.ruby_2_7_4; 67 | "2.7.5" = pkgs.ruby_2_7_5; 68 | "3.1.1" = pkgs.ruby_3_1_1; 69 | } 70 | # Force the default `ruby` packages from Nixpkgs in the attrset. 71 | # It is important for the default `pkgs.ruby.version.majMinTiny` usage 72 | # in the default value for `runtimes.ruby.version`. 73 | { 74 | "${pkgs.ruby_2_7.version.majMinTiny}" = mkForce pkgs.ruby_2_7; 75 | "${pkgs.ruby_3_0.version.majMinTiny}" = mkForce pkgs.ruby_3_0; 76 | "${pkgs.ruby_3_1.version.majMinTiny}" = mkForce pkgs.ruby_3_1; 77 | } 78 | ]; 79 | }; 80 | }; 81 | } 82 | -------------------------------------------------------------------------------- /modules/container.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | 3 | let 4 | inherit (lib) 5 | mkDefault 6 | mkIf 7 | mkOption 8 | types 9 | ; 10 | inherit (config.helpers) applyAssertions; 11 | layerType = with types; submodule { 12 | options = { 13 | contents = mkOption { 14 | type = listOf package; 15 | default = []; 16 | description = '' 17 | Derivations that will be copied in the new layer. 18 | 19 | Content of the derivations will be linked at the root, but content of 20 | transitive dependencies will only be copied to the Nix store path. 21 | ''; 22 | }; 23 | config = mkOption { 24 | type = attrsOf unspecified; 25 | default = {}; 26 | description = '' 27 | `config` is used to specify the configuration of the containers that 28 | will be started off the built image in Docker. The available options 29 | are listed in the [Docker Image Specification v1.2.0](https://github.com/moby/moby/blob/master/image/spec/v1.2.md#image-json-field-descriptions). 30 | ''; 31 | }; 32 | }; 33 | }; 34 | in 35 | { 36 | options = { 37 | container = { 38 | additionalLayers = mkOption { 39 | type = types.listOf layerType; 40 | default = []; 41 | description = '' 42 | List of additional layers to include in the container image. 43 | 44 | They are added between the base runtime layers, and the application-specific layers. 45 | ''; 46 | }; 47 | applicationLayers = mkOption { 48 | type = types.listOf layerType; 49 | default = []; 50 | description = '' 51 | List of application-specific layers to include in the container image. 52 | ''; 53 | }; 54 | includeBaseLayer = mkOption { 55 | type = types.bool; 56 | default = true; 57 | description = '' 58 | Whether to include the base runtime layer or not. 59 | ''; 60 | }; 61 | fromImage = mkOption { 62 | type = with types; nullOr package; 63 | default = null; 64 | internal = true; 65 | description = '' 66 | An image to use as a starting point. 67 | 68 | Left internal; it shouldn't be configured by end-users. 69 | ''; 70 | }; 71 | }; 72 | 73 | outputs = { 74 | container.image = mkOption { 75 | type = types.package; 76 | description = '' 77 | The container image. 78 | ''; 79 | }; 80 | }; 81 | }; 82 | 83 | config = { 84 | container = { 85 | fromImage = mkIf config.container.includeBaseLayer (mkDefault pkgs.fly.baseLayer); 86 | }; 87 | outputs = { 88 | container.image = applyAssertions (pkgs.fly.imageTools.buildSpecifiedLayers { 89 | inherit (config.container) fromImage; 90 | layeredContent = 91 | config.container.additionalLayers ++ 92 | config.container.applicationLayers 93 | ; 94 | }); 95 | }; 96 | }; 97 | } 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | This repo houses [Nix modules](https://nixos.wiki/wiki/Module) for simplifying shared development environments and production deployments based purely on Nix. Today, this repo contains modules targetting Rails applications. The goal is to extend it for other runtimes, frameworks and deployment scenarios. 3 | 4 | Currently, the module system is capable of taking a standard Rails application from development to production using only Nix conventions and a few wrapper shell scripts. A prerequisite for using it in development is having [installed Nix itself](https://nixos.wiki/wiki/Nix_Installation_Guide). You can check out [the sample Rails application](https://github.com/fly-apps/rails-nix) that's setup to use these modules. 5 | 6 | This guide assumes you generally understand what Nix is and why it's useful. We'll be writing about that and 7 | linking to that writing here. 8 | 9 | Note that nothing here is tailored for Fly.io infrastruture, though [flyctl](https://github.com/superfly/flyctl) will integrate it for running builds on remote builder VMs. We encourage comments and collaboration from others interested in Nix-based deployments! 10 | 11 | ## Usage 12 | 13 | For usage instructions, check out the [Rails sample app](https://github.com/fly-apps/rails-nix). 14 | ## Features 15 | 16 | The module system offers these features: 17 | 18 | * A bootstrapped [nix-shell](https://nixos.org/manual/nix/unstable/command-ref/nix-shell.html) for working with the same packages in development as in production 19 | * Configurable Ruby versions baesd on our custom Ruby [Nix overlays](pkgs/ruby) 20 | * Optimized Docker image builds for production deployments 21 | * A separate Docker layer for gems 22 | * A separate Docker layer for Rails asset compilation 23 | * Customizing which file changes trigger asset compilation 24 | * Adding additional packages into their own image layer 25 | * Customizing the Docker image command 26 | 27 | You can see how these options are used in the Rails sample project [default.nix](https://github.com/fly-apps/rails-nix/blob/main/default.nix). 28 | 29 | ## Rails support 30 | 31 | These modules should work with most Rails apps that use `rails asset:precompile` for asset compilation. 32 | 33 | Most pure-Ruby gems should work fine. Most popular Gems requiring binary builds should work, but we don't know if they're all covered here. For special cases we've added a [Nix overlay](pkgs/ruby/gem-config.nix). 34 | ## Caching builds for reuse 35 | 36 | One nix killer feature is the ability to cache any build in a remote store as a simple tarball. We've setup a binary cache on [cachix.org](https://cachix.org) for storing our custom Ruby builds, and perhaps gems and other useful things. 37 | 38 | A starting point for this is in [this Github Actions workflow](.github/workflows/cache.yaml). 39 | ## Developing on nix-base 40 | 41 | If your project is in the same directory as `nix-base`: 42 | 43 | ``` 44 | $ ls 45 | my-project 46 | nix-base 47 | $ cd my-project 48 | $ nix-build --arg fly-base '(import ../nix-base {})' [...] 49 | ``` 50 | ## Future plans 51 | 52 | Things we'd like to do next: 53 | 54 | * Setup build caching CI for more Ruby versions and architectures 55 | * Add more top-level module options for customizing behavior 56 | * Support multiple builds per repo (for example, for running Anycable alongside Rails) 57 | * Move options to a TOML/JSON "app spec" 58 | * Support more asset compilation scenarios (esbuid, webpack, vite, etc) -------------------------------------------------------------------------------- /support/image-tools.nix: -------------------------------------------------------------------------------- 1 | { lib, writeText, closureInfo, dockerTools }: 2 | 3 | let 4 | inherit (lib) 5 | foldl 6 | ; 7 | in 8 | { 9 | buildSpecifiedLayers = 10 | { layeredContent 11 | # An image to start building from. Note that store path deduplication will 12 | # not be attempted when starting from an existing Docker image. Store deduplication 13 | # will happen if using a previous evaluation of buildSpecifiedLayers. 14 | , fromImage ? null 15 | }: 16 | ( 17 | foldl (prev: 18 | # Name of the layer. 19 | { name ? "layer" 20 | # Derivations to directly include in the layer. 21 | # The content of the store path of these derivations (and not their 22 | # dependencies) will be linked at the root of the image. 23 | , contents ? [] 24 | # Commands to run in the context of the layer build. 25 | # The result will not be copied in the Nix store, meaning access rights 26 | # and other properties stripped by the Nix store are kept. 27 | , extraCommands ? "" 28 | , config ? {} 29 | # Any extra given to `buildLayeredImage`. 30 | , ... 31 | }@layerConfig: 32 | let 33 | # Increases the layer count. 34 | maxLayers = prev.maxLayers + 1; 35 | # Used to get the closure of the config (e.g. Config.Cmd or Config.Env) 36 | configJSON = writeText "layer-config.json" ( 37 | builtins.toJSON config 38 | ); 39 | layer = { 40 | layerNumber = prev.layerNumber + 1; 41 | inherit maxLayers; 42 | contents = prev.contents ++ contents; 43 | currentStorePaths = "${closureInfo { rootPaths = contents ++ [ configJSON ]; }}/store-paths"; 44 | previousStorePaths = "${closureInfo { rootPaths = prev.contents; }}/store-paths"; 45 | 46 | image = (dockerTools.buildLayeredImage (layerConfig // { 47 | inherit name; 48 | # Layer on top of the previous step. 49 | fromImage = prev.image; 50 | # We're consuming only one layer per step, but `buildLayeredImage` 51 | # assumes there is at least one layer for store paths, and one 52 | # customization layer. 53 | maxLayers = maxLayers + 1; 54 | # Skip the built-in implementation of the copy operation. 55 | includeStorePaths = false; 56 | # Since we're skipping the built-in implementation, let's add our store paths: 57 | extraCommands = '' 58 | paths() { 59 | # Given: 60 | # - currentStorePaths = [ c d e f ] 61 | # - previousStorePaths = [ a b c e ] 62 | # This returns [ d f ] 63 | # `uniq -u` keeps only unique path entries, and we're duplicating unwanted inputs. 64 | # 65 | # Skip configJSON, which is used only for its transitive dependencies. 66 | ( 67 | cat "${layer.currentStorePaths}" \ 68 | "${layer.previousStorePaths}" \ 69 | "${layer.previousStorePaths}" 70 | 71 | # Skip inclusion of the config file. 72 | echo ${configJSON} 73 | echo ${configJSON} 74 | ) \ 75 | | sort \ 76 | | uniq -u 77 | } 78 | 79 | echo ":: Building layer #${toString layer.layerNumber}" 80 | 81 | mkdir -p ./"$NIX_STORE" 82 | paths | while read path; do 83 | echo " → Copying '$path' in layer" 84 | cp -prf "$path" "./$path" 85 | done 86 | 87 | ${extraCommands} 88 | ''; 89 | })).overrideAttrs({ passthru ? {}, ... }: { 90 | passthru = passthru // { 91 | inherit layer; 92 | }; 93 | }); 94 | }; 95 | in 96 | layer 97 | ) 98 | ( 99 | # Can we continue from the exposed `layer` in `fromImage`? 100 | if fromImage ? layer && fromImage.layer ? layerNumber 101 | then fromImage.layer 102 | else { contents = []; layerNumber = 0; maxLayers = 1; image = fromImage; } 103 | ) 104 | layeredContent 105 | ) 106 | # Export the build artifact (image) from the last step. 107 | .image 108 | ; 109 | } 110 | -------------------------------------------------------------------------------- /modules/templates/rails/default.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | 3 | let 4 | inherit (lib) 5 | mkEnableOption 6 | mkIf 7 | mkOption 8 | types 9 | ; 10 | inherit (config.outputs) app; 11 | in 12 | { 13 | 14 | options = { 15 | templates.rails = { 16 | enable = mkEnableOption "the Rails template"; 17 | assetInputs = mkOption { 18 | type = with types; listOf str; 19 | description = '' 20 | Project-relative paths to be included in the assets pre-compilation step. 21 | 22 | Default includes the minimum needed for a basic Rails app. 23 | ''; 24 | }; 25 | gemInputs = mkOption { 26 | type = with types; listOf str; 27 | # NOTE: if needed for e.g. local gems, add an additional option that 28 | # adds to this internal implementation detail. 29 | internal = true; 30 | description = '' 31 | Project-relative paths to be included for the gems. 32 | 33 | Default includes the minimum assumed required. 34 | ''; 35 | }; 36 | outputs = { 37 | assets = mkOption { 38 | type = types.package; 39 | internal = true; 40 | description = '' 41 | Pre-compiled assets. 42 | ''; 43 | }; 44 | }; 45 | }; 46 | }; 47 | 48 | config = mkIf (config.templates.rails.enable) { 49 | templates.rails.assetInputs = [ 50 | "app/assets" 51 | "app/javascript" 52 | "vendor/javascript" 53 | 54 | # The following components are required for things to work correctly. 55 | "bin/rails" 56 | "config" 57 | "lib/tasks" 58 | "Rakefile" 59 | "config.ru" 60 | ] ++ config.templates.rails.gemInputs; 61 | 62 | templates.rails.gemInputs = [ 63 | "Gemfile" 64 | "Gemfile.lock" 65 | ".nix/gemset.nix" 66 | ]; 67 | 68 | # The assets output is a specialization of the main project build. 69 | templates.rails.outputs.assets = (app.override { 70 | # The source path is reduced to include only files relevant to the 71 | # assets pre-compilation 72 | source = pkgs.fly.keepPaths { 73 | root = config.app.source; 74 | paths = config.templates.rails.assetInputs; 75 | }; 76 | }).overrideAttrs({ installPhase ? "", ... }: { 77 | # The installPhase is modified to only output the assets build. 78 | installPhase = '' 79 | ${installPhase} 80 | 81 | # The main build copied the source to the out path. 82 | # Clean it up 83 | rm -rf $out 84 | 85 | # Needs to be created beforehand 86 | # NOTE: To start from a cached build, copy it here 87 | mkdir -p public/assets 88 | 89 | # Build the assets 90 | bundle exec rails assets:precompile 91 | 92 | mkdir -p $out/public 93 | cp -prf public/assets $out/public/assets 94 | ''; 95 | }); 96 | 97 | outputs = { 98 | app = pkgs.callPackage ( 99 | 100 | { stdenv 101 | , runCommandNoCC 102 | , lib 103 | , ruby 104 | , bundlerEnv 105 | 106 | # Source of the app 107 | , source 108 | # Groups installed in the bundler environment. 109 | , groups ? [ "default" ] 110 | }: 111 | 112 | let 113 | # Cheats and mark unwanted groups optional. 114 | gemfile = 115 | let 116 | # All unique groups referenced in `gemset.nix`; should map to all groups 117 | # actually present in the Gemfile. 118 | allGroups = lib.unique (builtins.concatLists ( 119 | lib.mapAttrsToList 120 | (k: v: v.groups) 121 | (import (source + "/.nix/gemset.nix")) 122 | )); 123 | 124 | # [ "default" "development" "test" ] - [ "default ] ⇒ [ "development" "test" ] 125 | selectedGroups = lib.subtractLists groups allGroups; 126 | in 127 | if groups == null 128 | then source + "/Gemfile" 129 | else (runCommandNoCC "patched-gemfile" {} '' 130 | cat ${source}/Gemfile > $out 131 | cat >> $out <