How to self-host Mastodon without Docker on your own VPS
Run Mastodon v4.7.3 on your own Ubuntu server with ox: Rails, Sidekiq, a Node streaming server, Postgres and Redis, from the config that ran.
To self-host Mastodon you need an Ubuntu server, a fork of the Mastodon repo with one ox.toml file in it, and 14 variables, seven of them required before the first deploy starts. In our run the first deploy took about 9 minutes, and ox set up Postgres and Redis and ran Sidekiq and the streaming server beside the web app.
How we tested On Oct 17, 2026, ox deployed Mastodon v4.7.3 from commit 8ff11c2b on a fresh DigitalOcean server with 8 vCPUs and 16 GB of memory. The run then redeployed it, backed it up and rebooted the server. Then a separate test agent, one that did not write the config, signed in, posted a status and saw it at the top of the home timeline after a reload, and checked the app's services and its backup.
Mastodon is a free social network server built on ActivityPub, the open standard that lets servers talk to each other, Mastodon or not. People on your server post text, links, pictures and video and follow each other. The code is under the AGPLv3. It is a Ruby on Rails app with PostgreSQL as its main database, Redis and Sidekiq for caching and queues, a separate Node.js server for live updates, and a React interface.
ox deployed Mastodon v4.7.3 (commit 8ff11c2b) on Oct 17, 2026 on a fresh DigitalOcean server with 8 vCPUs and 16 GB of memory; the first deploy took 9 minutes and a redeploy took 4 minutes.
- First deploy: 9 minutes, from adding the project until the app passed the run's first checks.
- Redeploy of the same commit: 4 minutes, until the app passed the checks again.
- Server: DigitalOcean, 8 vCPUs and 16 GB of memory (
s-8vcpu-16gb), with this app alone on it. Every one of the 85 apps we deployed ran on this one size, so it was not picked for this app. - Memory in use after the reboot: 2794 MB of 15986 MB, for the whole server: Ubuntu, the app and its staging copy, its services, Caddy and the ox agent. On a server with no apps, ox's tests on real servers hold the agent to 40 MB or less and Caddy to 50 MB or less.
- Services ox runs for it:
postgres,redis. - Background processes:
sidekiq,streaming, each its own systemd unit beside the web process. - Source: github.com/mastodon/mastodon at v4.7.3, commit
8ff11c2b.
What you need
An Ubuntu LTS server. The run used the size in the facts above. The repo pins Ruby 4.0.6 in
.ruby-versionand Node 24.19 in.nvmrc, and ox installs those toolchains and sets up the two services.A GitHub account to fork
mastodon/mastodon, and an ox account, which you open by signing in with it.A username, an email address and a password for the first account, the owner. A new Mastodon server has sign-ups closed.
A mail server, if you want real email. The run used a test mail catcher.
Not tested: real email, federation with other servers, media uploads, search, the mobile apps, a custom domain and a restore.
The steps
In the ox dashboard, press Add a server. It shows one install command with a token that works once. Run it on the server as root. It installs only the ox agent and Caddy.
Fork mastodon/mastodon on GitHub and take the tag v4.7.3, the release the run deployed. Put the ox.toml shown below at the root of your fork, commit it and push. The run added no other file. Next write a file named mastodon.env on your laptop with one line for each key in the variables list below, and set LOCAL_DOMAIN to ${PUBLIC_HOST}, as the run did. Mastodon's sample file says to make SECRET_KEY_BASE with bundle exec rails secret and the three encryption values with bin/rails db:encryption:init. With the ox CLI signed in, run:
ox new <you>/mastodon
ox review mastodon --from-file mastodon.env --waitThe project takes the repo's name. The review saves the values and starts the first deploy, and --wait prints the run until it ends. In the browser the same two steps are New project and the review screen, as the quickstart shows.
When the deploy is green, open the https address ox shows for the project and go to /auth/sign_in. Sign in with OWNER_EMAIL and OWNER_PASSWORD. On Home, type in the box that asks what is on your mind and press Post. The first page loads were slow while the server warmed up. The run served Mastodon on that preview address only. A domain of your own was not part of it, and the domains page covers how ox adds one.
The ox.toml, line by line
# Mastodon v4.7.3 (Rails 8.1, PostgreSQL, Redis, Sidekiq, a Node streaming
# server, Vite), as its operator would write it: ox detects Ruby, Yarn, the
# asset build, db:prepare, Puma and both services.
# No domain of its own: Mastodon forces https and marks its session cookie
# secure, and ox serves the preview address on https (its certificate is
# ordered while the long build runs), so sign-in works there.
# The server's name, which Mastodon says cannot change once it federates
# (.env.production.sample), the keys it aborts without
# (config/initializers/active_record_encryption.rb), and the owner, the only
# way in: a new server's registrations are closed.
required_variables = [
"LOCAL_DOMAIN",
"ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY",
"ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT",
"ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY",
"OWNER_USERNAME",
"OWNER_EMAIL",
"OWNER_PASSWORD",
]
# idn-ruby and charlock_holmes build against these; ffmpeg makes the video
# thumbnails.
packages = ["libidn-dev", "libicu-dev", "ffmpeg"]
[app]
health = "/health"
[build]
# db:prepare, then the owner, made as the admin guide's `tootctl accounts
# create NAME --email E --confirmed --role Owner` and `tootctl accounts
# approve NAME` make it (lib/mastodon/cli/accounts.rb), but with the
# password from OWNER_PASSWORD: tootctl makes up its own and prints it.
# Approval matters: a closed server leaves a new user pending, and a pending
# user cannot use the web app. Once: the runner prints which it did, and any
# error stops the deploy.
migrate = """\
RAILS_ENV=production bundle exec rails db:prepare && \
RAILS_ENV=production bundle exec rails runner '\
name = ENV.fetch("OWNER_USERNAME"); \
if Account.find_local(name)&.user then puts "Mastodon: the owner #{name} exists"; \
else user = User.new(email: ENV.fetch("OWNER_EMAIL"), password: ENV.fetch("OWNER_PASSWORD"), \
agreement: true, role: UserRole.find_by!(name: "Owner"), bypass_registration_checks: true, \
account: Account.new(username: name)); \
user.save!; user.mark_email_as_confirmed!; user.approve!; \
puts "Mastodon: made the owner #{name}"; end'"""
[services]
postgres = {}
redis = {}
[workers]
# Sidekiq runs the federation, mail and media jobs.
sidekiq = "RAILS_ENV=production bundle exec sidekiq"
# The streaming server is a separate Node process on its own port (ox gives
# each copy its own, so staging does not meet production's); the browser
# reaches it at /api/v1/streaming on the site's own name.
streaming = { run = "NODE_ENV=production BIND=127.0.0.1 node ./streaming", port = true, paths = ["/api/v1/streaming"] }
[storage]
# Media is kept under public/system.
keep = ["public/system"]required_variableslists seven keys.LOCAL_DOMAINis the server's name, which Mastodon's sample file says cannot be changed safely later. Mastodon's initializer aborts without the threeACTIVE_RECORD_ENCRYPTIONkeys. The threeOWNERkeys are the only way in while sign-ups are closed. A missing key stops the deploy before the long build.packagesadds three apt packages. Theidn-rubyandcharlock_holmesgems build againstlibidn-devandlibicu-dev, andffmpegmakes thumbnails of uploaded video.health = "/health"is Mastodon's own health route. ox moves traffic to a new release only once it answers.There is no install, build or start line. ox reads Ruby from
.ruby-version, installs the gems and Node packages, runs the asset build, and startsrails serveron the port it gives the app. Mastodon serves the compiled files inpublic/only whenRAILS_SERVE_STATIC_FILESistrue, and ox's Rails start sets it for you.There is no
domainsline. Mastodon forces https and marks its session cookie secure, so sign-in over plain http fails. ox serves the preview address on https and orders its certificate while the long build runs.migraterunsdb:prepare, then makes the owner astootctl accounts create(with--confirmed --role Owner) andtootctl accounts approvedo, but with the password fromOWNER_PASSWORD, where tootctl makes one up. The account also needs approving: on a closed server a new account waits as pending and cannot use the web app. The step prints whether it made the owner or found one, so it is safe on every deploy, and any error stops the deploy.[services]asks for Postgres and Redis. Redis runs as a unit only for this project.sidekiqin[workers]runs the background jobs: federation, mail and media.streamingis Mastodon's Node server.port = truegives each copy a port of its own, so staging never meets production's.BIND=127.0.0.1keeps it on the loopback address, andpathssends/api/v1/streaming, websockets included, to it on the site's own name.[storage]keepspublic/systemacross deploys. Mastodon writes uploaded media there.
LOCAL_DOMAIN
SECRET_KEY_BASE
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY
ES_ENABLED
S3_ENABLED
OWNER_USERNAME
OWNER_EMAIL
OWNER_PASSWORD
SMTP_SERVER
SMTP_PORT
SMTP_AUTH_METHOD
SMTP_FROM_ADDRESSNone of the DB_* or REDIS_* keys from Mastodon's sample file is in this list, because ox answers them from the two services. LOCAL_DOMAIN is a ${PUBLIC_HOST} reference, which ox fills in with the host name of the project's address. SECRET_KEY_BASE is the Rails secret that signs sessions. The sample file turns ES_ENABLED and S3_ENABLED on, and the run set both to false: it had no Elasticsearch and no S3 bucket. OWNER_USERNAME, OWNER_EMAIL and OWNER_PASSWORD are the first account. Mastodon blocks the usernames admin and owner, so the run used siteowner. It also checks that the email's domain has a mail server or an address record, so the run used an address at ${PUBLIC_HOST}. The four SMTP keys send mail to the test catcher, which takes it with no login, so the run set SMTP_AUTH_METHOD to none. See variables for how references work.
The services and the workers
Postgres holds accounts and posts, and Redis holds the cache and the job queues. The test agent posted a status, and Sidekiq logged its distribution job done right after. The streaming server logged a WebSocket upgrade and a started stream for the owner's timeline, which is how a new post reaches an open page. The run also deployed a staging copy beside production, and the memory figure above counts both.
Updates and backups
To move to a new release, merge its tag into your fork and push, or run ox deploy mastodon --wait. ox builds the new release beside the running one, runs db:prepare, and switches once /health answers. Before each migrate it dumps Postgres, and the run's redeploy left that dump. Read CHANGELOG.md in the repo before a major version.
Backups need no setup. The run's backup folder held three Postgres dumps (a daily one, one from Back up now and one from before the migrate) and a daily Redis archive. Run ox backups mastodon to list them, or ox backups mastodon now postgres --wait to take one. A restore was not tested, and neither was a backup of uploaded media. The backups page covers keeping copies in an S3 bucket.
Frequently asked questions
Do I need Docker to self-host Mastodon?
No. Mastodon's repo ships Docker files, and its README also points to a standalone install guide. With ox, the Rails app and the Node streaming server are built on your server and run as system services, with Postgres and Redis beside them. There is no image to pull, and an update is a push to your fork.
How do I make the first Mastodon account?
Set OWNER_USERNAME, OWNER_EMAIL and OWNER_PASSWORD before the first deploy. The migrate step makes that account, confirmed, approved and with the Owner role, and you sign in at /auth/sign_in. Sign-ups stay closed on a new server. Inviting people or opening them was not tested.
Do I have to set the ACTIVE_RECORD_ENCRYPTION keys?
Yes. Mastodon stops at boot when any of the three is empty, and ox lists them as required so a deploy stops before the build. Mastodon's sample file says not to change them once in use, since that can cause data loss. Make them with bin/rails db:encryption:init and keep a copy.
How much memory does self-hosted Mastodon use?
On the run's server, the whole machine used the memory the facts show after a reboot. That counts Mastodon, its staging copy, Postgres, Redis, Caddy and the ox agent. ox takes any server from 1 GB up, and says so when memory is why the app stopped.
New to running apps on your own server? How ox self-hosts your apps shows what ox sets up on your server.
