A forum, built on Meith.
Three paths onto a server, all ending at the same /install. Quick
start onto Coolify is the default and needs nothing
but a push; advanced/prebuilt moves the build off the server, onto
GitHub Actions, for a low-spec build server or a faster deploy; without a
panel is the same four containers run by hand, with your own .env and
reverse proxy, and no Coolify at all. Pick one — a board only ever runs one
of them at a time.
Coolify builds the image itself, from this repository, every time it deploys — there is nothing to push anywhere first and no image tag to paste in. Two steps:
-
Push this repository to GitHub.
-
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. Nothing else to set:docker-compose.yamlbuildswebandmigratefromDockerfileitself, so there is noMEITH_IMAGEhere at all. -
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 is picked up the next time Coolify's own Redeploy button runs — pushing alone does not rebuild it.
The trade for that zero setup is a heavier build: Dockerfile installs this
board's full dependency closure on the server itself, on every deploy, rather
than starting from a warm base image. A 2 GB VPS can OOM on it. If that is
your server, use the advanced path below instead.
A quick-start board never needs Dockerfile.prebuilt,
docker-compose.prebuilt.yaml or .github/workflows/build.yml — delete all
three.
Something else builds the image ahead of time; 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.ymlbuildsDockerfile.prebuilton 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.prebuilt.yaml— a Public Git repository resource with Docker Compose as its build pack, this repository as its source, and its Compose file field changed from Coolify's default ofdocker-compose.yamltodocker-compose.prebuilt.yaml. That file 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.prebuilt.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 this path takes, at the cost of an unrelated redeploy pulling whatevermainmost recently built. -
Deploy, then
/installon your own domain. Same installer, same docs/getting-started/deployment/coolify.md walk-through, same one-time seal. 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.prebuilt.yaml's MEITH_IMAGE can reach.
docker build -f Dockerfile.prebuilt --build-arg MEITH_VERSION=$(node -p "require('./package.json').dependencies['@meith/web']") -t meith-board .docker-compose.byhand.yaml, beside the two Coolify files above, is the same
four containers deployed with nothing generating secrets for you: a .env
you write yourself, a port published for the reverse proxy you already run,
and docker compose up -d --build in place of a panel's Deploy button.
docs/getting-started/deployment/docker-compose.md
is the full walkthrough this file is the last step of, including the
.env this repository does not carry — nothing here belongs in git. Delete
this file if you know you will only ever deploy through Coolify; keep it,
and it needs nothing else changed, if you later want to move away from
Coolify without changing how the board itself is built.
Two things nothing configures for you, on any path:
- 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. The compose file's
workerservice 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 runmeith 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 meith -- migrate
echo "<password>" | npm run meith -- 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 meith -- --help— the operator CLI. Everything the panel does and a few things it cannot, without a browser.
Nothing installs into a running container — a plugin or theme has to be built into the image. In this repository:
-
Add it. A plugin is one command, which installs the package and registers it:
npm run meith -- plugin:add @meith/plugin-dues
It writes
board.plugins.jsonand regeneratesmeith.plugins.tsfor you (npm run meith -- plugin:remove <key>reverses it). A theme isnpm install --save-exact @meith/theme-midnight, then an entry inmeith.config.ts'sthemesmap following the shape of thedefaultone already there — setdefaultThemeto its key to make it the board's default. -
Commit and push, then Redeploy from Coolify — pushing alone does not rebuild. Quick start builds the new image on that redeploy; advanced/prebuilt waits for
.github/workflows/build.ymlto finish first, and Redeploy is what actually pulls the result. -
If it ships database changes, apply them once it is up — from Admin → System (Version & migrations) in the browser, or:
docker compose run --rm web meith upgrade
See Installing plugins and themes for the full guide.
.github/workflows/update.yml does this for you: once a week — and
whenever you press Run workflow on the Actions tab — it checks for a new
Meith release and opens a pull request that moves every @meith/* package
and next together, and rewrites the deploy files this scaffold owns
(Dockerfile, the compose files, the workflows) to the new release's
shape. A file you have edited yourself is never rewritten; the run's log
names any it left for you. One-time setup: under
Settings → Actions → General, enable Allow GitHub Actions to create
and approve pull requests, or the workflow cannot open one.
Merging that pull request is still an upgrade, not a formality: read the
release notes it links, take a backup first, and press Redeploy in
Coolify after the merge — pushing alone does not rebuild. Once the new
version serves, run npm run meith -- upgrade against it for the plugin
migrations.
The same update, by hand and without waiting for the schedule:
npx create-meith@latest update
git commit -am "Update Meith"
git pushUnder the hood, the version move is these two commands, plus the deploy-file rewrite neither of them can do:
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")The second command is not optional. This board pins next itself, and
the npm commands alone never bump it: 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.
next and @meith/web move together or not at all, which is why one
updater owns the whole move and no dependency bot bumps either on its own.
What Dependabot does keep current is this repository's own GitHub Actions —
.github/dependabot.yml opens a weekly pull request bumping the actions
pinned under .github/workflows, a safe, independent update the updater
leaves to it.
On the quick-start path there is no version to keep in sync by hand:
Dockerfile runs npm install straight from this package.json on every
build, so a rebuild always picks up whatever is pinned there. On the
advanced/prebuilt path, that package.json change is the whole pin:
Dockerfile.prebuilt'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.prebuilt itself to keep in sync by hand. --save-exact matters
either way: npm's default save-prefix is ^, and a caret range is not a
legal Docker image tag for the advanced path — without it, this exact command
would write "^0.18.0" and the next Dockerfile.prebuilt 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 meith -- 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.