How to self-host Plausible Analytics on your own VPS
Run Plausible Community Edition v3.2.1 on your own Ubuntu server with ox: Postgres, ClickHouse and an Elixir build, from the config that ran.
To self-host Plausible Analytics you need an Ubuntu server, a fork of the Plausible repo with one ox.toml file in it, and five variables. In our run the first deploy took about 14 minutes, most of it the Elixir and asset build, and ox set up Postgres and ClickHouse for it.
How we tested On Oct 17, 2026, ox deployed Plausible Analytics v3.2.1 from commit e4f5a87a 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, added a website, sent it one pageview, and saw the visit on its dashboard after a reload, and checked the app's services and its backup.
Plausible Analytics counts the visitors, pages and sources of your websites without cookies. The company behind it sells a hosted service, and the same code is open source as Plausible Community Edition (CE), under the AGPL. It is an Elixir and Phoenix app. Accounts and site settings live in Postgres, and every pageview goes to ClickHouse, a database made for counting many events fast.
ox deployed Plausible Analytics v3.2.1 (commit e4f5a87a) on Oct 17, 2026 on a fresh DigitalOcean server with 8 vCPUs and 16 GB of memory; the first deploy took 14 minutes and a redeploy took 9 minutes.
- First deploy: 14 minutes, from adding the project until the app passed the run's first checks.
- Redeploy of the same commit: 9 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: 1888 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:
clickhouse,postgres. - Source: github.com/plausible/analytics at v3.2.1, commit
e4f5a87a.



