A forum, built on Meith.
This board ships two deploy shapes, and you pick one on the Coolify resource by which compose file it points at:
- Prebuilt (default, below). CI builds the image and the server only ever
pulls it — nothing heavy runs on the deploy host, so it suits a small VPS
and deploys fast. It costs one setup step: a
MEITH_IMAGEvalue to paste. This isdocker-compose.yaml+Dockerfile+.github/workflows/build.yml. - Quick start. Coolify builds the image from source, so there is no
registry, no build workflow and no
MEITH_IMAGE— push, point Coolify atdocker-compose.quickstart.yaml, deploy. The trade is a heavier build on the server (a 2 GB VPS can OOM on a Next.js build), so it is best on a box with headroom. This isdocker-compose.quickstart.yaml+Dockerfile.quickstart.
The rest of this section covers the prebuilt path. Nothing here builds on your
own server — a 2 GB VPS OOMs on a Next.js build, which is the whole reason
Dockerfile, docker-compose.yaml and .github/workflows/build.yml exist:
something else builds the image, the server only ever pulls one. Three steps,
nothing to configure by hand beyond one value only you know:
-
Push this repository to GitHub.
.github/workflows/build.ymlbuildsDockerfileon every push tomainand pushes the result to your own GitHub Container Registry,ghcr.io/<you>/meith-board— using only theGITHUB_TOKENevery GitHub Actions run already carries. No secret to add, no registry account beyond the GitHub account you already have.That build is the thing step 2 waits on: open the repository's Actions tab and let the run finish, because its Summary is where the exact image to paste into step 2 comes from. The Summary also links the package itself, to check it is public — a build from a public repository usually lands public already, and a private one fails Coolify's pull with an authentication error no operator can act on.
-
Point Coolify at
docker-compose.yaml— a Public Git repository resource with Docker Compose as its build pack, this repository as its source. The name is Coolify's own default, so its Compose file field is already right when the form opens, and the file already carries Coolify's own "magic variables" forAUTH_SECRET,TICK_SECRETand the database password, generated on the first deploy and never typed in. The one thing Coolify cannot generate is the image step 1 just pushed: setMEITH_IMAGEin the resource's own environment to one of the two values that run's Summary printed (docker-compose.yamlrefuses to start without it, with a message saying why).ghcr.io/<you>/meith-board:${{ github.sha }}names that one build and nothing else, ever;ghcr.io/<you>/meith-board:latestfollowsmaininstead, so installing a plugin later is a push and a Redeploy — the trade the quickstart takes, at the cost of an unrelated redeploy pulling whatevermainmost recently built. -
Deploy, then
/installon your own domain. Coolify issues the certificate; the installer from there is the one docs/getting-started/deployment/coolify.md walks through, screen for screen. It seals itself when it finishes, and/installanswers 404 from then on — run it against the database you are going to keep. Every push tomainafter this rebuilds the image; Coolify's own Redeploy button is what actually pulls it — pushing alone does not.
No Docker Hub, no paid CI: GitHub Actions' free tier and GHCR are the whole build side of this, for a board of any size.
Building it yourself: works on any machine with Docker, if you would
rather not use GitHub Actions for the build — push the result wherever
docker-compose.yaml's MEITH_IMAGE can reach.
docker build --build-arg MEITH_VERSION=$(node -p "require('./package.json').dependencies['@meith/web']") -t meith-board .Without a panel: docs/getting-started/deployment/docker-compose.md
is the same four containers by hand — your own .env, a reverse proxy you
already run, no Coolify. Dockerfile and docker-compose.yaml here are this
board's own version of exactly that shape.
Two things nothing configures for you:
- Mail. Until
MAIL_DRIVERand its three settings exist, every message is written to the log and delivered to nobody, so password reset fails silently. - The tick.
docker-compose.yaml'sworkerservice drives it here — a small loop calling/api/system/tickonce a minute, since@meith/web's own worker package is not something a board outside the meith monorepo can depend on yet. Deploy some other way and something still has to call that route (or runcommunity task:run) every minute, or nothing catches up and nothing errors.
npm install
npm run devNo environment file, no database: with no DATABASE_URL the board serves
deterministic in-memory sample data, which is enough to click through every
reading surface.
Posting needs Postgres. Copy .env.example to .env.local, set
DATABASE_URL and the two secrets in it, then:
npm run community -- migrate
echo "<password>" | npm run community -- user:create --username <name> --email <address> --group administratorsmeith.config.ts— installed themes and plugins. Everything installable is named here so the bundler can see it; nothing is found by scanning a directory at runtime./admin— settings, forums, groups, members, themes, maintenance. An administrator re-enters their password to get in, and again for anything destructive.npm run community -- --help— the operator CLI. Everything the panel does and a few things it cannot, without a browser.
npm install --save-exact @meith/web@latest @meith/cli@latest @meith/theme-default@latest
npm install --save-exact next@$(node -p "require('./node_modules/@meith/web/package.json').dependencies.next")
git commit -am "Upgrade Meith and the Next.js version it builds with"
git pushThe second command is not optional. This board pins next itself, and
nothing bumps it for you: upgrading only the @meith/* packages leaves the
board's own pin on the old Next while @meith/web depends on the new one,
which npm resolves by installing both — the build then runs on one version
while everything reading package.json sees the other. Reading the version
out of the freshly installed @meith/web is what keeps the two the same
without anybody having to know the number.
This upgrade is deliberate and manual for that reason: next and
@meith/web move together or not at all. What is kept current for you is
this repository's own GitHub Actions — .github/dependabot.yml opens a
weekly pull request bumping the actions pinned in
.github/workflows/build.yml, which is a safe, independent update the two
commands above never touch.
That package.json change is the whole pin: Dockerfile's own
FROM line takes the version as a build argument, and
.github/workflows/build.yml reads it straight out of package.json's
own @meith/web dependency when it rebuilds — nothing in Dockerfile
itself to keep in sync by hand. --save-exact matters: npm's default
save-prefix is ^, and a caret range is not a legal Docker image tag —
without it, this exact command would write "^0.18.0" and the next build
would fail with invalid reference format instead of building. This
project's own .npmrc sets save-exact=true for the same reason, so an
npm install of anything else here — a plugin, say — stays pinned too; the
build workflow also refuses to build from anything but an exact version, as
a second line of defense. Once the rebuilt image is deployed, run
npm run community -- upgrade against it for the plugin migrations — see
the operator CLI
for running it against this deployment.
Migrations are forward-only. Recovery is by restore, so take a backup first — there is no down migration to undo a destructive one, and a button that pretended otherwise would be worse than its absence.