← 输入框|github.com|刷新
Skip to content
contraptioncoPublic

About

Simple automation for a Mac-based home web server

Resources

Stars

38 stars

Watchers

0 watching

Forks

Repository files navigation

A lightweight framework for running multiple web apps on a Mac Mini (or, any Mac). This is the actual script that powers www.contraption.co and other web applications from a single Mac Mini home server. Learn more about the story in "A mini data center".

If you encounter any problems, please open an issue.

What is this?

Toolbox is a collection of Ruby scripts that orchestrate Docker containers, Git repositories, and system services to create a robust home server environment. It's designed to be simple, maintainable, and reliable - perfect for hosting personal web apps from home without complex infrastructure or monthly cloud costs.

For full setup instructions, read the post: How to host web apps on a Mac Mini

Philosophy

This toolbox is built with the following principles in mind:

  • Simplicity over complexity: Scripts are readable and maintainable
  • Docker-based: Each service runs in its own container
  • Git-powered: Auto-deployment from repositories
  • Configuration over code: Centralized configuration in one file
  • Secure access: Cloudflare Tunnel eliminates the need to open ports
  • Secret management: 1Password integration keeps credentials secure
  • Legibility: No frameworks, minimal learning curve, and mostly raw Docker

Features

  • Automatic service management: Start/restart services on boot
  • Cloudflare Tunnel integration: Expose services to the internet securely
  • Git repository management: Auto-update when changes are detected
  • Docker container orchestration: Simplified management of containerized apps
  • One-password integration: Secure credential management
  • Heartbeat monitoring: Optional uptime monitoring
  • Lock screen on boot: Secure your server with automatic screen locking after reboot
  • Complex Git Service Management: Supports services requiring initial setup scripts and managed via their own Docker Compose.
  • Private dashboard: Native host metrics, deployment status, Docker inventory, container metrics and redacted live logs behind Cloudflare Access.

Dashboard

toolbox.contraption.co is a read-only view of the Mac mini. It shows disk capacity (including the distinction between immediately free and reclaimable space), memory, load, uptime, heartbeat status, configured applications, and running or stopped Docker containers. Unexpected running containers are flagged. Expand a container for CPU, memory, network, block I/O, restart count and recent logs.

The web service runs natively in Ruby on 127.0.0.1:4178; running it inside Docker would report Docker's Linux VM as the host. Toolbox manages its separate co.contraption.toolbox.dashboard LaunchAgent and installs the locked dashboard gems under .runtime/dashboard/gems. Application containers and the shared PostgreSQL database are not dependencies of the UI. Netdata and its scheduled log rotation are retired; the dashboard reads host metrics directly. Existing Netdata files and metric history are preserved.

Cloudflare Access is the only login. The existing Toolbox UI application uses the same private allow policy as Junk Drawer. The tunnel connector and Ruby origin both verify the Access application token. There are no mutation or command endpoints. /healthz contains only readiness and the loaded revision; other routes require a valid token. Host logs are bounded and conservatively redacted, but may still contain sensitive application output and remain private.

On an iPhone, open the dashboard in Safari, sign in through Cloudflare, then use Share → Add to Home Screen. The web app has its own icons and standalone display. Offline mode shows a reconnect message; metrics, logs and authentication responses are never saved for offline use. Current metrics refresh every ten seconds and logs every five seconds while visible. History is not retained.

Develop and verify the dashboard

Use a separate development clone as described in AGENTS.md:

bundle config set --local path .runtime/bundle
bundle install
ruby web/server.rb --demo

Open http://127.0.0.1:4179 for synthetic host/container/log fixtures. Demo mode cannot read production metrics. Unit tests use fake command runners, temporary files and generated signing keys, so they do not need Docker or Cloudflare:

ruby -Itest -e 'Dir["test/**/*_test.rb"].sort.each { |file| require_relative file }'
zsh -n heartbeat.sh
node --check web/public/app.js
node --check web/public/sw.js

