[Blog](https://deploywithox.com/blog) [Guides](https://deploywithox.com/blog#c-guides) # How to self-host Pleroma without Docker on your own VPS Run Pleroma v2.10.2 on your own Ubuntu server with ox: an Elixir release and Postgres, built on the host with no Docker, from the config that ran. Saurav Sharma Oct 17, 2026 11 min read On this page - [What you need](https://deploywithox.com/blog/self-host-pleroma#what-you-need) - [The steps](https://deploywithox.com/blog/self-host-pleroma#steps) - [The ox.toml, line by line](https://deploywithox.com/blog/self-host-pleroma#config) - [The service and the gotchas](https://deploywithox.com/blog/self-host-pleroma#services) - [Updates and backups](https://deploywithox.com/blog/self-host-pleroma#updates) - [Frequently asked questions](https://deploywithox.com/blog/self-host-pleroma#faq) To self-host Pleroma you need an Ubuntu server, a copy of the Pleroma repo with an `ox.toml` and one config file added, and nine 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 ran the Elixir release as one service. **How we tested** On Oct 17, 2026, ox deployed Pleroma v2.10.2 from commit [`cd8816ec`](https://git.pleroma.social/pleroma/pleroma/-/commit/cd8816eccec355b5ff1533dc75fc86e9927baf2b) 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 with a picture and saw both on the profile after a reload, and checked the app's services and its backup. [Pleroma](https://git.pleroma.social/pleroma/pleroma) is a microblogging server for the fediverse. People post short statuses with pictures, follow each other and read timelines, and your server trades posts with any other server that speaks ActivityPub, [Mastodon](https://deploywithox.com/blog/self-host-mastodon) among them. It is written in Elixir, keeps its data in PostgreSQL, and offers the Mastodon client API with a few additions of its own. The code is under the AGPLv3. The backend serves its web front end, Pleroma-FE, at the site's root. ox deployed Pleroma v2.10.2 (commit cd8816ec) 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 6 minutes. - **First deploy:** 9 minutes, from adding the project until the app passed the run's first checks. - **Redeploy of the same commit:** 6 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:** 1145 MB of 15986 MB, for the whole server: Ubuntu, the app, 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`. - **Source:** [git.pleroma.social/pleroma/pleroma](https://git.pleroma.social/pleroma/pleroma) at v2.10.2, commit [`cd8816ec`](https://git.pleroma.social/pleroma/pleroma/-/commit/cd8816eccec355b5ff1533dc75fc86e9927baf2b). The Pleroma front page with its log-in form The Pleroma home timeline after signing in as owner The owner profile showing the first post on the server ## What you need - An Ubuntu LTS server. The run used the size in the facts above and measured its memory there. - A GitHub account to hold your copy of the repo, and an ox account, which you open by signing in with GitHub. Pleroma's code lives on its own forge, so you clone it from there. - A nickname, an email address and a password for the first admin. - A mail relay, if you want real email. The run pointed Pleroma at a test mail catcher. Not tested: real email, sign-ups by other people, federation with other servers, phone and desktop apps, another front end, a custom domain and a restore. ## The steps In the ox dashboard, press Add a server. ox shows an install command with a token that works once. Run it as root on your Ubuntu server. It adds the ox agent and Caddy and nothing more. On your laptop, clone Pleroma from `git.pleroma.social/pleroma/pleroma` and check out the tag `v2.10.2`, the release the run deployed. Add the `ox.toml` at the repo root and the `config/ox.exs` file, both shown below, and push the result to a new GitHub repository named `pleroma`. Then write a file called `pleroma.env` with one line for each key in the variables list below. Pleroma's own config generator makes its two secrets 64 characters long and its two salts 8. The run used random values of those lengths, which `openssl rand -base64 48` and `openssl rand -base64 6` print. With the [ox CLI](https://deploywithox.com/docs/cli) signed in, run: Shell ```bash ox new /pleroma ox review pleroma --from-file pleroma.env --wait ``` The review saves your values and starts the first deploy, and `--wait` prints the run until it ends. The browser has the same two steps, New project and the review screen, as the [quickstart](https://deploywithox.com/docs/quickstart) shows. When the deploy is green, open the address ox shows for the project. The first load takes a moment. Log in with `ADMIN_NICKNAME` and `ADMIN_PASSWORD` in the form on the front page. Type a status in the box at the top left, press the upload button to attach a picture, and press Post. The post and its picture show on your profile after a reload, and the test agent also read them without signing in. The run served Pleroma on that preview address only. A domain of your own was not part of it, and the [domains page](https://deploywithox.com/docs/operate/domains) covers how ox adds one. ## The ox.toml, line by line TOML ```toml # Pleroma 2.10.2 (Elixir, a Fediverse server) on ox, as its OTP install guide # and Dockerfile run it: a mix release, migrated with pleroma_ctl, started # with `pleroma start`. Its runtime settings live in config/ox.exs, which the # release reads at start through PLEROMA_CONFIG_PATH; every value in it comes # from a variable or from ox. # The Dockerfile's packages by their Ubuntu names: libvips-dev for vix, built # against the system libvips (VIX_COMPILATION_MODE below), libmagic-dev for # majic, and the media tools its runtime image adds (exiftool, ffmpeg). packages = ["libvips-dev", "libmagic-dev", "ffmpeg", "libimage-exiftool-perl"] required_variables = ["SECRET_KEY_BASE", "SIGNING_SALT", "LIVE_VIEW_SIGNING_SALT", "JWT_SECRET", "ADMIN_EMAIL", "ADMIN_NICKNAME", "ADMIN_PASSWORD"] [tools] # Pleroma's CI tests this release on Elixir 1.18.3 with OTP 27, the OTP ox runs. elixir = "1.18.3-otp-27" [app] # RELEASE_DISTRIBUTION=none: rel/env.sh.eex names every node # pleroma@127.0.0.1, so a new release could not start beside the running one. start = "export PLEROMA_CONFIG_PATH=\"$PWD/config/ox.exs\" RELEASE_TMP=/tmp RELEASE_DISTRIBUTION=none; exec _build/prod/rel/pleroma/bin/pleroma start" health = "/api/v1/instance" [build] commands = [ # The release refuses a config file other users can read or anyone else can write. "chmod o=,g-w config/ox.exs", "MIX_ENV=prod VIX_COMPILATION_MODE=PLATFORM_PROVIDED_LIBVIPS mix release", ] # The Dockerfile's entrypoint migrates before the start. Then the first admin, # made once with Pleroma's own task (docs: `pleroma_ctl user new ... --admin`) # from the ADMIN_* variables: the lookup and the task share one VM, so an # error in the lookup stops the deploy instead of making the user again. The # task echoes the password it was given; the sed cuts it from the log. migrate = """ export PLEROMA_CONFIG_PATH="$PWD/config/ox.exs" RELEASE_TMP=/tmp RELEASE_DISTRIBUTION=none _build/prod/rel/pleroma/bin/pleroma_ctl migrate _build/prod/rel/pleroma/bin/pleroma eval ' nick = System.fetch_env!("ADMIN_NICKNAME") Mix.Pleroma.start_pleroma() if Pleroma.User.get_by_nickname(nick) do IO.puts("admin #{nick} exists") else Pleroma.ReleaseTasks.run(["user", "new", nick, System.fetch_env!("ADMIN_EMAIL"), "--password", System.fetch_env!("ADMIN_PASSWORD"), "--admin", "-y"]) end ' 2>&1 | sed 's/- password:.*/- password: (not shown)/' """ [services] # priv/repo/migrations create citext, pg_trgm and uuid-ossp. postgres = { extensions = ["citext", "pg_trgm", "uuid-ossp"] } [storage] # uploads: Pleroma.Uploaders.Local; instance: the static_dir (custom emoji, # frontends installed with `pleroma_ctl frontend install`, robots.txt). keep = ["uploads", "instance"] ``` - `packages` adds four Ubuntu packages: `libvips-dev` for `vix`, which reads uploaded pictures, `libmagic-dev` for `majic`, and the media tools `ffmpeg` and `libimage-exiftool-perl` from Pleroma's docs. - `required_variables` lists seven keys, four secrets and the three admin values. ox asks for them before the first deploy starts. - `[tools]` pins Elixir 1.18.3 on OTP 27, one of the two pairs Pleroma's CI builds on. - There is no `install` line. ox reads `mix.exs`, gets Hex and Rebar, and fetches the production dependencies. - `[build]` runs `chmod o=,g-w config/ox.exs`, because Pleroma's release refuses a config file that others can read or the group can write, then runs `mix release`. `VIX_COMPILATION_MODE`, which Pleroma's Dockerfile also sets, builds `vix` against the system libvips. - `start` points `PLEROMA_CONFIG_PATH` at `config/ox.exs` and runs `pleroma start` in the foreground. `RELEASE_DISTRIBUTION=none` is there because `rel/env.sh.eex` gives every node the same name, which would stop a new release from starting beside the running one. - `health` is `/api/v1/instance`, a Mastodon API route. ox switches traffic only after it answers. - `migrate` runs `pleroma_ctl migrate`, as Pleroma's Docker entrypoint does. Then one `pleroma eval` runs Pleroma's own `user new` task with `--admin`, only when the admin nickname is missing. The task prints the password, so `sed` blanks that line in the log. - `[services]` asks for Postgres with the `citext`, `pg_trgm` and `uuid-ossp` extensions, which Pleroma's migrations need and which take superuser rights to create. - `[storage]` keeps `uploads`, where attached pictures go, and `instance`, Pleroma's static folder for custom emoji, front ends and `robots.txt`. There are no workers. Pleroma runs its job queue, Oban, inside the app process. `config/ox.exs` Code ``` import Config # Pleroma's runtime configuration on ox: the file `pleroma_ctl instance gen` # writes (priv/templates/sample_config.eex), with every value read from the # environment. Pleroma.Config.ReleaseRuntimeProvider loads it at start # through PLEROMA_CONFIG_PATH. No secret lives in this file. config :pleroma, Pleroma.Web.Endpoint, # The address ox serves the release on (https and 443, or http and 80). url: [ host: System.fetch_env!("PUBLIC_HOST"), scheme: System.fetch_env!("PUBLIC_SCHEME"), port: String.to_integer(System.fetch_env!("PUBLIC_PORT")) ], http: [ip: {127, 0, 0, 1}, port: String.to_integer(System.get_env("PORT", "4000"))], secret_key_base: System.fetch_env!("SECRET_KEY_BASE"), live_view: [signing_salt: System.fetch_env!("LIVE_VIEW_SIGNING_SALT")], signing_salt: System.fetch_env!("SIGNING_SALT") config :pleroma, :instance, name: System.get_env("INSTANCE_NAME", "Pleroma"), email: System.fetch_env!("ADMIN_EMAIL"), notify_email: System.get_env("NOTIFY_EMAIL", System.fetch_env!("ADMIN_EMAIL")), limit: 5000, registrations_open: System.get_env("REGISTRATIONS_OPEN", "true") == "true" config :pleroma, :media_proxy, enabled: false, redirect_on_failure: true config :pleroma, Pleroma.Repo, adapter: Ecto.Adapters.Postgres, url: System.fetch_env!("DATABASE_URL") config :pleroma, :database, rum_enabled: false # Both relative to the release folder, which keeps them ([storage] keep). config :pleroma, :instance, static_dir: "instance/static/" config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads" config :joken, default_signer: System.fetch_env!("JWT_SECRET") config :pleroma, configurable_from_database: false # instance gen's default with exiftool installed: strip GPS data from uploaded # images and prefill descriptions from the files. config :pleroma, Pleroma.Upload, filters: [Pleroma.Upload.Filter.Exiftool.StripLocation, Pleroma.Upload.Filter.Exiftool.ReadDescription] # PromEx's metrics server listens on a fixed port (4021), which a second # release started beside the first could not take. config :pleroma, Pleroma.PromEx, metrics_server: :disabled # Outgoing mail (password resets, admin notices) when SMTP_HOST names a relay. if smtp_host = System.get_env("SMTP_HOST") do smtp_port = String.to_integer(System.get_env("SMTP_PORT", "587")) smtp_user = System.get_env("SMTP_USERNAME") config :pleroma, Pleroma.Emails.Mailer, enabled: true, adapter: Swoosh.Adapters.SMTP, relay: smtp_host, port: smtp_port, username: smtp_user, password: System.get_env("SMTP_PASSWORD"), ssl: smtp_port == 465, tls: if(smtp_port == 465, do: :never, else: :if_available), auth: if(smtp_user, do: :always, else: :never), retries: 2 end ``` This file has the shape of the one `pleroma_ctl instance gen` writes, with every value read from the environment, so no secret sits in the repo. The web address comes from `PUBLIC_HOST`, `PUBLIC_SCHEME` and `PUBLIC_PORT`, which ox sets, and `DATABASE_URL` comes from the Postgres service. A few settings are off: the media proxy, the RUM search index, PromEx's metrics server, which listens on fixed port 4021 and so could not run in two releases at once, and `configurable_from_database`, the config generator's default answer. Shell ```bash SECRET_KEY_BASE SIGNING_SALT LIVE_VIEW_SIGNING_SALT JWT_SECRET ADMIN_NICKNAME ADMIN_EMAIL ADMIN_PASSWORD SMTP_HOST SMTP_PORT ``` `SECRET_KEY_BASE` and the two salts are the web endpoint's signing values, and `JWT_SECRET` signs Pleroma's JSON web tokens. `ADMIN_NICKNAME`, `ADMIN_EMAIL` and `ADMIN_PASSWORD` are the first admin, and the email is also the instance's contact address. The run used the nickname `owner`. `SMTP_HOST` and `SMTP_PORT` sent mail to a test catcher, so no real mail went out. The file also reads `INSTANCE_NAME`, `NOTIFY_EMAIL`, `REGISTRATIONS_OPEN`, `SMTP_USERNAME` and `SMTP_PASSWORD`, which the run left unset. The [variables page](https://deploywithox.com/docs/operate/variables) covers how values work. ## The service and the gotchas Postgres holds accounts, statuses and the job queue's table. ox ran one unit for this project, the app, and it showed no restarts when the test agent checked it twice, minutes apart. The admin panel's Settings page asks for `configurable_from_database`, which this setup leaves off. The browser's network tab shows two 404s, for `styles.custom.json` and `stickers.json`: Pleroma's front end asks for those optional files and ships without them. A fresh server has no remote posts, so the whole known network timeline stays empty until someone follows an account elsewhere. ## Updates and backups Pleroma's updating page says to read the release notes first. To move to a new release, merge its tag into your copy and push, or run `ox deploy pleroma --wait`. ox builds the release beside the running one, runs the migrate step, and switches once the health path answers. Before each migrate it dumps Postgres, as the run's redeploy did. The [rollback page](https://deploywithox.com/docs/operate/rollback) covers going back. Backups need no setup. The run's backup folder held two Postgres dumps, one from Back up now and one from before the migrate. Run `ox backups pleroma` to list them, or `ox backups pleroma now postgres --wait` to take one. The test agent checked only the dumps, so a backup of the `uploads` and `instance` folders was not tested, and neither was a restore. The [backups page](https://deploywithox.com/docs/operate/backups) covers keeping copies in an S3 bucket. ## Frequently asked questions ### Do I need Docker to self-host Pleroma? No. Pleroma is an Elixir release, and its install guide for Ubuntu unpacks that release on the host beside Postgres and a web server. ox does the same job from your repo copy: it compiles the release on your server and runs it as a system service with Postgres next to it. ### How do I create the first Pleroma admin? Set ADMIN_NICKNAME, ADMIN_EMAIL and ADMIN_PASSWORD before the first deploy. The migrate step runs Pleroma's user task with the admin flag, but only when that nickname does not exist yet, so later deploys leave it alone. Then log in on the front page. Pleroma's docs show the same task as pleroma_ctl user new. ### Which database does Pleroma need? PostgreSQL. Pleroma's migrations need the citext, pg_trgm and uuid-ossp extensions, which take superuser rights to create, so the ox.toml lists them and ox makes them. The run gave Pleroma only Postgres. The job queue runs inside the app, so there is no separate worker to set up. ### How much memory does self-hosted Pleroma use? After the reboot, the run's whole server used the memory the facts show. That counts Pleroma, Postgres, Caddy and the ox agent, and leaves out the build, which runs only during a deploy. ox takes any server from 1 GB up, and it tells you when memory is why the app stopped. New to running apps on your own server? [How ox self-hosts your apps](https://deploywithox.com/self-hosting) shows what ox sets up on your server. On this page - [What you need](https://deploywithox.com/blog/self-host-pleroma#what-you-need) - [The steps](https://deploywithox.com/blog/self-host-pleroma#steps) - [The ox.toml, line by line](https://deploywithox.com/blog/self-host-pleroma#config) - [The service and the gotchas](https://deploywithox.com/blog/self-host-pleroma#services) - [Updates and backups](https://deploywithox.com/blog/self-host-pleroma#updates) - [Frequently asked questions](https://deploywithox.com/blog/self-host-pleroma#faq) - [Do I need Docker to self-host Pleroma?](https://deploywithox.com/blog/self-host-pleroma#faq-docker) - [How do I create the first Pleroma admin?](https://deploywithox.com/blog/self-host-pleroma#faq-admin) - [Which database does Pleroma need?](https://deploywithox.com/blog/self-host-pleroma#faq-database) - [How much memory does self-hosted Pleroma use?](https://deploywithox.com/blog/self-host-pleroma#faq-memory) Written by Saurav Sharma Builds ox. Writes about running apps on servers you own, without Docker and without a platform bill. [Older post How to self-host pretalx on your own Ubuntu VPS, no Docker](https://deploywithox.com/blog/self-host-pretalx) [Newer post How to self-host Plausible Analytics on your own VPS](https://deploywithox.com/blog/self-host-plausible) ## Keep reading - Guides [Self-Hosted Open-Source Apps Deployed with ox: All 85](https://deploywithox.com/blog/self-hosted-open-source-apps-deployed-with-ox) ox deployed 85 open-source apps from pinned commits on fresh servers, with no Docker. The stacks, services, deploy times and a table of every app. Saurav Sharma Oct 17, 2026 16 min read - Guides [How to self-host AFFiNE on your own VPS without Docker](https://deploywithox.com/blog/self-host-affine) Run AFFiNE v0.27.4 on your own Ubuntu server with ox: Postgres with pgvector, Redis, a Rust and Node build, and an admin made from two variables. Saurav Sharma Oct 17, 2026 12 min read - Guides [How to self-host Akaunting on your own VPS without Docker](https://deploywithox.com/blog/self-host-akaunting) Run Akaunting 3.2.4 on your own Ubuntu server with ox: PHP, MariaDB, a scheduler and the first company and admin made on deploy, from the config that ran. Saurav Sharma Oct 17, 2026 10 min read ## Deploy to a server you own ox sets up systemd, Caddy and PostgreSQL on your Ubuntu server and deploys on every push. Free for your first project. [Sign up with GitHub](https://deploywithox.com/login/github) [Read the docs](https://deploywithox.com/docs)