commit 8f23618b47df8779f5e75e3a749d6049bed4fad1 Author: m3tam3re Date: Tue Feb 18 08:50:17 2025 +0100 +flake template diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..0b0ba0b --- /dev/null +++ b/flake.nix @@ -0,0 +1,29 @@ +{ + description = "Self Host Playbook!"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = {nixpkgs, ...}: let + systems = [ + "aarch64-linux" + "i686-linux" + "x86_64-linux" + "aarch64-darwin" + "x86_64-darwin" + ]; + forAllSystems = nixpkgs.lib.genAttrs systems; + in { + templates = { + starter = { + description = '' + Description here! + ''; + path = ./starter; + }; + }; + formatter = + forAllSystems (system: nixpkgs.legacyPackages.${system}.alejandra); + }; +} diff --git a/starter/README.md b/starter/README.md new file mode 100644 index 0000000..e69de29 diff --git a/starter/config.json b/starter/config.json new file mode 100644 index 0000000..e69de29 diff --git a/starter/configuration.nix b/starter/configuration.nix new file mode 100644 index 0000000..8e089d9 --- /dev/null +++ b/starter/configuration.nix @@ -0,0 +1,184 @@ +{ + lib, + pkgs, + ... +}: +# Read configuration from JSON +let + jsonConfig = builtins.fromJSON (builtins.readFile ./config.json); +in { + imports = [ + ./disko-config.nix + ./hardware-configuration.nix + ]; + + # Enable flakes and nix commands + nix = { + settings = { + experimental-features = ["nix-command" "flakes"]; + # Enable automatic garbage collection + auto-optimise-store = true; + trusted-users = [jsonConfig.username]; + }; + # Automatic cleanup of old generations + gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 30d"; + }; + }; + + # Boot configuration + boot.loader.grub = { + enable = true; + devices = ["/dev/sda"]; + efiSupport = true; + efiInstallAsRemovable = true; + }; + + # Networking + networking = { + firewall = { + enable = true; + # Only allow necessary ports + allowedTCPPorts = [80 443 2222]; # HTTP, HTTPS, and SSH + }; + }; + + environment.etc = { + environment-files = { + source = pkgs.copyPathToStore ./env; + }; + }; + + # System packages + environment.systemPackages = with pkgs; [ + # System utilities + neovim + git + # Docker tools + docker + docker-compose + ]; + + # Enable Docker with recommended settings + virtualisation.docker = { + enable = true; + # Enable docker daemon to start on boot + enableOnBoot = true; + # Use overlay2 storage driver + storageDriver = "overlay2"; + # Enable live restore + liveRestore = true; + }; + + # Services configuration + services = { + # SSH server configuration + openssh = { + enable = true; + settings = { + PermitRootLogin = "no"; + PasswordAuthentication = false; + # Additional security settings + MaxAuthTries = 3; + LoginGraceTime = "30s"; + }; + ports = [2222]; + }; + + # Caddy configuration with security headers + caddy = { + enable = true; + virtualHosts = { + "${jsonConfig.domains.portainer}" = { + extraConfig = '' + reverse_proxy localhost:9000 + header { + # Security headers + Strict-Transport-Security "max-age=31536000; includeSubDomains" + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + Referrer-Policy "strict-origin-when-cross-origin" + } + ''; + }; + "${jsonConfig.domains.n8n}" = { + extraConfig = '' + reverse_proxy localhost:5678 + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains" + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + Referrer-Policy "strict-origin-when-cross-origin" + } + ''; + }; + "${jsonConfig.domains.baserow}" = { + extraConfig = '' + reverse_proxy localhost:3000 + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains" + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + Referrer-Policy "strict-origin-when-cross-origin" + } + ''; + }; + }; + }; + }; + # User configuration + users.users.${jsonConfig.username} = { + isNormalUser = true; + extraGroups = ["wheel" "docker"]; + hashedPassword = jsonConfig.hashedPassword; + openssh.authorizedKeys.keys = [jsonConfig.sshKey]; + # Set default shell to bash + shell = pkgs.bash; + }; + + # Container configurations + virtualisation.oci-containers = { + backend = "docker"; + containers = { + "portainer" = { + image = "docker.io/portainer/portainer-ce:latest"; + ports = ["127.0.0.1:9000:9000"]; + volumes = [ + "/etc/localtime:/etc/localtime:ro" + "/var/run/docker.sock:/var/run/docker.sock:ro" + "portainer_data:/data" + ]; + extraOptions = [ + "--network=web" + ]; + }; + + "n8n" = { + image = "docker.io/n8nio/n8n:latest"; + environmentFiles = ["/etc/environment-files/n8n.env"]; + ports = ["127.0.0.1:5678:5678"]; + volumes = ["n8n_data:/home/node/.n8n"]; + extraOptions = ["--network=web"]; + }; + + "baserow" = { + image = "docker.io/baserow/baserow:1.30.1"; + environmentFiles = ["/etc/environment-files/baserow.env"]; + ports = ["127.0.0.1:3000:80"]; + volumes = ["baserow_data:/baserow/data"]; + extraOptions = ["--network=web"]; + }; + }; + }; + + system.activationScripts.createDockerNetworkWeb = lib.mkAfter '' + if ! /run/current-system/sw/bin/docker network ls | /run/current-system/sw/bin/awk '{print $2}' | /run/current-system/sw/bin/grep -q 'web'; then + /run/current-system/sw/bin/docker network create web + fi + ''; + + # System state version (do not change) + system.stateVersion = "24.11"; +} diff --git a/starter/disko-config.nix b/starter/disko-config.nix new file mode 100644 index 0000000..ed20592 --- /dev/null +++ b/starter/disko-config.nix @@ -0,0 +1,41 @@ +let + jsonConfig = builtins.fromJSON (builtins.readFile ./config.json); +in { + disko.devices = { + disk = { + main = { + type = "disk"; + device = jsonConfig.rootDevice; + content = { + type = "gpt"; + partitions = { + bios = { + size = "1M"; + type = "EF02"; # for GRUB BIOS boot + priority = 1; + }; + esp = { + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = ["defaults" "umask=0077"]; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + mountOptions = ["noatime" "nodiratime" "discard"]; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/starter/env/baserow.env b/starter/env/baserow.env new file mode 100644 index 0000000..80ba6ae --- /dev/null +++ b/starter/env/baserow.env @@ -0,0 +1 @@ +BASEROW_PUBLIC_URL=BASEROW_DOMAIN diff --git a/starter/env/n8n.env b/starter/env/n8n.env new file mode 100644 index 0000000..c66516e --- /dev/null +++ b/starter/env/n8n.env @@ -0,0 +1,3 @@ +N8N_HOST=N8N_DOMAIN +NODE_ENV=production +N8N_ENCRYPTION_KEY=changeme diff --git a/starter/flake.lock b/starter/flake.lock new file mode 100644 index 0000000..742661d --- /dev/null +++ b/starter/flake.lock @@ -0,0 +1,48 @@ +{ + "nodes": { + "disko": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1739760714, + "narHash": "sha256-SaGuzIQUTC2UPwX0aTm9W4aSEUcq3h6yZbi30piej2U=", + "owner": "nix-community", + "repo": "disko", + "rev": "be1e4321c9fb3a4bc2c061dafcdb424937a74dad", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "disko", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1739580444, + "narHash": "sha256-+/bSz4EAVbqz8/HsIGLroF8aNaO8bLRL7WfACN+24g4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "8bb37161a0488b89830168b81c48aed11569cb93", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "disko": "disko", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/starter/flake.nix b/starter/flake.nix new file mode 100644 index 0000000..304c4cf --- /dev/null +++ b/starter/flake.nix @@ -0,0 +1,25 @@ +{ + description = "Self-hosted server setup with Portainer, n8n, and Baserow"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + disko = { + url = "github:nix-community/disko"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { + self, + nixpkgs, + ... + } @ inputs: { + nixosConfigurations.server = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + inputs.disko.nixosModules.disko + ./configuration.nix + ]; + }; + }; +} diff --git a/starter/hardware-configuration.nix b/starter/hardware-configuration.nix new file mode 100644 index 0000000..4daad50 --- /dev/null +++ b/starter/hardware-configuration.nix @@ -0,0 +1,26 @@ +# Do not modify this file! It was generated by β€˜nixos-generate-config’ +# and may be overwritten by future invocations. Please make changes +# to /etc/nixos/configuration.nix instead. +{ + lib, + modulesPath, + ... +}: { + imports = [ + (modulesPath + "/profiles/qemu-guest.nix") + ]; + + boot.initrd.availableKernelModules = ["ata_piix" "uhci_hcd" "virtio_pci" "virtio_scsi" "sd_mod" "sr_mod"]; + boot.initrd.kernelModules = []; + boot.kernelModules = []; + boot.extraModulePackages = []; + + # Enables DHCP on each ethernet and wireless interface. In case of scripted networking + # (the default) this is the recommended approach. When using systemd-networkd it's + # still possible to use this option, but it's recommended to use it in conjunction + # with explicit per-interface declarations with `networking.interfaces..useDHCP`. + networking.useDHCP = lib.mkDefault true; + # networking.interfaces.ens18.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; +} diff --git a/starter/install.sh b/starter/install.sh new file mode 100755 index 0000000..212bca8 --- /dev/null +++ b/starter/install.sh @@ -0,0 +1,257 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Ensure we're in a flakes-enabled environment with required tools +if ! command -v nix &> /dev/null; then + echo "❌ Nix is not installed. Please install Nix first:" + echo "curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install" + exit 1 +fi + +# Check if we need to enter a new shell +if [[ ! -v INSIDE_NIX_SHELL ]]; then + export NIX_CONFIG="experimental-features = nix-command flakes" + export INSIDE_NIX_SHELL=1 + exec nix shell nixpkgs#git nixpkgs#mkpasswd --command bash "$0" +fi + +# Function to setup from template +setup_from_template() { + local TEMPLATE=starter + local DIR_NAME="self-host-playbook" + + if [ -d "$DIR_NAME" ]; then + echo "πŸ“‚ Directory '$DIR_NAME' already exists" + read -p "Do you want to proceed in the existing directory? (y/N) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + cd "$DIR_NAME" + else + echo "❌ Please choose a different directory or remove the existing one" + exit 1 + fi + else + echo "πŸ”„ Creating new self-host-playbook configuration from template..." + nix flake new --template "git+https://code.m3tam3re.com/m3tam3re/self-host-playbook#${TEMPLATE}" "$DIR_NAME" + cd "$DIR_NAME" + fi +} + +# Function to generate SSH key +generate_ssh_key() { + local KEY_NAME="self-host-playbook" + local KEY_PATH="$HOME/.ssh/${KEY_NAME}" + + if [ ! -f "$KEY_PATH" ]; then + mkdir -p "$HOME/.ssh" + echo "πŸ”‘ Generating new SSH key pair..." >&2 + ssh-keygen -t ed25519 -f "$KEY_PATH" -N "" >&2 + echo "βœ… New SSH key pair generated" >&2 + else + echo "πŸ”‘ Using existing SSH key pair" >&2 + fi + + if [ ! -f "${KEY_PATH}.pub" ]; then + echo "❌ Error: Public key file not found" >&2 + exit 1 + fi + + printf "%s" "$KEY_PATH" +} + +echo "πŸš€ Welcome to the Self-Host Playbook!" +echo "================================================" +echo "This script will help you manage your NixOS server with:" +echo "- Portainer (Docker management)" +echo "- n8n (Workflow automation)" +echo "- Baserow (No-code database)" +echo "- Caddy (Automatic HTTPS reverse proxy)" +echo "================================================" +echo + +# Function to hash password using mkpasswd +hash_password() { + local password=$1 + mkpasswd -m sha-512 "$password" +} + +# Function to display device detection guide +show_device_guide() { + echo + echo "πŸ“ How to find your root device name:" + echo "------------------------------------" + echo "1. SSH into your server with the credentials provided by your cloud provider or use the web console" + echo "2. Run the following command:" + echo " lsblk -d -o NAME,SIZE" + echo + echo "Look for the main disk, usually the largest one. It will be shown as:" + echo "- AWS (older): xvda" + echo "- AWS (newer): nvme0n1" + echo "- GCP/Azure/Linode: sda" + echo "- DigitalOcean: vda" + echo + echo "The device name will be /dev/[name shown in lsblk]" + echo + echo "πŸ’‘ Example:" + echo " NAME SIZE" + echo " sda 76.3G" + echo " sr0 1024M" + echo "------------------------------------" + echo +} + +# Function to get device name based on provider +get_device_name() { + local provider=$1 + case $provider in + "aws-new") + echo "/dev/nvme0n1" + ;; + "aws-old") + echo "/dev/xvda" + ;; + "gcp") + echo "/dev/sda" + ;; + "azure") + echo "/dev/sda" + ;; + "digitalocean") + echo "/dev/vda" + ;; + "linode") + echo "/dev/sda" + ;; + "hetzner") + echo "/dev/sda" + ;; + *) + echo "unknown" + ;; + esac +} + +# Collect user input +echo "πŸ“ Please provide the following information:" +echo "-------------------------------------------" +read -p "1. Enter target server IP address: " SERVER_IP +read -p "2. Enter desired username for server access: " USERNAME +read -s -p "3. Enter desired password: " PASSWORD +echo +echo "4. Enter domain names for services (must point to $SERVER_IP):" +read -p " - Domain for Portainer: " PORTAINER_DOMAIN +read -p " - Domain for n8n: " N8N_DOMAIN +read -p " - Domain for Baserow: " BASEROW_DOMAIN + +echo +echo "5. Select your cloud provider:" +echo " 1) AWS (Newer instances with NVMe)" +echo " 2) AWS (Older instances)" +echo " 3) Google Cloud Platform" +echo " 4) Microsoft Azure" +echo " 5) DigitalOcean" +echo " 6) Linode" +echo " 7) Hetzner" +echo " 8) I'm not sure (Show detection guide)" +echo + +read -p "Enter your choice (1-8): " PROVIDER_CHOICE + +# Set device name based on provider choice +case $PROVIDER_CHOICE in + 1) + PROVIDER="aws-new" + ;; + 2) + PROVIDER="aws-old" + ;; + 3) + PROVIDER="gcp" + ;; + 4) + PROVIDER="azure" + ;; + 5) + PROVIDER="digitalocean" + ;; + 6) + PROVIDER="linode" + ;; + 7) + PROVIDER="hetzner" + ;; + 8) + show_device_guide + read -p "Enter your root device name (e.g., /dev/sda): " DEVICE_NAME + PROVIDER="custom" + ;; + *) + echo "❌ Invalid choice" + exit 1 + ;; +esac + +if [ "$PROVIDER" != "custom" ]; then + DEVICE_NAME=$(get_device_name "$PROVIDER") +fi + +echo +echo "Using root device: $DEVICE_NAME" +read -p "Is this correct? (y/N) " CONFIRM +if [[ ! $CONFIRM =~ ^[Yy]$ ]]; then + echo "Installation aborted. Please run the script again with the correct device." + exit 1 +fi + +echo "πŸ› οΈ Preparing server configuration..." + +# Generate SSH key +SSH_KEY_PATH=$(generate_ssh_key) || exit 1 +SSH_PUB_KEY=$(cat "${SSH_KEY_PATH}.pub") || { + echo "❌ Error: Failed to read public key from ${SSH_KEY_PATH}.pub" + exit 1 +} + +# Hash the password +HASHED_PASSWORD=$(hash_password "$PASSWORD") + +echo "πŸ“ Customizing configuration files..." +# Write configuration to JSON file +cat > config.json << EOF +{ + "username": "$USERNAME", + "hashedPassword": "$HASHED_PASSWORD", + "sshKey": "$SSH_PUB_KEY", + "domains": { + "portainer": "$PORTAINER_DOMAIN", + "n8n": "$N8N_DOMAIN", + "baserow": "$BASEROW_DOMAIN" + }, + "rootDevice": "$DEVICE_NAME" +} +EOF + +echo "πŸ“¦ Setting up environment files..." +# Update environment files with domains +sed -i "s/N8N_DOMAIN/$N8N_DOMAIN/g" ./env/n8n.env +sed -i "s/BASEROW_DOMAIN/$BASEROW_DOMAIN/g" ./env/baserow.env + +echo "πŸš€ Starting NixOS installation..." +echo "This process might take several minutes..." +# Run nixos-anywhere installation +nix run github:nix-community/nixos-anywhere -- --flake .#server root@$SERVER_IP + +echo +echo "πŸŽ‰ Installation completed successfully!" +echo "=====================================>" +echo "You can now access your services at:" +echo "- Portainer: https://$PORTAINER_DOMAIN" +echo "- n8n: https://$N8N_DOMAIN" +echo "- Baserow: https://$BASEROW_DOMAIN" +echo +echo "To connect to your server, use:" +echo "ssh -i $SSH_KEY_PATH -p 2222 $USERNAME@$SERVER_IP" +echo +echo "⚠️ Important: Please save your SSH key path: $SSH_KEY_PATH" +echo "=====================================>"