For an authorized rollout, merge a verified PR and let the heartbeat update production. Check the native origin's /healthz, verify anonymous public requests reach the Access login, then exercise the authenticated UI and API. This tunnel's remote configuration overrides config.yml: fetch and edit only the intended remote ingress rule through Cloudflare MCP, preserving unrelated routes and the Access policy. Do not expose the origin before Access is confirmed.

Requirements

For full setup instructions, read: How to host web apps on a Mac Mini

Before you start, ensure you have:

Quick start

  1. Fork this repository
  2. Clone your fork to your Mac
  3. Configure your Cloudflare tunnel in config.yml
  4. Edit config.rb to match your services and domains
  5. Install Ruby: asdf install
  6. Commit and push your configuration to your fork's main, then update the production checkout and run: ruby toolbox.rb --production
  7. Set up the heartbeat script for auto-updates

Setting up 1Password CLI

The toolbox relies on 1Password CLI for secure credential storage:

  1. Install 1Password CLI:

    brew install 1password-cli
    
  2. Security Recommendation: Create a dedicated vault in 1Password specifically for your server credentials. This limits the scope of access to only the secrets needed for this toolbox, keeping your personal secrets separate.

  3. Choose an authentication method:

    For manual use:

    • Sign in to your 1Password account interactively:
      eval $(op signin)
      

    For automated use (recommended for the heartbeat service):

    • Create a service account token:
      • Go to 1Password account settings → Developer → Service Accounts
      • Create a new service account token with access only to your dedicated server vault
      • Add the token to the LaunchAgent plist file (no interactive login required)

For detailed setup instructions, follow the official 1Password CLI guide.

Setting up Cloudflare Tunnel

Cloudflare Tunnel provides a secure way to expose your home server to the internet without opening ports. For a detailed guide on setting up Cloudflare Tunnel on a Mac, follow Sam Rhea's excellent guide.

To set up Cloudflare Tunnel for this toolbox:

  1. Create a Cloudflare account and add your domain
  2. Install cloudflared: brew install cloudflared
  3. Authenticate with Cloudflare: cloudflared tunnel login
  4. Create a tunnel: cloudflared tunnel create toolbox
  5. Create a config.yml file in your toolbox directory with your host-to-port mappings:
    tunnel: YOUR_TUNNEL_ID
    credentials-file: /path/to/credentials/file.json
    
    ingress:
      - hostname: yourapp.yourdomain.com
        service: http://localhost:8000
      - hostname: anotherapp.yourdomain.com
        service: http://localhost:4000
      - service: http_status:404
  6. Update the TUNNEL_CONFIG section in config.rb to reference your config.yml file

The toolbox will manage starting and running your Cloudflare Tunnel using this configuration.

Configuring your services

  1. Edit config.rb to match your services and domains
  2. Configure your Docker services, Git repositories, and system services in the corresponding sections
  3. Update the Cloudflare Tunnel configuration to reference your config.yml file
  4. Each service requires its own configuration

Here's an example of how services are defined in config.rb:

DOCKER_SERVICES = [
  {
    name: 'ghost',
    image: 'ghost:5.109.2',
    ports: ['2368:2368'],
    volumes: ["#{DATA_DIR}/ghost:/var/lib/ghost/content"],
    environment: {
      url: 'https://your-domain.com',
      # Other environment variables...
    }
  }
]

Here's an example of a service cloned from Git that runs its own install script and is managed by Docker Compose within its directory:

GIT_SERVICES = [
  {
    name: 'sentry',                                         # Service name
    repo_url: 'https://github.com/getsentry/self-hosted.git', # Sentry repo URL
    local_path: "#{Config::CODE_DIR}/sentry-self-hosted",   # Local path for the repo
    # Command to run ONCE after initial clone.
    install_cmd: './install.sh --no-report-self-hosted-issues',
    # Indicates this service uses docker-compose in its directory
    use_compose: true,
    # Auto-update set to false as Sentry upgrades involve install script, not just git pull
    auto_update: false
  }
]