What you need
An Ubuntu LTS server. The run used a DigitalOcean server with 8 vCPUs and 16 GB of memory, the one size every app we deployed ran on, and the facts above show what it measured there. Plausible's community edition notes say ClickHouse needs a CPU with SSE 4.2 or NEON.
A GitHub account to fork
plausible/analytics, and an ox account, which you open by signing in with GitHub.An email address and a password for the first admin. Plausible sends invites and reports by email, but email was not tested.
The steps
In the ox dashboard, press Add a server. It shows one install command with a fresh token in it. Run that on the server as root. It installs only the ox agent and Caddy.
Fork plausible/analytics on GitHub and check out the tag v3.2.1, the release the run deployed. Add the ox.toml and the create_admin.exs file shown below, at the paths shown, then commit and push. Next write a file named plausible.env on your laptop with the five values from the variables section. Plausible's docs make SECRET_KEY_BASE with openssl rand -base64 48, and the run used a random value of that kind. With the ox CLI signed in, run:
ox new <you>/analytics
ox review analytics --from-file plausible.env --waitox names the project after the repo, so it is analytics here. The review saves the values and starts the first deploy, and --wait prints the run until it ends. The same two steps in the browser are New project and the review screen, as the quickstart shows.
When the deploy is green, open the address ox shows for the project and sign in with ADMIN_EMAIL and ADMIN_PASSWORD. Press Add a website, type your site's domain, and Plausible shows the script tag to paste into your site. The run served Plausible 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
# Plausible Community Edition v3.2.1 (Phoenix, postgres, ClickHouse), built
# as its Dockerfile builds it. The Community Edition is the "ce" build
# (MIX_ENV=ce), not the "prod" one Plausible's own cloud runs, so every mix
# command and the release folder name it. Oban jobs and cron run inside the
# web process, so there are no workers.
# runtime.exs raises without BASE_URL and SECRET_KEY_BASE, and its
# CLICKHOUSE_DATABASE_URL default is a docker host name. The Community
# Edition closes sign-up once a user exists (DISABLE_REGISTRATION defaults to
# invite_only), so the first admin comes from ADMIN_EMAIL and ADMIN_PASSWORD.
required_variables = ["BASE_URL", "SECRET_KEY_BASE", "CLICKHOUSE_DATABASE_URL", "ADMIN_EMAIL", "ADMIN_PASSWORD"]
[build]
install = "mix local.hex --force && mix local.rebar --force && MIX_ENV=ce mix deps.get --only ce"
commands = [
"MIX_ENV=ce mix deps.compile",
"npm ci --include=dev --prefix assets",
"npm ci --include=dev --prefix tracker",
"NODE_ENV=production npm run deploy --prefix tracker", # the tracking script sites embed
"MIX_ENV=ce NODE_ENV=production mix assets.deploy", # tailwind, esbuild, phx.digest
"MIX_ENV=ce mix download_country_database", # the free IP to country database
"MIX_ENV=ce mix sentry.package_source_code",
"MIX_ENV=ce mix release plausible",
]
# The image's migrate.sh (both databases in one ordered pass; ox made both
# databases, so its createdb.sh has nothing to do), then the first admin
# from ADMIN_EMAIL, ADMIN_NAME and ADMIN_PASSWORD (rel/overlays/
# create_admin.exs prints whether it made it or found it, and stops the
# deploy on any other answer).
migrate = "_build/ce/rel/plausible/migrate.sh && _build/ce/rel/plausible/bin/plausible eval 'Code.eval_file(Path.join(System.fetch_env!(\"RELEASE_ROOT\"), \"create_admin.exs\"))'"
[app]
# DEFAULT_DATA_DIR is the image's (its VOLUME): CSV imports and exports.
start = "DEFAULT_DATA_DIR=\"$PWD/data\" PHX_SERVER=true RELEASE_DISTRIBUTION=none exec _build/ce/rel/plausible/bin/plausible start"
health = "/api/system/health/live"
[services]
postgres = {}
clickhouse = {}
[storage]
keep = ["data"]Plausible builds two editions from one codebase. Its Dockerfile sets MIX_ENV=ce for the Community Edition, and the config does the same on every mix step, so the release lands in _build/ce/rel/plausible. The other edition is the one Plausible's paid cloud runs.
required_variableslists the five keys the app or the first admin cannot do without. Plausible'sruntime.exsraises withoutBASE_URLandSECRET_KEY_BASE, and its ClickHouse default points at a Docker host name. A missing key stops the deploy in its first seconds, before the long build.installfetches Hex and Rebar, then only the dependencies the CE edition uses.The build
commandsfollow the Dockerfile in order: compile the dependencies, install the npm packages for the dashboard and for the tracker, build the tracking script your sites embed, bundle and digest the assets, download the free IP to country database, package the source for error reports, and build the release.migrateruns the release's ownmigrate.sh, which migrates Postgres and ClickHouse in one pass. Then it runscreate_admin.exsto make the first admin.startruns the release in the foreground.PHX_SERVER=truetells Phoenix to serve HTTP on the port ox gives it,RELEASE_DISTRIBUTION=noneturns off Erlang clustering, andDEFAULT_DATA_DIRis the folder for CSV imports and exports.healthis Plausible's own liveness path. ox switches traffic to a new release only once it answers.[services]asks for Postgres and ClickHouse, and[storage]keeps thedatafolder across deploys.
There are no workers. Plausible runs its Oban jobs and its cron inside the web process, and the test agent saw them working in that process's log.
rel/overlays/create_admin.exs
# The first admin, from ADMIN_EMAIL, ADMIN_NAME (default "Admin") and
# ADMIN_PASSWORD, made the way the register page makes a user
# (Plausible.Auth.User.new, then Repo.insert). Safe on every deploy: it prints
# whether it made the admin or found it, and any other answer (no database,
# a password the app refuses) raises, which stops the deploy.
#
# bin/plausible eval 'Code.eval_file(Path.join(System.fetch_env!("RELEASE_ROOT"), "create_admin.exs"))'
:ok = Application.ensure_loaded(:plausible)
Enum.each([:ssl, :postgrex, :ch, :ecto], &Application.ensure_all_started/1)
{:ok, _} = Plausible.Repo.start_link(pool_size: 2)
email = System.fetch_env!("ADMIN_EMAIL")
name = System.get_env("ADMIN_NAME", "Admin")
password = System.fetch_env!("ADMIN_PASSWORD")
if Plausible.Repo.get_by(Plausible.Auth.User, email: email) do
IO.puts("admin #{email} exists")
else
changeset =
Plausible.Auth.User.new(%{
"email" => email,
"name" => name,
"password" => password,
"password_confirmation" => password
})
case Plausible.Repo.insert(changeset) do
{:ok, user} ->
IO.puts("admin #{user.email} created")
{:error, changeset} ->
errors =
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
raise "the admin #{email} was not created: #{inspect(errors)}"
end
endCE closes sign-up as soon as one user exists: DISABLE_REGISTRATION defaults to invite_only. So the first account cannot wait for a visitor. This file makes it from the variables, the same way Plausible's register page makes a user. It prints whether it made the admin or found one, so it is safe on every deploy, and any other answer stops the deploy.
BASE_URL
SECRET_KEY_BASE
CLICKHOUSE_DATABASE_URL
ADMIN_EMAIL
ADMIN_NAME
ADMIN_PASSWORD
SMTP_HOST_ADDR
SMTP_HOST_PORTBASE_URL is ${PUBLIC_URL}, a reference ox fills in with the project's address, so it follows the address when it changes. CLICKHOUSE_DATABASE_URL is ${CLICKHOUSE_URL}, the address of the ClickHouse service ox set up. SECRET_KEY_BASE signs sessions, and Plausible derives its two-factor key from it unless TOTP_VAULT_KEY is set. ADMIN_EMAIL, ADMIN_NAME and ADMIN_PASSWORD are the first admin. The run pointed SMTP_HOST_ADDR and SMTP_HOST_PORT at a test mail catcher, so it sent no real mail. See variables for how references work.
Postgres and ClickHouse
Postgres is shared with other apps on the server, each with its own database and user. ClickHouse runs only for this project. The run also deployed a staging copy beside production, and the memory figure above counts both. You can look inside each database from the Services tab, or with ox explore analytics postgres.
Plausible drops events sent with a bot or tool User-Agent, so a pageview sent with curl's own User-Agent never shows. The test agent sent its pageview with a browser's, and the dashboard counted it. Events reach ClickHouse in small batches, so a visit takes a few seconds to appear.
Updates and backups
To move to a new release, merge its tag into your fork and push, or run ox deploy analytics --wait. ox builds the new release beside the running one, runs the migrations, and switches only when the new one answers. Before each migrate it dumps Postgres, and the run's redeploy left that dump. Read Plausible's changelog before a major version.
Backups need no setup. The run's backups held a Postgres dump, a ClickHouse archive and the data folder, both daily and from Back up now. Run ox backups analytics to list them. A restore was not tested. The backups page covers keeping copies in an S3 bucket.
Frequently asked questions
Do I need Docker to self-host Plausible?
No. Plausible's own setup uses Docker Compose, but the app is a normal Elixir release. ox builds it from your fork on the server and runs it as a system service, with Postgres and ClickHouse installed beside it. There is no image to pull, and an update is a push to your fork or one deploy command.
How do I make the first Plausible account?
Set ADMIN_EMAIL and ADMIN_PASSWORD before the first deploy. The migrate step runs create_admin.exs, which makes that user, and you sign in at /login. Community Edition then keeps sign-up closed and lets new people in by invite only. Inviting more people needs email, which was not tested.
How much memory does self-hosted Plausible use?
After a reboot, the run's whole server used 1888 MB of its 16 GB. That counts Ubuntu, Plausible and its staging copy, Postgres, ClickHouse, Caddy and the ox agent. The build compiles Elixir and runs npm on the server too. ox takes any server from 1 GB up, and says so when memory is why the app stopped.
Can I change SECRET_KEY_BASE later?
Changing it signs every user out, and Plausible derives its two-factor key from it unless TOTP_VAULT_KEY is set, so codes made with the old key stop working. Set it once before the first deploy and keep it. Our run never changed it, so a rotation of the key was not tested.
New to running apps on your own server? How ox self-hosts your apps shows what ox sets up on your server.
