> For the complete documentation index, see [llms.txt](https://docs.membercounter.app/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.membercounter.app/self-hosting/self-hosting.md).

# Self-hosting the bot

Run your own copy of Member Counter with Docker, on your own machine or your own server.

A self-hosted bot is a copy of Member Counter that you run yourself. It logs in with your own Discord bot token and stores its data on your machine. It also serves its own dashboard at `http://localhost:3000`.

It has every feature: channels update every 5 minutes and every counter works, as long as you turn on the privileged intents in the Discord Developer Portal.

## Self-hosting or a custom bot?

Self-hosting and a [custom bot](/custom-bot/custom-bot.md) give you the same bot. The difference is who runs the machine.

<table><thead><tr><th width="180">Self-hosted bot</th><th>Custom bot</th></tr></thead><tbody><tr><td>Free. You only pay for the hardware.</td><td>Paid, per quota unit per quarter.</td></tr><tr><td>You install, update and back it up.</td><td>We install, update and back it up.</td></tr><tr><td>Full control over your data and over every setting in <code>.env</code>.</td><td>Control panel with the settings that matter, on our hosting.</td></tr><tr><td>Runs as long as your machine is on.</td><td>Runs 24/7 on our infrastructure.</td></tr><tr><td>No official support beyond this guide.</td><td>Support from the team.</td></tr></tbody></table>

If you want the same result without the maintenance, read [How to get a custom bot](/custom-bot/how-to-get-a-custom-bot.md).

## Requirements

You will type commands in a terminal. Read this guide top to bottom before you start: every step depends on the one above it.

<table><thead><tr><th width="220">Software</th><th>Notes</th></tr></thead><tbody><tr><td>Docker, with Docker Compose v2.23.1 or newer</td><td><a href="https://docs.docker.com/engine/install/">Install Docker</a>. Check your version with <code>docker compose version</code>. Older versions cannot read the compose file.</td></tr><tr><td>git</td><td><a href="https://git-scm.com/download">Install git</a>.</td></tr><tr><td>A CPU with AVX support</td><td>The database runs <code>mongo:7</code>, and MongoDB 5.0 and newer need AVX. Older processors lack it, and some VMs and NAS boxes hide it from their guests. On Linux, check with <code>grep -o avx /proc/cpuinfo | head -1</code>.</td></tr></tbody></table>

{% hint style="warning" %}
Without AVX the `mongo` container exits the moment it starts, and the bot and the backend keep failing to reach the database.
{% endhint %}

## 1. Create your Discord application

Your bot needs its own application in the Discord Developer Portal. The steps are the same as for a custom bot:

{% content-ref url="/pages/Fx7QIcY4vrBPVjlfegeZ" %}
[Creating a Discord bot account](/custom-bot/creating-a-discord-bot-account.md)
{% endcontent-ref %}

Come back with these four things:

* the **bot token**
* the **client ID**, on the OAuth2 tab
* the **client secret**
* a redirect URL added and saved on the OAuth2 tab: `http://localhost:3000/api/auth/callback`

{% hint style="warning" %}
Turn on the **PRESENCE INTENT** and the **SERVER MEMBERS INTENT** in the Developer Portal. Without both, your bot cannot log in. **MESSAGE CONTENT INTENT** is not used and should stay off.
{% endhint %}

## 2. Download Member Counter

{% tabs %}
{% tab title="HTTPS" %}

```bash
git clone --depth 1 -b master https://github.com/member-counter/bot.git
```

{% endtab %}

{% tab title="SSH" %}

```bash
git clone --depth 1 -b master git@github.com:member-counter/bot.git
```

{% endtab %}
{% endtabs %}

Then open the folder it created:

```bash
cd bot
```

{% hint style="info" %}
`-b master` matters: the repository's default branch is `dev`, which runs beta images that break more often.
{% endhint %}

## 3. Create your `.env` file

Every setting lives in one file. Copy the example:

```bash
cp .env.example .env
```

Open the new `.env` with any text editor. You might have to turn on hidden files in your file explorer to see it.

```bash
COOKIE_SECRET='8F2k9QvR7pLxW3mN5sT1yB6cH4dJ0zA2'
DISCORD_CLIENT_ID='1234567890123456789'
DISCORD_CLIENT_SECRET='aB3dE5fG7hJ9kL1mN3pQ5rS7tU9vW1xY'
DISCORD_BOT_INSTANCE_TOKEN='MTIzNDU2Nzg5MDEyMzQ1Njc4OQ.GaBcDe.FaKeToKeNfAkEtOkEnFaKeToKeN12'
WEBSITE_URL="http://localhost:3000"
DISCORD_OAUTH2_REDIRECT_URI='http://localhost:3000/api/auth/callback'
```

### The variables you must fill in

The file starts with the block of variables you have to fill in yourself. Set these six:

<table><thead><tr><th width="270">Variable</th><th>What to put in it</th></tr></thead><tbody><tr><td><code>COOKIE_SECRET</code></td><td>A random string of <strong>at least 32 characters</strong>. On macOS or Linux, <code>openssl rand -base64 32</code> generates one. Shorter values are rejected on start-up.</td></tr><tr><td><code>DISCORD_CLIENT_ID</code></td><td>Your application's client ID.</td></tr><tr><td><code>DISCORD_CLIENT_SECRET</code></td><td>Your application's client secret.</td></tr><tr><td><code>DISCORD_BOT_INSTANCE_TOKEN</code></td><td>Your bot token.</td></tr><tr><td><code>WEBSITE_URL</code></td><td>Where you will open the dashboard. Leave <code>http://localhost:3000</code> unless you put the bot behind a reverse proxy or a domain. <strong>Never add a trailing slash</strong>.</td></tr><tr><td><code>DISCORD_OAUTH2_REDIRECT_URI</code></td><td>Must match the redirect URL you saved in the Developer Portal character for character. Leave <code>http://localhost:3000/api/auth/callback</code> if you kept the default.</td></tr></tbody></table>

### Two more you should change

These sit in the second block, the one marked as not needing changes. They have values already, but those defaults are written for the public bot.

<table><thead><tr><th width="270">Variable</th><th>Why it matters</th></tr></thead><tbody><tr><td><code>PUBLIC_BOTS_IDS</code></td><td>Ships as <code>'["main", "premium", "beta"]'</code>. Your install runs a single bot with the id <code>main</code>, so the other two ids make your dashboard show two bots that are permanently offline. Set it to <code>PUBLIC_BOTS_IDS='["main"]'</code>.</td></tr><tr><td><code>MAIN_DISCORD_CLIENT_ID</code></td><td>Ships as <code>478567255198662656</code>, the official Member Counter application. <a href="/commands/invite.md">/invite</a> builds its link from this value, so until you change it, <code>/invite</code> on your own bot invites ours. Set it to the same value as your <code>DISCORD_CLIENT_ID</code>.</td></tr></tbody></table>

Everything else already works for a single bot on one machine. For extra counters (YouTube, Twitch, Memerator, currency rates), a custom domain, a reverse proxy or sharding, see the [configuration reference](/self-hosting/configuration.md).

## 4. Prepare the database

Start only the database services:

```bash
docker compose up -d mongo setup-mongo-rs redis
```

{% hint style="info" %}
`setup-mongo-rs` runs once and exits. Seeing it as `Exited (0)` in `docker compose ps -a` is the healthy outcome, not a failure.
{% endhint %}

Then create the collections and indexes, and apply the schema:

```bash
docker compose run --rm -T db-utils npm run db:migrate up
docker compose run --rm -T db-utils npm run db:push
```

Both commands print their progress and exit.

## 5. Start the bot for the first time

Start everything, keeping the bot's and the backend's output on screen:

```bash
docker compose up --attach bot --attach backend
```

A healthy first start ends with these lines:

```log
bot     | 2026-09-19T12:00:08.431Z - info - [bot: main (0)] - Bot ready
bot     | 2026-09-19T12:00:08.433Z - info - [bot: main (0)] - Logged in as My Counter#7391
bot     | 2026-09-19T12:00:08.434Z - info - [bot: main (0)] - Invite link: https://discord.com/oauth2/authorize?client_id=1234567890123456789&permissions=269569044&scope=bot
```

Keep the invite link for the next step. The `warn` line about `OPENEXCHANGERATESORG_KEY` is harmless.

{% hint style="info" %}
On a cold start the bot and the backend can print database connection errors and restart a few times, because they start before MongoDB is ready and recover on their own.
{% endhint %}

If the log keeps failing, read the error and check the previous steps:

| Error                   | Fix                                          |
| ----------------------- | -------------------------------------------- |
| Disallowed intents      | Turn on both privileged intents in step 1.   |
| Invalid token           | Check `DISCORD_BOT_INSTANCE_TOKEN`.          |
| Cookie secret too short | Make `COOKIE_SECRET` at least 32 characters. |

When the log looks right, stop it with <kbd>Ctrl</kbd> + <kbd>C</kbd> and start it again in the background, so it survives closing the terminal:

```bash
docker compose up -d
```

## 6. Invite your bot and open the dashboard

Open the invite link from the log in a browser and add the bot to your server. The link already carries the permissions the bot needs.

Then open your dashboard at <http://localhost:3000> and sign in with Discord.

<figure><img src="https://1582170640-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lu2S5r7lKtu4IK5ucu9%2Fuploads%2Fgit-blob-30365444130f1744a4dd29873b91c06a8e2436c7%2Fself-host-dashboard-first-login.png?alt=media" alt="The dashboard server picker after signing in with Discord"><figcaption><p>Your servers appear in the picker on the left</p></figcaption></figure>

From here the bot works like any other install. Continue with [Quick setup](/getting-started/quick-setup.md).

## Day-to-day commands

Run all of these from the `bot` folder.

<table><thead><tr><th width="330">Command</th><th>What it does</th></tr></thead><tbody><tr><td><code>docker compose ps</code></td><td>Shows which services are running.</td></tr><tr><td><code>docker compose logs -f bot</code></td><td>Follows the bot's log. Swap <code>bot</code> for <code>backend</code>, <code>frontend</code> or <code>website</code>.</td></tr><tr><td><code>docker compose restart bot</code></td><td>Restarts one service. It does <strong>not</strong> pick up <code>.env</code> changes; use <code>docker compose up -d</code> for that.</td></tr><tr><td><code>docker compose up -d</code></td><td>Starts everything in the background, and applies <code>.env</code> changes.</td></tr><tr><td><code>docker compose down</code></td><td>Stops and removes the containers.</td></tr></tbody></table>

{% hint style="success" %}
`docker compose down` does not delete your data. The database lives in the `storage/db` folder next to the compose file.
{% endhint %}

## Back up your data

Back up two things; everything else can be rebuilt:

* `storage/db` holds your whole database: server settings, templates and channel settings.
* `.env` holds your secrets and settings.

Run `docker compose down` before you copy `storage/db`, so you copy a database that nothing is writing to. To restore, put both back in place and run `docker compose up -d`. Redis is only a cache, so it needs no backup.

New versions ship as new container images. See [Updating a self-hosted bot](/self-hosting/updating-a-self-hosted-bot.md).

## Getting help

Self-hosting is not covered by official support. Ask the community in #support on the [support server](https://membercounter.app/support). Bring the output of `docker compose logs --tail 50 bot`, and never paste your bot token. To report a bug or improve the bot itself, see [How to contribute](/contributing/how-to-contribute.md).

<details>

<summary>Technical details</summary>

* The v2.23.1 floor comes from the inline `configs:` entry that carries the nginx configuration; older versions, including the standalone `docker-compose` v1, fail to parse it.
* Branch and image tags: pushes to `master` build the `:latest` images, pushes to `dev` build the `:beta` images, and the compose file on each branch pins the matching tags. The git submodules are only needed to build the dashboard image yourself.
* Update interval: the channel update job runs on the cron `0 */5 * * * *` when the instance is premium and `0 */10 * * * *` when it is not. `DISCORD_BOT_INSTANCE_IS_PREMIUM` ships as `true`.
* Intents: GuildMembers and GuildPresences are requested only when `DISCORD_BOT_INSTANCE_IS_PRIVILEGED` and `DISCORD_BOT_INSTANCE_IS_PREMIUM` are both true, and GuildVoiceStates when the second is. With the portal toggles off, discord.js stops with `Privileged intent provided is not enabled or whitelisted.`
* Every service validates `.env` on start-up, prints `❌ Invalid environment variables:` with the failing keys and exits with `Invalid environment variables`, so a bad value shows up as a restart loop. `COOKIE_SECRET` signs the dashboard session cookies; `WEBSITE_URL` is the base URL and the allowed browser origin, which is why a trailing slash breaks it.
* Invite link: `permissions=269569044` is ViewChannel, ManageChannels, Connect, SendMessages, ReadMessageHistory, EmbedLinks, ManageRoles and BanMembers.
* The `deployCommands:` lines on every start are normal: the bot registers its slash commands before logging in, because `DISCORD_BOT_INSTANCE_DEPLOY_COMMANDS` ships as `true`.
* `OPENEXCHANGERATESORG_KEY` ships empty, so the log shows `OPENEXCHANGERATESORG_KEY is not set, all rates will be x1` and every rate stays at 1. No counter uses it; it only affects currency conversion on the donors and donations pages. The optional counter keys are `YOUTUBE_API_KEY`, `TWITCH_CLIENT_ID` with `TWITCH_CLIENT_SECRET`, and `MEMERATOR_API_KEY`.
* Log lines are `timestamp - level - [meta] - message`, and the bot's meta is `[bot: <DISCORD_BOT_INSTANCE_ID> (<DISCORD_BOT_INSTANCE_CHILD_ID>)]`. `LOG_LEVEL` takes `error`, `warn`, `info` (the default) or `debug`; set it in `.env` and run `docker compose up -d` again.
* Database: `mongo:7` runs with `--replSet rs0` and the bind mount `./storage/db:/data/db`; `setup-mongo-rs` runs `rs.initiate` with `restart: "no"` and is the only service that waits for MongoDB's healthcheck, which is why the bot and the backend can start first. `db:migrate up` runs mongo-migrate-ts and records what it applied in the `migrations_changelog` collection; `db:push` runs `prisma db push`.
* Only `website` publishes a port, `3000:80`. It is `nginx:latest`, configured from the inline compose config: it rewrites `/api/(.*)` to `backend:8080/$1` and serves everything else from `frontend`. Behind another proxy, raise `BACKEND_MAX_TRUSTED_PROXIES`; its default `1` covers the bundled nginx only. `rest-proxy` and `db-utils` ship with `deploy.replicas: 0`, and the proxy only matters when several bot processes share one token: scale it to 1 and set `DISCORD_BOT_INSTANCE_REST_PROXY_URL=http://rest-proxy:8765/api`.
* The dashboard image is a static build, so `VITE_SUPPORT_URL`, `VITE_BOT_DOCS_URL`, `VITE_BOT_REPO_URL`, `VITE_TRANSLATION_PLATFORM_URL` and `SITE_URL` are baked in when the image is built and editing them in `.env` changes nothing. `VITE_SUPPORT_URL` and `VITE_BOT_REPO_URL` must stay in `.env` anyway, because the bot requires them; `SITE_URL` ships commented out and is only read by the frontend build.

</details>
