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.
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
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
- 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.
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.
Use a separate development clone as described in AGENTS.md:
bundle config set --local path .runtime/bundle
bundle install
ruby web/server.rb --demoOpen 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.jsFor 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.
For full setup instructions, read: How to host web apps on a Mac Mini
Before you start, ensure you have:
- A Mac running macOS (tested on Monterey and later)
- Homebrew installed
- Docker Desktop installed and running
- Git installed and configured with access to your repositories
- ASDF for Ruby version management
- 1Password CLI installed and configured
- Cloudflare account with a domain
- Cloudflare Tunnel configured
- (Optional) UptimeRobot account for monitoring
- (Optional) Papertrail account for log aggregation
- Fork this repository
- Clone your fork to your Mac
- Configure your Cloudflare tunnel in
config.yml - Edit
config.rbto match your services and domains - Install Ruby:
asdf install - Commit and push your configuration to your fork's
main, then update the production checkout and run:ruby toolbox.rb --production - Set up the heartbeat script for auto-updates
The toolbox relies on 1Password CLI for secure credential storage:
-
Install 1Password CLI:
brew install 1password-cli -
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.
-
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)
- Sign in to your 1Password account interactively:
For detailed setup instructions, follow the official 1Password CLI guide.
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:
- Create a Cloudflare account and add your domain
- Install cloudflared:
brew install cloudflared - Authenticate with Cloudflare:
cloudflared tunnel login - Create a tunnel:
cloudflared tunnel create toolbox - Create a
config.ymlfile 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
- Update the
TUNNEL_CONFIGsection inconfig.rbto reference your config.yml file
The toolbox will manage starting and running your Cloudflare Tunnel using this configuration.
- Edit
config.rbto match your services and domains - Configure your Docker services, Git repositories, and system services in the corresponding sections
- Update the Cloudflare Tunnel configuration to reference your
config.ymlfile - 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
}
]To keep your server updated automatically:
- From the production checkout, run:
ruby install_launch_agent.rb --production - Edit the generated plist file:
vi ~/Library/LaunchAgents/co.contraption.toolbox.heartbeat.plist - Configure the plist file:
- Set your 1Password service account token in the
OP_SERVICE_ACCOUNT_TOKENfield - Keep
TOOLBOX_OP_BACKENDset todockeron macOS hosts where the nativeopdaemon is unreliable - Update other paths if necessary
- Set your 1Password service account token in the
- 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.
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-changeUse 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.
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:
- Add
lock_screen_on_login.appto your Login Items:- Open System Preferences/Settings → Users & Groups → Login Items
- Click the "+" button and select the
lock_screen_on_login.appfrom 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.
If you want to forward logs to a service like Papertrail:
- Create a Papertrail account and set up a log destination
- Run the installation script:
ruby install_papertrail.rb - Follow the on-screen instructions to complete the setup
- Update the generated
log_files.ymlconfiguration 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:
- Install the remote_syslog tool:
brew install remote_syslog - Configure the LaunchDaemon:
sudo cp com.papertrailapp.remote_syslog.plist /Library/LaunchDaemons/ sudo vi /Library/LaunchDaemons/com.papertrailapp.remote_syslog.plist - Load the LaunchDaemon:
sudo launchctl load /Library/LaunchDaemons/com.papertrailapp.remote_syslog.plist
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
The toolbox includes configuration for running Ollama, an open-source LLM inference server:
- Model: Configured with
gpt-oss:20bby default - Port: Exposed on
11434for 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 inconfig.yml)
- Internal Docker network:
- Web Interface: Ollama provides a web UI accessible at the same URL
- Model persistence: Models are stored in
~/data/ollamaand 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 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
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.
This is a personal tool that I use for my own server - you'll need to adapt it:
- Remove services you don't need (Ghost, Plausible, etc.)
- Add your own applications to the config
- Update domain names and URLs
- Configure your own Cloudflare tunnel
- Set up your own 1Password vault items
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.
This project is licensed under the MIT License - see the LICENSE file for details.