Skip to content

Automated, modular, idempotent server setup via GitHub Actions for infrastructure tooling.

License

Notifications You must be signed in to change notification settings

christianwhocodes/server-bootstrap

Β 
Β 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

135 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ—οΈ Server Bootstrap

Automated, modular, idempotent server setup via GitHub Actions for infrastructure tooling.

🧭 Overview

The workflow (.github/workflows/server-bootstrap.yaml) connects to your Linux server over SSH using a deployment private key, uploads the bash scripts in scripts/, and runs only the modules selected by a profile or by custom repository variables.

All scripts are idempotent: safe to re-run; they skip work when already satisfied.


πŸš€ Quick Start

  1. Fork this repository to your own GitHub account (required to add Secrets/Variables).
  2. Add repository Secrets: SERVER_SSH_KEY, SUDO_ACCESS_USER.
  3. (Optional) Add Variables: NVM_VERSION and/or any SETUP_* for the Custom profile.
  4. Run the workflow: Actions β†’ Server Bootstrap β†’ choose a profile and inputs.

βœ… Prerequisites

  • Ubuntu server (tested on 24.04)
  • Existing sudo user with passwordless sudo (for system modules)
  • SSH access using the deployment key's public part
  • Forked repository to add Secrets/Variables

πŸ” Required Secrets

Add these in Settings β†’ Secrets and variables β†’ Actions β†’ Secrets tab. Click New repository secret.

SERVER_SSH_KEY

Private SSH key used by the workflow runner to connect to the server. The corresponding public key must be present in ~/.ssh/authorized_keys for SUDO_ACCESS_USER (and any other target you connect as).

Setup:

  • Paste full private key content (no passphrase recommended for automation).
  • Ensure its public key is in ~/.ssh/authorized_keys of the sudo access user.

SUDO_ACCESS_USER

Existing user on the server with passwordless sudo (NOPASSWD:ALL). Used whenever system modules are executed (package installs, service management). Must be reachable via SERVER_SSH_KEY.

Example: ubuntu, root, or a dedicated deploy user with passwordless sudo.


βš™οΈ Optional Variables

Add these in Settings β†’ Secrets and variables β†’ Actions β†’ Variables tab if you need overrides.

Variable Default Description
NVM_VERSION v0.40.4 nvm tag used by install script

Custom Profile Module Variables

Only evaluated when setup_profile == "Custom (use repository variables)". Each must be set to literal string "true" or "false" (GitHub stores them as strings).

System (run via SUDO_ACCESS_USER):

  • SETUP_OPENSSH_UFW
  • SETUP_PACKAGES
  • SETUP_NGINX
  • SETUP_CERTBOT
  • SETUP_POSTGRES

User (run as target_user):

  • SETUP_UV
  • SETUP_NVM
  • SETUP_REPOS_DIR
  • SETUP_GIT_SSH

πŸŽ›οΈ Setup Profiles

Select one profile when dispatching the workflow:

1. Full Development Server

System modules: OpenSSH/UFW, Packages, Nginx, Certbot, PostgreSQL
User modules: uv, nvm, repos directory, Git + SSH key

Use case: Complete server bootstrap with all tools

2. System Services Only

System modules only (OpenSSH/UFW, Packages, Nginx, Certbot, PostgreSQL)

Use case: Infrastructure setup without user development tools

3. User Tools Only

User modules only (uv, nvm, repos directory, Git + SSH key)

Use case: Add dev tools to existing user without system changes

4. Custom (use repository variables)

Boolean repository variables (SETUP_*) decide which modules run.

Use case: Fine-grained control over individual modules


🧩 Workflow Inputs

When dispatching the workflow, you'll be prompted for these inputs:

Input Required Default Description
server_host Yes - Server IP or hostname
server_port No 22 SSH port
target_user Yes - The Linux user to configure on the server. This is the user who will have dev tools installed (uv, nvm, repos, Git/SSH). If the user doesn't exist, it will be created automatically. User modules run as this user.
make_user_sudo No false Gives target user passwordless sudo if true
ssh_public_key No - Added to target user's authorized_keys (optional)
setup_profile Yes - One of the four profiles
git_user_name No - Required if Git/SSH module runs
git_user_email No - Required if Git/SSH module runs
dry_run No false Dry run mode - simulates execution without making actual changes. Useful for validating configuration and logic before affecting the server.

Note: There is no "create user" toggleβ€”creation is automatic if the user does not exist.


🧱 Available Modules

System Modules (require sudo)

πŸ”’ OpenSSH/UFW

Script: openssh-ufw.sh
What it does:

  • Adds OpenSSH allow rule if absent
  • Only enables UFW after confirming the rule, mitigating lockout risk
  • Skips enable if rule confirmation fails

