Everything you need to go from signup to a live app.
git push
ships it from then on.Once something is running, the left-hand menu covers the rest of the machine: databases, cron jobs, the firewall and SSH access.
Everything is free during the beta, until 17 September 2026. The paid plans open after that; the free plan stays free.
| Plan | Price | What it allows |
|---|---|---|
| Free | $0 | 1 server · 1 domain · 3 apps |
| Pro | $12/month | 5 servers · unlimited domains · unlimited apps |
| Business | $29/month | unlimited servers · unlimited domains · unlimited apps |
Limits count what exists, not what you have ever created: delete an app and the slot comes back. A staging clone is an app like any other, so it counts. Buddy never deletes anything of yours to make room — it refuses to create the next one and says which limit you reached.
None of this is what your servers cost. You pay your cloud provider directly for those, at their price, with no markup from us.
Buddy stores every credential encrypted and uses it only for the operations you trigger.
Use a Personal Access Token — the Tokens tab of API in the control panel, not OAuth Applications (those are for apps acting on someone else's account).
droplet: create, read, update, delete.dop_v1_ and is shown only once.Not open yet, so Buddy will not accept a Hetzner token: the integration is written but has never created a server on a real account, and shipping it on that basis would make you the one who finds out. When it opens, the token comes from the Cloud Console (not the Robot panel) → your project → Security → API tokens → Generate, with Read & Write.
Account → API → enable the API and copy the Personal Access Token. Under Access Control allow
0.0.0.0/0: Buddy calls Vultr from Google Cloud and its source IP is not fixed, so an allow-list with only your own address blocks it. Vultr keys are account-wide — there are no per-resource scopes.
Create an IAM user — not your root account, whose keys cannot be scoped — and give it an access key (Security credentials → Create access key → Third-party service). Paste the key ID and the secret; the secret is shown once and cannot be recovered.
AmazonEC2FullAccess for servers,
AmazonS3FullAccess for backups, AmazonSSMReadOnlyAccess so Buddy can look up the current Ubuntu image for the region you pick, and AmazonRDSFullAccess for databases.
AmazonRDSReadOnlyAccess is enough if you only want Buddy to list the instances you already run; creating one from the Databases page needs the full policy. Either way this key never creates a database or a login inside an instance — AWS has no API for that, so Buddy does it through one of your own servers.buddy-managed per VPC opening ports 22, 80 and 443. Nothing else is touched.Create a service account with roles/compute.admin (plus roles/storage.admin if you want backups), create a JSON key, and paste the JSON:
gcloud iam service-accounts create buddy-deployer --project=YOUR-PROJECT gcloud projects add-iam-policy-binding YOUR-PROJECT \ --member="serviceAccount:buddy-deployer@YOUR-PROJECT.iam.gserviceaccount.com" \ --role="roles/compute.admin" gcloud iam service-accounts keys create key.json \ --iam-account=buddy-deployer@YOUR-PROJECT.iam.gserviceaccount.com
My Profile → API Tokens → Create Token → Edit zone DNS template, scoped under Zone Resources to the domains Buddy should manage. Use an API token, not the Global API Key — the latter grants everything on your account and cannot be scoped.
Read-only access to the repositories you deploy. See private repositories.
Servers → Add server → pick a provider, credential, region, size and operating system. Ubuntu LTS releases are offered for now; the list comes from your own provider account, so it is always current.
Then choose how apps run on the machine:
Save your public keys under SSH access and pick them when you create a server; Buddy installs them for root at first boot. Removing a key from that list never touches a server it is already on — taking access away is something you do on the machine, deliberately. A server you brought yourself gets no keys, because there is no first boot to install them at: it is already yours and already reachable.
A startup script is something of your own to run on a new server — packages you always want, a monitoring agent, your dotfiles. Write it under Servers and pick it on the form.
It does not run from cloud-init, which is how the providers do it, and the difference is worth knowing. A boot script that fails leaves a server that never appears in the panel, or appears broken, with the reason in a log on a machine you may not be able to reach. Buddy runs yours as an ordinary command the moment the agent first reports in: after the machine is finished and reachable, so a mistake in your script cannot break the setup; once, as root; and with its output in that server's activity log, where a failure is visible and you can read what happened. It works the same on a server you brought yourself.
Editing or deleting a script changes what the next server gets. A server that already ran it is not touched, and nothing re-runs on its own.
The Databases page can make you one of three things, and they are not equally expensive.
Deleting is offered only for instances Buddy created, and asks you to type the name first. An instance that was already in your account is listed and can be managed, never removed — one wrong click on somebody's production database is not worth the convenience of a button. Removing an engine Buddy installed on your own server takes the entry off the page and leaves the software installed, because another app on that machine may be using it.
A classic server is created with one database server, installed on the first deploy and shared by every site on that machine — so the choice cannot be changed afterwards. Only long-term releases are offered, because this is the one thing on the box you cannot casually replace later:
The ready-made apps here — WordPress, Drupal, Joomla, Nextcloud, Magento — all need MySQL or MariaDB, so a classic server built with PostgreSQL will not offer them. It says so when you try, rather than failing halfway through the install. A Git-deployed app is free to use whichever engine its server has.
Buddy talks to MariaDB, MySQL and PostgreSQL the same way: it creates a database and a user scoped to it per app, dumps it during backups and drops it when the app is deleted.
The Databases page does more than list what is there. On a classic server you can create a database, create a login and grant it access, give a login a new password, and remove either — each one is carried out by the agent on the machine. Buddy never connects to your database itself; it has no route to it, and should not.
Back up next to a database dumps that schema on its own into a bucket in your own cloud project, and Restore puts it back. A restore can go into a different database if you name one, which is how you take a copy of production into a scratch database without touching the original. Restoring replaces whatever is in the target.
An app's own database and login are not offered for deletion here: removing them would leave the site running and unable to reach its data. Delete the app instead, which removes both.
Generated passwords are shown once and never stored — Buddy has no way to tell you one again, only to set a new one.
An app on a classic server can use a Cloud SQL instance in your own Google Cloud project instead of the database on the machine. Connect the Google Cloud account that owns it under
Cloud accounts — the service account needs roles/cloudsql.admin — and the instance appears in the app form.
On each deploy Buddy creates the app's schema and a user scoped to it, and adds the server's IP address to the instance's authorized networks. Deleting the app drops that schema and user. The instance itself is always yours: Buddy never creates, resizes or deletes one, so nothing here starts costing you money on its own. The instance must have a public IP address — Buddy connects to the public endpoint behind the allow-list, not through the Cloud SQL Auth Proxy.
Connect an AWS account and every MySQL, MariaDB and PostgreSQL instance it can see is listed, across all the regions Buddy offers. Oracle and SQL Server are left out — there is nothing useful Buddy could do with them.
RDS works differently from Cloud SQL, and it is worth knowing why. Google's API can create a database, create a login and export one schema; AWS has no equivalent — the RDS API creates instances and snapshots, and everything inside an instance exists only over a SQL connection. So managing an RDS instance goes through one of your own servers: pick the server that can reach the endpoint, give Buddy the master login, and that server runs the SQL. The master password is stored encrypted and is only ever sent to the server you chose; AWS never reveals it, which is why Buddy has to be told it.
The server has to be able to reach the endpoint — the same VPC, or a security group that lets it in. A private instance is invisible to anything outside its VPC, so a server elsewhere cannot manage it however correct the password is.
Back up here is a dump of one database into your own S3 bucket, not an RDS snapshot: a snapshot covers the whole instance and only restores as a new instance, which is a much larger and more expensive thing than putting one schema back where it was. Buddy never creates, resizes or deletes an instance.
Any Debian/Ubuntu server with Docker works. Add a server with “I already have a server” and run the install command shown on its page:
curl -fsSL https://devopsbuddy.io/install.sh | sudo BUDDY_TOKEN=… BUDDY_URL=https://devopsbuddy.io bash
The agent is outbound-only: it polls over HTTPS and nothing listens on your server. Note: on BYO servers you manage your own reverse proxy unless you let Buddy provision Traefik.
Choose Git repository as the source, paste the URL and optionally a branch. The agent clones and builds with Nixpacks — PHP/composer, Node, Python, Go, Ruby and more are detected automatically. Your app must listen on the PORT environment variable (default 8080).
Choose Ready-made app as the source and Buddy deploys the application with its database, its cache where it uses one, and HTTPS — and then installs it. You land on a working site with the admin account shown on the app's page, not on a setup wizard. The password is generated for you and stored encrypted.
| App | Runs on | Comes with |
|---|---|---|
| WordPress | container + classic | MySQL or MariaDB, optional Redis object cache, PHP tuned with OPcache |
| Drupal | container + classic | MariaDB, the standard profile installed, and Drush for caches and updates |
| Joomla | container + classic | MariaDB, installer removed once it has run |
| Nextcloud | container + classic | MariaDB, Redis file locking, users' files kept outside the web root |
| Magento | container + classic | MySQL, OpenSearch, Redis for sessions and cache |
The installation runs again on every deploy and stops as soon as it sees the app is already there, so redeploying is safe and takes about a second.
Magento is the heavy one, on either kind of server. Adobe publishes no image, so the store is built from source on your machine. Before you pick it:
127.0.0.1, so nothing outside the machine can reach it.It needs no Adobe Marketplace account: the Open Source release is a public tag and its dependencies come from Packagist.
On a classic server, Joomla's admin login has to be letters only — Joomla's own installer rejects anything else, and it does so on the server rather than in the form.
Connect Bitbucket, GitHub or GitLab under Cloud accounts, then pick that account in the Private repository? box on the app form. Buddy needs read access only — it clones, it never pushes.
Always use the https:// address of the repository. An ssh:// or
git@host:path URL has nowhere to carry a token, and Buddy will tell you so instead of failing at deploy time.
repository:read. Paste the token and leave the username blank.read_repository.Encrypted, on the credential — never on the app and never in the deploy history. Each deploy carries only a reference to the credential; the clone URL is assembled at the moment your server asks for the job. If a clone fails, git echoes the remote it tried, so the credentials are stripped out of the log before it is saved. And since the token reaches your own machine, it ends up in .git/config inside the app directory there — on classic servers the vhost blocks dotfiles, so it is never reachable over the web.
Revoke a token at Bitbucket or GitHub and the next deploy fails cleanly; disconnecting the account in Cloud accounts leaves running apps untouched but stops future deploys of private repos.
Paste any compose file. When a domain is set, Buddy strips host port bindings from the web service, injects Traefik labels (TLS via Let's Encrypt) and attaches services to the shared buddy network. Persistent data belongs in ./data (bind-mounted under /var/lib/buddy/apps/<slug>/data on the VM).
Pick a domain and subdomain on the app form. Buddy upserts the Cloudflare A record to the VM's IP and Traefik obtains the certificate — the app is live at https://sub.yourdomain.com right after the first deploy.
On an app's page, Password protection asks for a username and password before anything on the site loads. Leave the password blank and Buddy generates one; it is shown on that page. It takes effect from the next deploy.
Staging copies get this automatically. A clone is a second copy of a live site — its content, and often its customers' data — sitting on the public internet under a name anyone can guess. The credentials are on the clone's page; turn it off there if the copy is meant to be seen.
The certificate keeps renewing either way: Let's Encrypt's check is let through without a password, which is otherwise an excellent way to have a site expire in ninety days.
On a Git app, open it and turn on Push to deploy. Buddy shows a URL and a secret to paste into your repository's webhook settings:
application/json, secret in the Secret field, event: just pushes.?secret=…; that link is the secret and anyone holding it can trigger a deploy.Only pushes to the branch the app tracks deploy it — a push to any other branch is answered politely and ignored. GitHub and GitLab requests are checked against the secret before anything happens, so a stranger who guesses the URL gets a 401. If the secret ever leaks, Rotate secret replaces it; deploys stop until you paste the new one into the repository.
Under Cron jobs, pick a server, give the job a name, a five-field schedule, the user to run it as and one command. Buddy writes a single file, /etc/cron.d/buddy, and rewrites it in full every time you change something here — your own crontab and anything else under /etc/cron.d are never read or modified.
Use absolute paths: cron's environment is not your login shell's. Named shortcuts like @daily
are not accepted; write them out (0 3 * * *). Pausing a job takes it out of the file without forgetting what it was.
A worker is a process that should always be running — a Laravel queue worker, a consumer, a scheduler daemon. On the app page (classic servers), add one with a name, the command, the user and how many processes you want. Buddy writes one systemd unit per process, so each is restarted if it exits and started again after a reboot, and its output goes to the journal (journalctl -u buddy-worker-<app>-<name>).
Stopping sends SIGTERM and waits the timeout you set (60 seconds by default) before killing it, which is what lets a queue worker finish the job it is holding rather than losing it. On a container server, add the process to your compose file instead.
Servers start out reachable only as root, with the key you pasted when creating them. Under
SSH access you can give each person their own account: a username and one or more public keys, optionally with passwordless sudo.
Accounts are key-only — no passwords are ever set — and Buddy manages only the accounts it created, so anything you set up yourself on the machine is left alone. Removing a login deletes the account and its home directory on the next sync, which is how you take one person's access away without touching anybody else's.
Every VM Buddy creates installs security updates on its own: unattended-upgrades is configured for the security pocket only, and it never reboots the machine by itself. Kernel updates therefore wait for a reboot you choose. On a server you brought yourself, whatever you already had stays as it is.
Every site on a classic server is served by an nginx configuration Buddy writes and rewrites on each deploy. You do not have to ask for any of this:
X-Content-Type-Options: nosniff,
X-Frame-Options: SAMEORIGIN and a strict referrer policy..php that arrives through a file upload is refused rather than executed — the single most common way a content site is taken over..env, .git, database dumps, .bak, .log and friends all return 403. Bots start asking within minutes of a new address appearing./.well-known/, which certificates need.xmlrpc.php is blocked — it takes password guesses in batches and can be used to reflect traffic at someone else — and wp-login.php is rate limited to 20 attempts a minute per address, which a person never notices and a password list cannot get past. Readable giveaways like readme.html and a stray wp-config.php backup are refused too..htaccess to protect are blocked in nginx, which does not read
.htaccess.pub/, and everything above it — source, configuration, generated code — is blocked as well as being out of the document root.Container servers sit behind Traefik instead, which terminates TLS and routes by hostname; the application's own image decides what it serves.
Both kinds of server are meant to hold more than one site, and each app keeps to itself:
/var/www/<app>, its own nginx rules, its own access and error logs.The free plan allows one server and one domain, with as many apps on it as you like.
Off by default. Turning it on for a server opens ports 80 and 443 to the world and closes everything else; add a rule for anything more, optionally limited to one network (10.0.0.0/8, your office address). All your servers are on one page under Firewall.
Rules are applied with iptables in two chains. That is not an implementation detail you can ignore: Docker publishes ports by writing rules that are evaluated before the usual input chain, so a firewall that only guarded the host would silently let every published container port through. Buddy guards both.
SSH, loopback traffic and ping are always allowed, whatever the rules say — a firewall you can lock yourself out of is worse than none.
A rule closes a port; this closes the door on one visitor. Turn on Block addresses that keep failing and Buddy installs fail2ban with four jails: repeated SSH failures, repeated wrong passwords on a password-protected site, anyone hammering a login page past its rate limit, and the scanners that ask every new address for .env and .git/config. A ban lasts an hour, two for SSH.
Every ready-made app has a limit in front of its login page — twenty attempts a minute per address, invisible to a person and useless to a bot working through a password list. On its own that limit only answers 429, which costs the sender nothing; with blocking on, an address that keeps collecting 429s stops being answered at all.
A ban only ever covers the ports its jail is about, so tripping a web jail while testing your own site can never cost you SSH. If you do lock yourself out of a site, the ban expires by itself, or
fail2ban-client unban --all on the server clears it immediately.
Not CSF, deliberately. CSF wants to own the whole firewall, and on a server running containers its port list does not cover what Docker publishes — which is exactly the case Buddy's own two chains handle. fail2ban writes into a chain of its own and leaves the rest alone.
On the app page, Backup now archives the app directory (compose files + ./data) to a buddy-backups-* bucket in your own cloud account — Google Cloud Storage (the service account needs roles/storage.admin) or Amazon S3 (the key needs s3:CreateBucket,
s3:PutObject and s3:GetObject). Buddy makes the bucket on the first backup. With both connected it uses the one in the same cloud as the server, so the bytes do not cross a network you pay for.
Restore stops the app, restores the snapshot and brings it back up, and always reads from the bucket that backup actually went into. Transfers use pre-signed URLs — storage credentials never reach the VM.
The agent reports server CPU/memory/disk and per-app container metrics every 20 seconds. Find them on the dashboard, agent pages and app pages.
Account → Enable 2FA → scan the QR with any authenticator app and confirm a code. From then on, login asks for a 6-digit code.
Buddy sends a welcome note when you sign up, a link when you ask to reset your password, and a heads-up when that password changes. Nothing else — no newsletters, and no address is ever passed on.
Replies go to buddy@devopsbuddy.io and reach a person. If a message never turns up, look in the spam folder first and then write to that address from anywhere.
The Activity page records what changed and who changed it — servers created and deleted, deploys, accounts connected, sign-ins. It answers the question you only ask afterwards.
Only actions are recorded, never what you looked at. Passwords and tokens never reach it: anything that looks like a credential is replaced before the entry is written, because an audit log that quietly collects secrets is worse than none — it is the table nobody thinks to protect. Entries outlive what they refer to, so deleting a server does not erase the record of who deleted it.
docker network create buddy once.