Heartbeat script (auto-updates)

To keep your server updated automatically:

  1. From the production checkout, run: ruby install_launch_agent.rb --production
  2. Edit the generated plist file:
    vi ~/Library/LaunchAgents/co.contraption.toolbox.heartbeat.plist
    
  3. Configure the plist file:
    • Set your 1Password service account token in the OP_SERVICE_ACCOUNT_TOKEN field
    • Keep TOOLBOX_OP_BACKEND set to docker on macOS hosts where the native op daemon is unreliable
    • Update other paths if necessary
  4. Load the LaunchAgent: launchctl load ~/Library/LaunchAgents/co.contraption.toolbox.heartbeat.plist

The script will check for updates every minute and apply them if found. This is a production deployment mechanism: merging into main can update the live server on the next successful heartbeat.

Developing Toolbox on the production Mac

Use a separate clone for development. Leave the production checkout at ~/code/toolbox and the application checkouts named in config.rb untouched:

mkdir -p ~/development
git clone git@github.com:contraptionco/toolbox.git ~/development/toolbox
cd ~/development/toolbox
git pull --ff-only origin main
git switch -c my-change

Use your fork's remote URL if appropriate. A separate development environment is not necessary for editing Toolbox and running its unit tests. See AGENTS.md for the production boundaries, test commands, and PR workflow.

Do not start Toolbox from the development clone. Its configuration still names the production Docker containers, data, ports, and application repositories. The entrypoints refuse an unintended checkout before running services or writing heartbeat state. Manual orchestration and LaunchAgent installation additionally require --production. The default production location is ~/code/toolbox; TOOLBOX_PRODUCTION_REPO_DIR explicitly selects a different production location. Changing this variable does not move application data or isolate Docker.

For an intentional production run, prefer ./heartbeat.sh from the production checkout so the heartbeat lock serializes scheduled and manual runs. Toolbox requires a clean main matching origin/main and refuses unexpected branches instead of switching them. Development edits to the production checkout are unsafe even with these checks: the scheduler loads its own code from that path.

Git application services default to the remote main branch when branch is omitted. Set branch explicitly to pin another branch or release tag. Application source validation rejects unexpected branches, local commits ahead of or diverged from the remote, tracked modifications, and untracked source files. A pinned tag must match the remote tag's commit. Toolbox does not discard local changes to make a deployment pass.

Docker and static-file builds use a temporary snapshot of the verified commit, so ignored files and edits made during a build cannot enter that build context. Snapshots omit .git and reject submodules rather than silently building without their contents; applications requiring either need a supported build path first. Replacement app containers use the immutable ID of the newly built image. An unexplained change to a production image tag is refused while the existing container is retained. Do not use production image tags for development builds. Compose services retain their existing project directories and generated runtime files; their bind mounts and install commands still need production-only paths.

These checks reduce accidental deployment risk; they do not provide automatic rollback or zero-downtime replacement. Before rolling out this change, review existing application checkouts for local modifications and branch drift. A source-validation rejection leaves its existing container running and reports a failure. Do not remove local files or relax the checks just to silence that failure.

On macOS, the disk check accounts for storage the operating system can reclaim, while requiring at least 20 GiB to be immediately free before work starts. It blocks at 90% effective usage, or when that physical reserve is unavailable, and still warns about high raw usage. If macOS capacity information is unavailable, the original df percentage limit applies along with the physical reserve. Other operating systems retain the 90% df limit. The reserve is a starting floor, not a guarantee that every backup or build fits; disk usage is checked again after the run. Diagnose disk pressure separately; do not lower these guards or prune production volumes as part of development.

Security: Automatic screen locking

Included in the repository is a lock_screen_on_login.app utility that automatically locks your Mac's screen upon login. This is useful for servers configured with automatic login (to ensure services start on reboot) while still protecting physical access to the machine.