πŸ› οΈ Packages

Script: packages.sh
What it does:

  • Installs build/development libraries: git, curl, toolchains, SQLite, Pandoc, TeX, snapd, etc.

🌐 Nginx

Script: nginx.sh
What it does:

  • Installs and starts Nginx service
  • Creates /etc/nginx/snippets/port-proxy.conf (expects $upstream_port variable)
  • Does NOT create server blocksβ€”you must add your own site config

Example server block:

server {
  listen 80;
  server_name your.domain;
  set $upstream_port 8080;  # match your application port
  include /etc/nginx/snippets/port-proxy.conf;
}

Then run: sudo nginx -t && sudo systemctl reload nginx

πŸ” Certbot

Script: certbot.sh
What it does:

  • Installs Certbot via snap

Manual follow-up: Run sudo certbot certonly --nginx -d your.domain after setting up domain + server block.

🐘 PostgreSQL

Script: postgres.sh
What it does:

  • Installs PostgreSQL server + contrib
  • Enables and starts service
  • Does NOT create databases or rolesβ€”manual configuration required

Manual follow-up: Create roles/databases with sudo -iu postgres psql

User Modules (non-sudo)

🐍 uv

Script: uv.sh
What it does:

  • Installs uv Python package manager
  • If Python missing, installs via uv python install

πŸ“— nvm

Script: nvm.sh
What it does:

  • Installs specified nvm version
  • Installs latest Node.js
  • Updates npm

πŸ“ Repos Directory

Script: repos.sh
What it does:

  • Ensures ~/repos directory exists

πŸ”‘ Git/SSH

Script: git-ssh.sh
What it does:

  • Sets global git config (overwrites previous name/email values)
  • Generates ~/.ssh/id_ed25519 key pair if absent
  • Public key surfaced in workflow summary for adding to hosting platforms

Requirements: git_user_name and git_user_email must be provided in workflow inputs.


πŸ“œ Module Summary Table

Variable Script(s) Notes
SETUP_OPENSSH_UFW openssh-ufw.sh Adds OpenSSH allow rule; enables UFW only if rule confirmed
SETUP_PACKAGES packages.sh Development toolchain & libs including snapd
SETUP_NGINX nginx.sh Installs, enables service; creates proxy snippet only
SETUP_CERTBOT certbot.sh Installs via snap (snapd provided by packages.sh)
SETUP_POSTGRES postgres.sh Enables & starts service
SETUP_UV uv.sh Installs uv + Python if missing
SETUP_NVM nvm.sh Installs Node + updates npm
SETUP_REPOS_DIR repos.sh Ensures ~/repos
SETUP_GIT_SSH git-ssh.sh Needs workflow inputs git_user_name/email

πŸ”„ How It Works (Execution Flow)

  1. Repository checkout - Scripts become available locally on runner
  2. Module determination - Based on profile or custom variables
  3. Input validation - Checks Git info if needed; verifies SUDO_ACCESS_USER for system modules
  4. SSH key provisioning - Sets up runner authentication
  5. SSH connectivity test - As sudo access user or target user depending on module mix
  6. Target user creation/update - Automatically creates user if missing
  7. Script upload - Via scp to /tmp/ on remote server
  8. Module execution - Conditionally runs each selected module
  9. Credential retrieval - Fetches SSH public key if Git/SSH module ran
  10. Summary output - Displays configuration details
  11. Cleanup - Removes remote scripts and local SSH key

♻️ Idempotency

All scripts are designed to be safe to re-run:

  • Variables only control whether a script runs - Each script internally checks state
  • Binary presence - Skips installation if already exists
  • Service state - Only starts/enables if needed
  • Directory/key existence - Creates only if absent

Re-running with "true" does not break prior setup. Safe to add modules later by re-running workflow with Custom profile and enabling only new modules.

Idempotency Examples

  • Add PostgreSQL later: Set SETUP_POSTGRES=true (custom profile) β†’ Only PostgreSQL script runs
  • Profile switch: Include previously omitted modules β†’ Already installed modules detected and skipped

οΏ½ Dry Run Mode

The workflow includes a dry run mode that simulates execution without making any actual changes to the server. This is useful for:

  • Validation: Verify configuration before running against production/development servers
  • Debugging: Test workflow logic without affecting server state
  • Documentation: Generate execution plans for review
  • Training: Understand what the workflow will do without risk

How to Use Dry Run

Set the dry_run input to true when dispatching the workflow. The workflow will:

  • βœ… Connect to the server via SSH
  • βœ… Check current state (users, installed packages, etc.)
  • βœ… Display what actions would be taken
  • ❌ Not create users, install packages, or modify any files
  • ❌ Not enable services or change configurations

