Deploy a dozen modern, isolated web apps on a $5 Linux box, without ever touching the server. Docker not required.
BonesDeploy is a feature-rich, yet very lightweight deployment framework for developers and vibe-coders who want to run self-hosted sites, with an emphasis on tried and true, old-school security principles. Most modern deployment systems just wrap everything in Docker. Docker is incredible, one of the best technologies ever. But I, and many others, are getting tired of running complex machinery through YAML.
WARNING: BonesDeploy is still under active development, but is almost in a stable state. Expect sharp edges and perhaps some cool bugs.
Self-hosting should not require building your own miniature cloud platform.
Coolify is impressive software, and it serves developers who want a flexible, Docker-first platform capable of running almost anything. BonesDeploy makes a different bet: most web applications do not need that much machinery.
BonesDeploy is batteries included. It supports a deliberate set of modern web frameworks, makes the important decisions for you, and runs directly on the operating system wherever possible. There is less to configure, less to understand, and less sitting between your application and the machine you paid for.
Docker is remarkable technology. It is also frequently overkill for deploying a small web application. You inherit a daemon, container networking, volumes, port mappings, Compose files, and another security model layered on top of Linux. Get one port binding wrong and a private database can become a public one. BonesDeploy avoids that entire class of mistake by refusing unsafe configurations and keeping private services private by default.
Containers still have their place. BonesDeploy uses rootless Podman for isolated builds, where the boundary is genuinely useful. The build runs inside a constrained environment, produces a release, and then disappears.
The application itself runs as an ordinary Linux service. Every site gets its own user, processes, permissions, and resource limits. Systemd, AppArmor, seccomp, cgroups, and the Unix permission model do the work they were designed to do.
The result is not a general-purpose platform for every imaginable workload. It is a complete deployment system for the kind of web applications most developers actually run: automatic server setup, HTTPS, encrypted secrets, isolated builds, atomic releases, rollbacks, diagnostics, and strong defaults.
All without turning a $5 Linux box into a tiny Kubernetes tribute act.
BonesDeploy deploys project releases to a remote Linux server over SSH. It scaffolds deployment configs and scripts into your repo, publishes the .bones/ dataset into root-owned bonesremote site state, and runs the release lifecycle remotely without turning the bare Git repo into the control plane.
No platform. No control plane. No required Docker setup. No pretending your VPS is a tiny Kubernetes cluster.
It gives you versioned releases, rollback, shared runtime state, service restarts, and per-site Linux isolation using the tools already on the box.
It's also AI agent friendly, with dedicated commands to help your agent understand how to setup and manage your server without ever leaving your machine.
BonesDeploy builds two binaries:
bonesdeployβ the local CLIbonesremoteβ the remote release runner
And embeds a Python provisioning runtime:
bonesinfraβcrates/bonesinfra/python/, embedded by the Rustbonesinfracrate
Deploying small apps should not require a platform team.
Most apps need a few boring things done correctly:
- put each release in its own directory
- keep uploads and runtime files outside the release
- restart the right service
- keep a few old releases around
- roll back without drama
- stop one site from casually reading another site's files
That is what BonesDeploy is for.
This is the part I care about.
BonesDeploy treats each site as its own thing on the server. Each site gets its own isolated services via systemd.
Each site can get its own:
- Linux user
- Linux group
- writable shared paths
- systemd runtime services
- nginx config
- AppArmor policy
- Seccomp configs
The deploy user deploys. The runtime user runs the app. Root provisions the machine.
That is the whole model.
Docker is useful. It gives you packaging, repeatability, and another layer of isolation.
But Docker is heavy, and slow, and you see this when you try running multiple Docker sites on a machine with less than 8GB of RAM.
Docker is also where a lot of people hide from Linux.
Instead of setting up users, groups, permissions, services, sockets, nginx, PHP-FPM, AppArmor, and runtime directories correctly, we stuff the app in a container and call it done.
Sometimes that is the right trade.
BonesDeploy takes the other trade.
It assumes the server is the deployment target, and then does the annoying work of centralizing the Linux setup per site.
You can still use Docker with BonesDeploy. Put docker compose in your deploy scripts.
Docker just is not the foundation.
BonesDeploy can run applications directly on Linux or inside Docker. Native is
the default, so existing bones.toml files do not change behavior. Select the
backend during initialization or set it explicitly:
[runtime]
backend = "docker"Docker mode keeps the existing release lifecycle and rootless Podman build pipeline. Docker is used only for the application runtime: BonesDeploy owns the container command and mounts, the active release is read-only, shared paths remain writable, and host Nginx and TLS remain the public ingress.
Docker runtime mode uses the conventional privileged Docker daemon. It does not grant Docker access to the deploy, build, runtime, or git users, does not execute project Compose files, and does not mount the Docker socket into an application. This is a different security tradeoff from native mode because a privileged daemon is part of the runtime control plane.
Laravel Docker runtime selection is currently the supported containerized runtime. Other frameworks continue to use the native backend.
Runtime templates set up the Linux pieces for a framework.
| Template | Status | Notes |
|---|---|---|
| Laravel | Working | PHP / PHP-FPM setup |
| Next.js | Working | Node runtime setup |
| Nuxt | Working | Nuxt runtime setup |
| Vue | Working | Static frontend setup |
| Django | Not tested | Python / Gunicorn not tested yet |
| Rails | Not tested | Ruby not tested yet |
Templates are not magic. They are shared server setup so every project does not become a custom snowflake.
Install the local CLI:
cargo install --locked --git https://github.com/AlextheYounga/bonesdeploy.git bonesdeployInstall the remote runner on the server:
sudo cargo install --locked --root /usr/local --git https://github.com/AlextheYounga/bonesdeploy.git bonesremote --forceRemote host provisioning, including sudoers policy, is handled by bonesinfra during bonesdeploy init remote setup.
From your project repo:
bonesdeploy initFor CI or AI agents, pick a runtime template and pass variables non-interactively:
bonesdeploy init --non-interactive --project-name atlas --host deploy.example.com \
--template laravel --runtime-backend docker --runtime-var php_version=8.5 \
--service postgres --service valkeySee bonesdeploy skill doc templates for every template and its variables.
This creates:
.bones/
βββ bones.toml
βββ deployment/
β βββ 01_*.sh
βββ infra/ # project-owned provisioning source
βββ __init__.py
βββ runtime.py # orchestrates the framework's services
βββ manifest.py # declares artifacts, services, and mode
βββ custom.py # ordinary project infrastructure module
βββ templates/ # local Jinja2 templates for this project
The files are yours. Edit them. Commit them. Read them when something breaks.
infra/runtime.py is imported and run by BonesInfra when you invoke
bonesdeploy remote runtime. It is a vendored snapshot of the selected canonical
framework implementation; edit it, its supporting modules, and its local
templates as project infrastructure.
Deployment scripts run in filename order:
01_install_deps.sh
02_build.sh
03_migrate.sh
Provision the base server:
bonesdeploy remote setupProvision the site runtime:
bonesdeploy remote runtimeThis runs the provisioning in your project's .bones/infra/runtime.py:
framework services, per-site nginx, AppArmor, and your infra/custom.py
project hook. Templates rendered by the generated infrastructure come from
.bones/infra/templates/.
Database services selected at init are provisioned by bonesdeploy setup, or later with:
bonesdeploy remote dbsSupported services are PostgreSQL, MariaDB, MySQL, MongoDB, Valkey, and Redis. They listen only on localhost; Redis and Valkey use separate per-project instances, while the SQL/Mongo services use database-scoped accounts. Use an SSH tunnel for workstation access. Generated credentials live in the protected remote shared/.env, never in .bones/. MariaDB and MySQL are alternatives and cannot share one host.
Add SSL after DNS points at the server:
bonesdeploy remote ssl --domain app.example.com --email [email protected]SSL is separate on purpose. Get the site working first. Add certificates after DNS is real.
Deploy:
bonesdeploy deployRollback:
bonesdeploy rollbackInspect releases, including a release that is currently building:
bonesdeploy releasesCancel a named building or interrupted release and clean its temporary build state:
bonesdeploy releases kill 20260715_225306Check the setup:
bonesdeploy doctorCheck only the local side:
bonesdeploy doctor --localdoctor reports three states: green checks are healthy, yellow pending items
are expected next steps (such as the first Git push after setup), and red
failures need attention. Pending first-push state exits successfully so setup
can finish without looking broken. For agents and scripts, use the stable
machine-readable next-step guide:
bonesdeploy skill next --format jsonInspect every project-specific remote artifact and managed systemd service
declared by .bones/infra/manifest.py, the configured services, and the SSL
strategy without changing the server:
bonesdeploy manifest
bonesdeploy manifest --format jsonThe manifest reports present, missing, and wrong-kind paths, plus active and enabled state for project-managed services. JSON is intended for automation; neither format prints file contents or secrets.
Embedded documentation for AI agents lives under the skill command:
bonesdeploy skill # orientation doc
bonesdeploy skill list # names of every embedded doc
bonesdeploy skill doc workflows # end-to-end flows
bonesdeploy skill doc methodology # permission model and doctrineSync .bones/ changes to the server:
bonesdeploy pushUpdate the local and remote binaries:
bonesdeploy updatebonesdeploy init creates .bones/bones.toml:
[app]
remote_name = "production"
project_name = "myproject"
repo_path = "/home/git/myproject.git"
project_root = "/srv/sites/myproject"
[app.server]
host = "deploy.example.com"
ssh_user = "root"
port = "22"
[app.deploy]
branch = "master"
deploy_on_push = false
releases = 5
[app.dns]
domain = ""
preview_domain = ""
email = ""
ssl_enabled = false
[runtime]
template = "custom"Common defaults:
.bones/
βββ bones.toml # project, build, and runtime configuration
βββ deployment/
βββ build/
β βββ 01_*.sh # build scripts (run sequentially in the buildpack-deps container)
βββ prepare/
βββ 01_*.sh # prepare scripts (run as the site user before activation)
The optional git push transport uses two thin internal adapters (local pre-push guard and remote post-receive trigger) that are embedded in the binaries. You do not see or manage them under .bones/. Set deploy_on_push = true in .bones/bones.toml to enable git-triggered deploys; the default is false.
Build scripts in .bones/deployment/build/ must be numbered (for example 01_install_deps.sh, 02_build.sh) and run in order inside bonesremote's buildpack-deps:bookworm container. Each build script is capped at [build].timeout_seconds (default 300; systemd terminates the script's whole process tree when exceeded). A value of 0 disables the per-script timeout. Bonesremote streams an ephemeral copy of the deployment bundle into the container at /workspace/deployment, so the build user never needs host access to bonesremote's control-plane files. BonesInfra provisions a private persistent cache for each build user; bonesremote mounts it at /workspace/cache and exposes BUILD_CACHE_DIR. The shared deployment functions use it for Node, Corepack, npm, pnpm, Yarn, Composer, and Bundler downloads. Installed dependency trees and build output remain disposable. Prepare scripts in .bones/deployment/prepare/ also run in order, but on the host as the site runtime user after shared paths are wired and before activation. Bonesremote streams the shared functions into each prepare shell before the prepare script, so prepare scripts do not source the root-owned deployment bundle.
Build scripts can set runtime options such as NODE_OPTIONS=--max-old-space-size=<MiB> when a project needs a V8 heap limit. Node does not provide a general CPU-percentage limit; UV_THREADPOOL_SIZE only changes libuv's file-system, crypto, DNS, and zlib worker pool. Beyond per-script timeouts, BonesInfra caps each build user's host-level slice at 80% CPU quota, 80% memory high/max, and MemorySwapMax=0, so a runaway build fails rather than exhausting host memory or swap.
BonesRemote also exposes scalar values from bones.toml as transient BONES_* variables in the build container (for example, BONES_RUNTIME_IS_STATIC and BONES_RUNTIME_TEMPLATE). Runtime permissions, shared paths, service identities, server connection details, and DNS/SSL configuration are excluded. Use .env.build for committed public build configuration; use shared/.env for runtime secrets.
Rootless Podman commands run through the dedicated build user's systemd user manager. Deploy verifies that manager, Podman, and the Infra-provisioned build cache before staging a release. The runtime application user remains a separate home-less, non-login account and never owns or operates the build container.
Git hooks are an optional transport β bonesdeploy deploy is the primary deployment command. The remote post-receive trigger is embedded in the bonesremote binary and installed into the bare repo automatically.
BonesDeploy is for:
- one-server apps
- VPS deployments
- small production apps
- side projects that grew up
- Raspberry Pis and old servers
- developers who want to understand their deploys
- developers who want Linux isolation without making Docker mandatory
BonesDeploy is not trying to be:
- Kubernetes
- Heroku
- Nomad
- a PaaS
- a dashboard
- a managed database service
- a multi-node orchestration layer
Use those when you need those.
Install:
cargo install cargo-llvm-covRun:
cargo covLCOV:
cargo cov-lcovHTML:
cargo cov-htmlReports go here:
target/coverage/
MIT