To set up automatic screen locking:

  1. Add lock_screen_on_login.app to your Login Items:
    • Open System Preferences/Settings → Users & Groups → Login Items
    • Click the "+" button and select the lock_screen_on_login.app from the repository
    • This ensures the screen locks immediately after automatic login

This setup allows your server to reboot and start services automatically without requiring manual login, while still ensuring that anyone with physical access to the machine needs a password to use it.

Configuring log forwarding (optional)

If you want to forward logs to a service like Papertrail:

  1. Create a Papertrail account and set up a log destination
  2. Run the installation script: ruby install_papertrail.rb
  3. Follow the on-screen instructions to complete the setup
  4. Update the generated log_files.yml configuration file with your Papertrail host and port

The script will:

  • Create a LaunchDaemon plist file for remote_syslog
  • Generate a sample log configuration if one doesn't exist
  • Provide commands to finalize the installation

For manual installation instead:

  1. Install the remote_syslog tool:
    brew install remote_syslog
    
  2. Configure the LaunchDaemon:
    sudo cp com.papertrailapp.remote_syslog.plist /Library/LaunchDaemons/
    sudo vi /Library/LaunchDaemons/com.papertrailapp.remote_syslog.plist
    
  3. Load the LaunchDaemon:
    sudo launchctl load /Library/LaunchDaemons/com.papertrailapp.remote_syslog.plist
    

Key services

Docker containers

All web applications run in Docker containers, making them isolated and easy to manage. The toolbox handles:

  • Container creation and configuration
  • Automatic updates when new images are available
  • Network configuration
  • Volume management for persistent data
  • Health checks for service monitoring

Ollama LLM Service

The toolbox includes configuration for running Ollama, an open-source LLM inference server:

  • Model: Configured with gpt-oss:20b by default
  • Port: Exposed on 11434 for API access
  • Access Methods:
    • Internal Docker network: http://ollama:11434
    • Local host: http://localhost:11434
    • Via Cloudflare Tunnel: https://ollama.contraption.co (configure your domain in config.yml)
  • Web Interface: Ollama provides a web UI accessible at the same URL
  • Model persistence: Models are stored in ~/data/ollama and persist across container restarts
  • Auto-start: Automatically starts on boot and pulls the specified model if not already present
  • Health monitoring: Includes health checks to ensure the service is responsive

To test the Ollama service after starting:

# Test from the host
curl http://localhost:11434/api/generate \
     -d '{"model":"gpt-oss:20b","prompt":"Write one sentence about Mac Minis."}'

# From another Docker container in the toolbox network
curl http://ollama:11434/api/generate \
     -d '{"model":"gpt-oss:20b","prompt":"Hello, Ollama!"}'

Cloudflare Tunnel

Cloudflare Tunnel creates a secure connection between your Mac and Cloudflare's edge network:

  • No need to open ports on your router
  • SSL termination and DDoS protection
  • Access control with Cloudflare Access (optional)
  • Custom domain support

Git repositories

For apps that require custom builds:

  • Automatic cloning and updating
  • Build process management
  • Deployment to containers or directories
  • Support for private repos (requires GitHub authentication)
  • Can handle complex setups involving initial installation scripts and integrated Docker Compose management.

Customizing for your needs

This is a personal tool that I use for my own server - you'll need to adapt it:

  1. Remove services you don't need (Ghost, Plausible, etc.)
  2. Add your own applications to the config
  3. Update domain names and URLs
  4. Configure your own Cloudflare tunnel
  5. Set up your own 1Password vault items

Telemetry

Toolbox collects anonymous usage data to help us understand how many people are using this software, so we can prioritize feature updates. We do not collect any personal information, credentials, or the content of your services.

You can view our full privacy policy at contraption.co/privacy, and see the telemetry script at lib/telemetry.rb.

To disable telemetry, set DISABLE_ANONYMOUS_TELEMETRY = true in config.rb or modify the code to remove it.

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

Simple automation for a Mac-based home web server

Resources

Stars

38 stars

Watchers

0 watching

Forks

Contributors

Languages