All steps will output [DRY RUN] Would... messages instead of performing actual operations.

Example Output

[DRY RUN] Would create user: developer
[DRY RUN] Would install necessary packages
[DRY RUN] Would setup Nginx
[DRY RUN] Would setup uv

οΏ½πŸ“š Common Use Cases

1. New Full Development Server

Profile: Full Development Server
Inputs: Provide git_user_name/email
Outcome: Complete system + user environment with all tools

2. Infrastructure Only

Profile: System Services Only
Outcome: Services installed; no user toolchain

3. Add Dev Tools to Existing User

Profile: User Tools Only
Outcome: uv, nvm, repos, Git/SSH installed; no system changes

4. Incremental Module Addition

Profile: Custom
Variables: Set specific SETUP_* variables (e.g., SETUP_POSTGRES=true)
Outcome: Only specified modules run; existing setup untouched

5. Add Just nvm Later

Profile: Custom
Variables: SETUP_NVM=true
Outcome: nvm added; prior modules untouched


πŸ§ͺ Example Custom Configuration

Enable only Nginx + nvm:

SETUP_NGINX=true
SETUP_NVM=true

Then dispatch workflow with profile: Custom (use repository variables).


πŸ”‘ Credential Retrieval

After successful setup, if Git/SSH module runs, the workflow fetches:

  • SSH public key (id_ed25519.pub) displayed in workflow summary for adding to hosting services (GitHub, GitLab, etc.)

The private key remains securely on the server at ~/.ssh/id_ed25519.


πŸ“ When Git Inputs Are Required

git_user_name and git_user_email must be provided if:

  • Profile includes Git/SSH module (Full Development Server, User Tools Only)
  • Custom profile sets SETUP_GIT_SSH=true

πŸ§ͺ Troubleshooting

Common Issues

Issue Solution
SSH connection fails Verify host, port, key format (ssh -i key user@host), firewall rule, server reachable
Missing SUDO_ACCESS_USER error Add secret; verify passwordless sudo exists
Certbot failure (snap not found) This shouldn't happen anymoreβ€”packages.sh now installs snapd
Git module error (missing name/email) Provide both inputs when profile includes Git/SSH
UFW enable skipped Ensure OpenSSH rule added; rerun script

Getting Help

Open an issue with:

  • Workflow run URL
  • Relevant log excerpts
  • Server OS/version
  • Module(s) failing
  • Steps attempted

πŸ“Œ Manual Follow-ups After Run

  • Nginx: Create server block referencing snippets
  • Certbot: Run sudo certbot certonly --nginx -d your.domain (after domain + server block configured)
  • SSH Key: Add retrieved public key to Git hosting provider
  • PostgreSQL: Create roles/databases (sudo -iu postgres psql)
  • Security: Rotate generated SSH keys as needed

πŸ›‘οΈ Security Considerations

  • Passwordless sudo is restricted to SUDO_ACCESS_USER and optionally target_user if make_user_sudo=true
  • Deployment SSH key should be dedicated; rotate periodically
  • Generated Git SSH key remains on server (private part not exfiltrated)
  • Limit the number of users with passwordless sudo
  • Audit ~/.ssh/authorized_keys regularly
  • Review workflow run logs for unexpected warnings
  • Limit SUDO_ACCESS_USER privileges to what is necessary
  • Rotate SERVER_SSH_KEY regularly

🧱 Extending the Workflow

To add new scripts:

  1. Create script under scripts/ following existing pattern:
    • Check existence first
    • Exit early on success
    • Echo status clearly
  2. Add corresponding variable in workflow logic (e.g., SETUP_YOUR_MODULE)
  3. Add step in workflow YAML with conditional execution

πŸ—‚οΈ Repository Structure

.github/
  workflows/
    server-bootstrap.yaml
scripts/
  certbot.sh
  git-ssh.sh
  nginx.sh
  nvm.sh
  openssh-ufw.sh
  packages.sh
  postgres.sh
  repos.sh
  uv.sh
LICENSE
README.md
TODO.md

πŸ“ Reference: Scripts

Located in scripts/:

System (require sudo):

  • openssh-ufw.sh - Firewall configuration
  • packages.sh - Development packages + snapd
  • nginx.sh - Web server
  • certbot.sh - SSL certificates
  • postgres.sh - Database server

User (non-sudo):

  • uv.sh - Python package manager
  • nvm.sh - Node version manager
  • repos.sh - Repos directory
  • git-ssh.sh - Git config + SSH key generation

πŸ“„ License

MIT

About

Automated, modular, idempotent server setup via GitHub Actions for infrastructure tooling.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Shell 100.0%