> 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/updating-a-self-hosted-bot.md).

# Updating a self-hosted bot

Back up, pull the new images, run the database migrations, start again.

New versions of Member Counter ship as new container images.

Run the steps in order, from a terminal in the `bot` folder you created when you [set the bot up](/self-hosting/self-hosting.md). Your counters stop updating while the bot is down.

{% hint style="warning" %}
`git pull` on its own does **not** update your bot. Every service runs a published image, so without `docker compose pull` you recreate the containers from the image you already had.
{% endhint %}

## Update

{% stepper %}
{% step %}

### Stop the bot

```bash
docker compose down
```

This stops and removes the containers. Your database stays on disk in `storage/db`.
{% endstep %}

{% step %}

### Back up `storage/db` and `.env`

Copy both somewhere safe while nothing is running:

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

A migration can fail and leave the database half-updated, and this backup is how you restore it. Redis holds only a cache, so it needs no backup.
{% endstep %}

{% step %}

### Download the new compose file

```bash
git pull
```

This refreshes `docker-compose.yml` and `.env.example`, which is how new services, new web server settings and new variables reach you.
{% endstep %}

{% step %}

### Pull the new images

Run `docker compose images` first and keep the output: those digests are the only way back to the version you have now. Then download the new version of every service:

```bash
docker compose pull
```

{% endstep %}

{% step %}

### Check for new settings

Compare the refreshed example file with your own:

```bash
diff .env.example .env
```

On Windows without `diff`, open both files side by side. Only add variables that appear in `.env.example` and are missing from `.env`; the lines that differ because you filled them in are supposed to differ. The meaning of every variable is in the [configuration reference](/self-hosting/configuration.md).
{% endstep %}

{% step %}

### Start the database services

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

The migrations need the database running, and nothing else.
{% endstep %}

{% step %}

### Apply the database migrations

```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 exit when they are done. If either one fails, stop here and ask in #support on the [support server](https://membercounter.app/support) before starting the bot, so your database stays in a state you can still restore.
{% endstep %}

{% step %}

### Start everything again

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

Check that the bot came back:

```bash
docker compose logs -f bot
```

You are looking for `Bot ready` and `Logged in as`. Press <kbd>Ctrl</kbd> + <kbd>C</kbd> to stop following the log; the bot keeps running.
{% endstep %}

{% step %}

### Clean up the old images

```bash
docker image prune -f
```

Optional, and best left until you are sure the update worked. It removes every untagged image on this machine, not only Member Counter's, and those are the images you would roll back to.
{% endstep %}
{% endstepper %}

## If the update goes wrong

Rolling back takes two parts: the data and the code. Restoring the backup alone is not enough, because the tags in `docker-compose.yml` now point at the new images.

1. Stop everything:

   ```bash
   docker compose down
   ```
2. Put your backed-up `storage/db` and `.env` back in place.
3. Pin the previous images. Find the digest you wrote down before the pull, or list what is still on disk:

   ```bash
   docker image ls --digests --filter=reference="ghcr.io/member-counter/*"
   ```

   Then in `docker-compose.yml`, replace the tag with that digest, for every service you are rolling back:

   ```yaml
   image: ghcr.io/member-counter/bot@sha256:<old digest>
   ```
4. Start again:

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

{% hint style="danger" %}
Skip step 3 and you run the new code against the restored old database. If you already ran `docker image prune -f`, the old images are gone and the only way back is to pull the digest again from the registry.
{% endhint %}

Then bring the failure to #support on the [support server](https://membercounter.app/support) with the output of `docker compose logs --tail 50 bot`. Never paste your bot token. A [custom bot](/custom-bot/custom-bot.md) is the same bot on our hosting, updated for you: see [How to get a custom bot](/custom-bot/how-to-get-a-custom-bot.md).

<details>

<summary>Technical details</summary>

* Nothing in the compose file is built from source: `frontend`, `backend`, `bot`, `rest-proxy` and `db-utils` run `ghcr.io/member-counter/*` images, and the rest run `nginx:latest`, `mongo:7` and `redis`. Pushes to `master` build the `:latest` images and pushes to `dev` build the `:beta` images, and the compose file on each branch pins its own tags, which is what `git pull` keeps in step with the images.
* The tags are pins, not digests, so `docker compose pull` moves them to the new images. The old images stay on disk untagged until they are pruned, and `docker compose up -d` starts the new ones even when you restored an old database.
* `db:migrate up` applies the migrations in `packages/db/src/migrations` and records each one it applied in the `migrations_changelog` collection, so a second run applies only what is missing. `db:push` runs `prisma db push`.
* The `db-utils` image idles on `tail -f /dev/null`, which is why the migrations run through `docker compose run`. `--rm` deletes the one-off container and `-T` drops the TTY, which is what makes both commands safe in a script.
* MongoDB is the only service with storage: the bind mount `./storage/db:/data/db`. Redis declares no volume, so its cache is rebuilt after the update.
* A new variable that you miss shows up as a restart loop: each service validates its environment on start-up, prints `❌ Invalid environment variables:` with the failing keys and exits with `Invalid environment variables`. `docker compose up -d` recreates only the containers whose image, environment or configuration changed, and `docker compose restart` picks up neither a new image nor a new `.env`.
* Counters resume on the next channel update tick, which runs on the cron `0 */5 * * * *` with the default settings. The bot also re-registers its slash commands on every start, because `DISCORD_BOT_INSTANCE_DEPLOY_COMMANDS` ships as `true`.

</details>
