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.
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-var php_version=8.5 --db postgres --db valkeySee bonesdeploy skill doc templates for every template and its variables.
This creates:
.bones/
βββ bones.toml
βββ deployment/
βββ 01_*.sh
The files are yours. Edit them. Commit them. Read them when something breaks.
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 frameworkDatabase 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 ops@example.comSSL 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 jsonEmbedded 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
[framework]
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. 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 framework-specific 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.
BonesRemote also exposes scalar values from bones.toml as transient BONES_* variables in the build container (for example, BONES_FRAMEWORK_IS_STATIC and BONES_FRAMEWORK_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
