create-meith 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.mjs +387 -73
- package/package.json +1 -1
- package/src/bin.ts +1 -1
- package/src/cli.ts +1 -1
- package/src/scaffold-extension.ts +1 -1
- package/src/scaffold.ts +386 -70
package/src/scaffold.ts
CHANGED
|
@@ -29,7 +29,7 @@ export const MATERIALIZED_AT_ROOT = [
|
|
|
29
29
|
'next-env.d.ts',
|
|
30
30
|
]
|
|
31
31
|
|
|
32
|
-
export const VERCEL_BUILD_COMMAND = `
|
|
32
|
+
export const VERCEL_BUILD_COMMAND = `meith migrate && forum-web build ${AT_ROOT_FLAG}`
|
|
33
33
|
|
|
34
34
|
export const TICK_PATH = '/api/system/tick'
|
|
35
35
|
|
|
@@ -103,7 +103,7 @@ const ENV_DATABASE_URL_PROSE = `# Your Postgres connection string.
|
|
|
103
103
|
# processes in front of it, does not need one.`
|
|
104
104
|
|
|
105
105
|
const ENV_DIRECT_DATABASE_URL_PROSE = `# The other half of that pair: the DIRECT (non-pooler) string, used only by
|
|
106
|
-
# \`
|
|
106
|
+
# \`meith migrate\` and \`meith backup\`. Migrations hold a session-level
|
|
107
107
|
# advisory lock so that two deploys landing together queue instead of both
|
|
108
108
|
# applying the same migration, and a transaction-mode pooler cannot hold that
|
|
109
109
|
# lock: it takes the connection back the moment the lock statement ends, which
|
|
@@ -238,7 +238,7 @@ DATABASE_URL=
|
|
|
238
238
|
${ENV_DIRECT_DATABASE_URL_PROSE}
|
|
239
239
|
#
|
|
240
240
|
# On Vercel this is not optional, and it is no longer yours to copy. DATABASE_URL
|
|
241
|
-
# here is the pooler string, the build runs \`
|
|
241
|
+
# here is the pooler string, the build runs \`meith migrate\` against it, and
|
|
242
242
|
# /install takes the second of those two session locks on first run. Left blank,
|
|
243
243
|
# the board reads Neon's own direct string — \`DATABASE_URL_UNPOOLED\` first, then
|
|
244
244
|
# \`POSTGRES_URL_NON_POOLING\` — and refuses to boot if neither is there, naming
|
|
@@ -275,7 +275,7 @@ CRON_SECRET=
|
|
|
275
275
|
#
|
|
276
276
|
# BLOB_READ_WRITE_TOKEN is the other way in, and you make it yourself on the
|
|
277
277
|
# store. Set it when something has to reach the store from OUTSIDE a Vercel
|
|
278
|
-
# deployment — \`
|
|
278
|
+
# deployment — \`meith backup\` run on your own machine is the case that
|
|
279
279
|
# matters — because there is no OIDC identity there to borrow. Set both and the
|
|
280
280
|
# board prefers the store id, unless the token names a different store, in which
|
|
281
281
|
# case the token wins: naming another store is a deliberate act.
|
|
@@ -353,7 +353,9 @@ const SELF_HOST_DEPLOY_KIT = [
|
|
|
353
353
|
'.github/dependabot.yml',
|
|
354
354
|
'.github/workflows/build.yml',
|
|
355
355
|
'Dockerfile',
|
|
356
|
+
'Dockerfile.prebuilt',
|
|
356
357
|
'docker-compose.yaml',
|
|
358
|
+
'docker-compose.prebuilt.yaml',
|
|
357
359
|
'docker-entrypoint.sh',
|
|
358
360
|
'docker-healthcheck.sh',
|
|
359
361
|
] as const
|
|
@@ -433,7 +435,7 @@ export function scaffold(options: ScaffoldOptions): ReadonlyMap<string, string>
|
|
|
433
435
|
dev: `forum-web dev${atRootFlag}`,
|
|
434
436
|
build: `forum-web build${atRootFlag}`,
|
|
435
437
|
start: `forum-web start${atRootFlag}`,
|
|
436
|
-
|
|
438
|
+
meith: 'meith',
|
|
437
439
|
},
|
|
438
440
|
dependencies: {
|
|
439
441
|
'@meith/web': version,
|
|
@@ -523,7 +525,7 @@ export default defineForumConfig({
|
|
|
523
525
|
* ]
|
|
524
526
|
*
|
|
525
527
|
* and the matching entry in board.plugins.json, which is what
|
|
526
|
-
* \`
|
|
528
|
+
* \`meith plugin:add\`/\`plugin:remove\` read inside the monorepo — kept
|
|
527
529
|
* here too so the two files agree about what is installed.
|
|
528
530
|
*/
|
|
529
531
|
import type { InstalledPlugin } from '@meith/web/config'
|
|
@@ -575,7 +577,88 @@ updates:
|
|
|
575
577
|
'Dockerfile',
|
|
576
578
|
`# syntax=docker/dockerfile:1.7-labs
|
|
577
579
|
# check=skip=InvalidDefaultArgInFrom
|
|
578
|
-
# ${name}'s deploy image
|
|
580
|
+
# ${name}'s quick-start deploy image — built from source, with nothing to
|
|
581
|
+
# set up first.
|
|
582
|
+
#
|
|
583
|
+
# FROM node:26-alpine directly rather than a published Meith base image:
|
|
584
|
+
# Coolify (or a plain \`docker build\`) builds this from this repository, so
|
|
585
|
+
# there is no registry account, no image tag to paste anywhere, and no
|
|
586
|
+
# \`.github/workflows/build.yml\` run to wait on. The cost of that zero setup
|
|
587
|
+
# is that this installs the board's full dependency closure itself (see the
|
|
588
|
+
# \`npm install\` below), so a build here is heavier than \`Dockerfile.prebuilt\`'s
|
|
589
|
+
# thin delta — that image, pulled rather than built, is the trade the advanced
|
|
590
|
+
# path takes for a low-spec build server or a faster deploy (see \`README.md\`
|
|
591
|
+
# and, in the meith repository, docs/getting-started/deployment/docker-compose.md,
|
|
592
|
+
# "Custom boards").
|
|
593
|
+
#
|
|
594
|
+
# Two stages, not three: unlike the official image, this does not prune down
|
|
595
|
+
# to Next's own standalone output. The migrate role below runs \`meith
|
|
596
|
+
# migrate\`, and \`meith\` materializes @meith/cli's sources and runs them
|
|
597
|
+
# with tsx at the moment it runs (see the meith repository's
|
|
598
|
+
# docs/contributing/development.md, "Consuming the board from a workspace") — it needs
|
|
599
|
+
# the full, un-pruned node_modules tree this board installed, not what Next
|
|
600
|
+
# traced as reachable from the web server alone. The tick itself is driven
|
|
601
|
+
# by docker-compose.yaml's own \`worker\` service — a lightweight loop against
|
|
602
|
+
# /api/system/tick, not a compiled worker process, because @meith/worker is
|
|
603
|
+
# not published (see the meith repository's docs/contributing/release.md).
|
|
604
|
+
FROM node:26-alpine@sha256:aadf416b2cdce311a8811ba3f0608a61b77dbf997500e2eafe781b51f6a0b019 AS deps
|
|
605
|
+
WORKDIR /board
|
|
606
|
+
|
|
607
|
+
# This board's own manifest, cached independently of its source — editing
|
|
608
|
+
# meith.config.ts should not re-run npm install. Nothing warms node_modules
|
|
609
|
+
# ahead of this the way \`Dockerfile.prebuilt\`'s base image does: the full
|
|
610
|
+
# @meith/web, @meith/cli and @meith/theme-default closure this board depends
|
|
611
|
+
# on is installed here, from scratch, which is the heavier half of the
|
|
612
|
+
# quick-start trade.
|
|
613
|
+
COPY package.json ./
|
|
614
|
+
RUN npm install
|
|
615
|
+
|
|
616
|
+
FROM deps AS runtime
|
|
617
|
+
WORKDIR /board
|
|
618
|
+
COPY . .
|
|
619
|
+
|
|
620
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
621
|
+
ENV NODE_ENV=production
|
|
622
|
+
|
|
623
|
+
# DATA_SOURCE is scoped to this one RUN, not declared with ENV — an ENV
|
|
624
|
+
# persists into every container started from this image afterward, and this
|
|
625
|
+
# Dockerfile has no later stage to reset it in (see "Two stages, not three"
|
|
626
|
+
# above). The build needs neither a database nor a production secret (see
|
|
627
|
+
# the meith repository's docs/contributing/development.md, "Fixture mode"), but baking
|
|
628
|
+
# DATA_SOURCE=fixture into the image itself would silently force fixture
|
|
629
|
+
# mode — and with it the in-memory queue driver — at runtime too, no matter
|
|
630
|
+
# what DATABASE_URL an operator supplies to \`docker run\`.
|
|
631
|
+
RUN DATA_SOURCE=fixture npx forum-web build
|
|
632
|
+
|
|
633
|
+
ENV PORT=3000
|
|
634
|
+
ENV HOSTNAME=0.0.0.0
|
|
635
|
+
EXPOSE 3000
|
|
636
|
+
|
|
637
|
+
# node:alpine already carries a non-root "node" user; the board's own files
|
|
638
|
+
# are copied in as root above, so they need handing over before this drops
|
|
639
|
+
# privilege.
|
|
640
|
+
RUN chown -R node:node /board
|
|
641
|
+
USER node
|
|
642
|
+
|
|
643
|
+
COPY --chown=node:node docker-entrypoint.sh docker-healthcheck.sh ./
|
|
644
|
+
RUN chmod +x docker-entrypoint.sh docker-healthcheck.sh
|
|
645
|
+
|
|
646
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \\
|
|
647
|
+
CMD ["./docker-healthcheck.sh"]
|
|
648
|
+
|
|
649
|
+
ENTRYPOINT ["./docker-entrypoint.sh"]
|
|
650
|
+
`,
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
files.set(
|
|
654
|
+
'Dockerfile.prebuilt',
|
|
655
|
+
`# syntax=docker/dockerfile:1.7-labs
|
|
656
|
+
# check=skip=InvalidDefaultArgInFrom
|
|
657
|
+
# ${name}'s advanced deploy image — built by \`.github/workflows/build.yml\` and
|
|
658
|
+
# pulled by \`docker-compose.prebuilt.yaml\`. A quick-start board never builds
|
|
659
|
+
# this file directly; it can delete this file, \`docker-compose.prebuilt.yaml\`
|
|
660
|
+
# and \`.github/workflows/build.yml\` outright and keep only \`Dockerfile\` and
|
|
661
|
+
# \`docker-compose.yaml\` (see README.md, "Deploy").
|
|
579
662
|
#
|
|
580
663
|
# FROM the published framework base image — deps + framework layers only,
|
|
581
664
|
# locked to this exact release (see the meith repository's
|
|
@@ -586,8 +669,8 @@ updates:
|
|
|
586
669
|
# of minutes rather than a cold toolchain build.
|
|
587
670
|
#
|
|
588
671
|
# Two stages, not three: unlike the official image, this does not prune down
|
|
589
|
-
# to Next's own standalone output. The migrate role below runs \`
|
|
590
|
-
# migrate\`, and \`
|
|
672
|
+
# to Next's own standalone output. The migrate role below runs \`meith
|
|
673
|
+
# migrate\`, and \`meith\` materializes @meith/cli's sources and runs them
|
|
591
674
|
# with tsx at the moment it runs (see the meith repository's
|
|
592
675
|
# docs/contributing/development.md, "Consuming the board from a workspace") — it needs
|
|
593
676
|
# the full, un-pruned node_modules tree this board installed, not what Next
|
|
@@ -658,22 +741,22 @@ ENTRYPOINT ["./docker-entrypoint.sh"]
|
|
|
658
741
|
set -e
|
|
659
742
|
|
|
660
743
|
# An explicit command wins over the role, the same as the official image —
|
|
661
|
-
# \`docker run <image> node_modules/.bin/
|
|
744
|
+
# \`docker run <image> node_modules/.bin/meith --help\` should still run
|
|
662
745
|
# the CLI rather than silently starting the web server.
|
|
663
746
|
if [ "$#" -gt 0 ]; then
|
|
664
747
|
exec "$@"
|
|
665
748
|
fi
|
|
666
749
|
|
|
667
|
-
case "\${
|
|
750
|
+
case "\${MEITH_ROLE:-web}" in
|
|
668
751
|
migrate)
|
|
669
752
|
# Runs to completion and exits; compose's one-shot service waits on it.
|
|
670
|
-
exec node_modules/.bin/
|
|
753
|
+
exec node_modules/.bin/meith migrate
|
|
671
754
|
;;
|
|
672
755
|
web)
|
|
673
756
|
exec node_modules/.bin/forum-web start
|
|
674
757
|
;;
|
|
675
758
|
*)
|
|
676
|
-
echo "Unknown
|
|
759
|
+
echo "Unknown MEITH_ROLE: \${MEITH_ROLE}. Expected 'web' or 'migrate'." >&2
|
|
677
760
|
exit 1
|
|
678
761
|
;;
|
|
679
762
|
esac
|
|
@@ -688,7 +771,7 @@ esac
|
|
|
688
771
|
# a health probe taken while it runs has no opinion.
|
|
689
772
|
set -e
|
|
690
773
|
|
|
691
|
-
if [ "\${
|
|
774
|
+
if [ "\${MEITH_ROLE:-web}" = "migrate" ]; then
|
|
692
775
|
exit 0
|
|
693
776
|
fi
|
|
694
777
|
|
|
@@ -710,10 +793,10 @@ node -e "fetch('http://127.0.0.1:3000/api/ready').then(r=>process.exit(r.ok?0:1)
|
|
|
710
793
|
|
|
711
794
|
files.set(
|
|
712
795
|
'.github/workflows/build.yml',
|
|
713
|
-
`#
|
|
714
|
-
#
|
|
715
|
-
#
|
|
716
|
-
#
|
|
796
|
+
`# The advanced/prebuilt path (see README.md, "Deploy"). Builds this board's
|
|
797
|
+
# Dockerfile.prebuilt and pushes it to your own GHCR, on every push to main.
|
|
798
|
+
# No secret to configure: GITHUB_TOKEN is provided automatically by GitHub
|
|
799
|
+
# Actions and is enough to push to ghcr.io/<this repository>.
|
|
717
800
|
name: Build and push
|
|
718
801
|
|
|
719
802
|
on:
|
|
@@ -748,7 +831,7 @@ jobs:
|
|
|
748
831
|
echo "::error::@meith/web in package.json is '$MEITH_VERSION', not an exact X.Y.Z version — that is not a legal Docker image tag. Upgrade with \\\`npm install --save-exact\\\` (see README.md, Upgrading) so this dependency always resolves to one."
|
|
749
832
|
exit 1
|
|
750
833
|
fi
|
|
751
|
-
docker build --build-arg MEITH_VERSION="$MEITH_VERSION" -t "$IMAGE:\${{ github.sha }}" -t "$IMAGE:latest" .
|
|
834
|
+
docker build -f Dockerfile.prebuilt --build-arg MEITH_VERSION="$MEITH_VERSION" -t "$IMAGE:\${{ github.sha }}" -t "$IMAGE:latest" .
|
|
752
835
|
docker push "$IMAGE:\${{ github.sha }}"
|
|
753
836
|
docker push "$IMAGE:latest"
|
|
754
837
|
|
|
@@ -789,9 +872,142 @@ jobs:
|
|
|
789
872
|
|
|
790
873
|
files.set(
|
|
791
874
|
'docker-compose.yaml',
|
|
792
|
-
`# ${name}, deployed by Coolify — the same shape as the meith
|
|
793
|
-
# docker/compose.coolify.yml: db, migrate, web, worker. See
|
|
794
|
-
# the
|
|
875
|
+
`# ${name}, quick-start deployed by Coolify — the same shape as the meith
|
|
876
|
+
# repository's own docker/compose.coolify.yml: db, migrate, web, worker. See
|
|
877
|
+
# README.md for the deploy story this file is the last step of.
|
|
878
|
+
#
|
|
879
|
+
# Coolify builds \`Dockerfile\` from this repository itself, so there is no
|
|
880
|
+
# MEITH_IMAGE to set here — every deploy is a build from source. For a
|
|
881
|
+
# low-spec build server or a faster deploy, use \`docker-compose.prebuilt.yaml\`
|
|
882
|
+
# instead, which pulls the image \`.github/workflows/build.yml\` pushes to GHCR.
|
|
883
|
+
#
|
|
884
|
+
# No published ports — Coolify's proxy routes to the container and issues
|
|
885
|
+
# the certificate. The two secrets and the database password are Coolify's
|
|
886
|
+
# own "magic variables": it fills them in on the first deploy and shows them
|
|
887
|
+
# in the panel, so nothing here needs a value typed into it. Requires Coolify
|
|
888
|
+
# v4.0.0-beta.411 or newer, which is when magic variables in a compose file
|
|
889
|
+
# from a Git source arrived.
|
|
890
|
+
services:
|
|
891
|
+
postgres:
|
|
892
|
+
image: postgres:18-alpine@sha256:d3e1620b530c944afa6e887d22eb899824da68e19c52024bf98f5220c88a65b2
|
|
893
|
+
restart: unless-stopped
|
|
894
|
+
mem_limit: \${POSTGRES_MEM_LIMIT:-1g}
|
|
895
|
+
cpus: \${POSTGRES_CPUS:-1}
|
|
896
|
+
environment:
|
|
897
|
+
POSTGRES_USER: community
|
|
898
|
+
POSTGRES_PASSWORD: $SERVICE_PASSWORD_POSTGRES
|
|
899
|
+
POSTGRES_DB: community
|
|
900
|
+
volumes:
|
|
901
|
+
- pgdata:/var/lib/postgresql
|
|
902
|
+
healthcheck:
|
|
903
|
+
test: ['CMD-SHELL', 'pg_isready -U community -d community']
|
|
904
|
+
interval: 10s
|
|
905
|
+
timeout: 5s
|
|
906
|
+
retries: 5
|
|
907
|
+
|
|
908
|
+
# Runs to completion, then exits. web waits for it, so the schema is
|
|
909
|
+
# always applied before the first request rather than racing it.
|
|
910
|
+
migrate:
|
|
911
|
+
build: .
|
|
912
|
+
image: ${name}
|
|
913
|
+
environment:
|
|
914
|
+
MEITH_ROLE: migrate
|
|
915
|
+
DATABASE_URL: postgres://community:$SERVICE_PASSWORD_POSTGRES@postgres:5432/community
|
|
916
|
+
AUTH_SECRET: $SERVICE_BASE64_64_AUTH
|
|
917
|
+
TICK_SECRET: $SERVICE_BASE64_64_TICK
|
|
918
|
+
depends_on:
|
|
919
|
+
postgres:
|
|
920
|
+
condition: service_healthy
|
|
921
|
+
restart: 'no'
|
|
922
|
+
|
|
923
|
+
web:
|
|
924
|
+
build: .
|
|
925
|
+
image: ${name}
|
|
926
|
+
restart: unless-stopped
|
|
927
|
+
mem_limit: \${WEB_MEM_LIMIT:-1g}
|
|
928
|
+
cpus: \${WEB_CPUS:-2}
|
|
929
|
+
# A readiness probe Coolify gates a rolling deploy on: with "Rolling
|
|
930
|
+
# update" enabled on the resource, the new container must answer
|
|
931
|
+
# /api/ready before the old one is retired, so a redeploy swaps in with no
|
|
932
|
+
# gap. Without it Coolify recreates the stack — old removed, then new
|
|
933
|
+
# started — and the board is down while the new web boots.
|
|
934
|
+
healthcheck:
|
|
935
|
+
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/api/ready').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
|
936
|
+
interval: 30s
|
|
937
|
+
timeout: 5s
|
|
938
|
+
start_period: 20s
|
|
939
|
+
retries: 3
|
|
940
|
+
environment:
|
|
941
|
+
# Ask Coolify for a domain on port 3000, then hand the board the same
|
|
942
|
+
# thing with a scheme in front.
|
|
943
|
+
- SERVICE_FQDN_WEB_3000
|
|
944
|
+
- APP_URL=$SERVICE_URL_WEB
|
|
945
|
+
- DATABASE_URL=postgres://community:$SERVICE_PASSWORD_POSTGRES@postgres:5432/community
|
|
946
|
+
- AUTH_SECRET=$SERVICE_BASE64_64_AUTH
|
|
947
|
+
- TICK_SECRET=$SERVICE_BASE64_64_TICK
|
|
948
|
+
- QUEUE_DRIVER=postgres
|
|
949
|
+
- CACHE_DRIVER=next
|
|
950
|
+
- FILESTORE_DRIVER=local
|
|
951
|
+
# Left unset, mail is configured on the board itself — the installer
|
|
952
|
+
# asks on first run. Set MAIL_DRIVER here and this file wins instead.
|
|
953
|
+
- MAIL_DRIVER=\${MAIL_DRIVER:-log}
|
|
954
|
+
- MAIL_SMTP_HOST=\${MAIL_SMTP_HOST:-}
|
|
955
|
+
- MAIL_SMTP_PORT=\${MAIL_SMTP_PORT:-}
|
|
956
|
+
- MAIL_SMTP_SECURITY=\${MAIL_SMTP_SECURITY:-}
|
|
957
|
+
- MAIL_SMTP_USERNAME=\${MAIL_SMTP_USERNAME:-}
|
|
958
|
+
- MAIL_SMTP_PASSWORD=\${MAIL_SMTP_PASSWORD:-}
|
|
959
|
+
- MAIL_FROM=\${MAIL_FROM:-}
|
|
960
|
+
volumes:
|
|
961
|
+
- uploads:/app/.uploads
|
|
962
|
+
depends_on:
|
|
963
|
+
postgres:
|
|
964
|
+
condition: service_healthy
|
|
965
|
+
migrate:
|
|
966
|
+
condition: service_completed_successfully
|
|
967
|
+
|
|
968
|
+
# @meith/worker is not published (see the meith repository's
|
|
969
|
+
# docs/contributing/release.md), so there is no compiled worker binary a scaffolded
|
|
970
|
+
# board can run — this drives the tick the alternative way the meith
|
|
971
|
+
# repository documents in docs/getting-started/deployment/docker-compose.md, "Running the tick without
|
|
972
|
+
# a second set of credentials": a small loop calling /api/system/tick.
|
|
973
|
+
worker:
|
|
974
|
+
image: alpine:3.24@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b
|
|
975
|
+
restart: unless-stopped
|
|
976
|
+
mem_limit: \${WORKER_MEM_LIMIT:-64m}
|
|
977
|
+
cpus: \${WORKER_CPUS:-0.25}
|
|
978
|
+
environment:
|
|
979
|
+
TICK_SECRET: $SERVICE_BASE64_64_TICK
|
|
980
|
+
command:
|
|
981
|
+
- sh
|
|
982
|
+
- -c
|
|
983
|
+
- |
|
|
984
|
+
apk add --no-cache curl >/dev/null
|
|
985
|
+
while true; do
|
|
986
|
+
curl -fsS -m 55 -H "Authorization: Bearer $$TICK_SECRET" \\
|
|
987
|
+
http://web:3000/api/system/tick >/dev/null 2>&1 \\
|
|
988
|
+
|| echo "tick failed at $$(date -Is)"
|
|
989
|
+
sleep 60
|
|
990
|
+
done
|
|
991
|
+
depends_on:
|
|
992
|
+
- web
|
|
993
|
+
|
|
994
|
+
volumes:
|
|
995
|
+
pgdata:
|
|
996
|
+
uploads:
|
|
997
|
+
`,
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
files.set(
|
|
1001
|
+
'docker-compose.prebuilt.yaml',
|
|
1002
|
+
`# ${name}, deployed by Coolify from a prebuilt image — the advanced path: point
|
|
1003
|
+
# Coolify's compose-file at this file instead of docker-compose.yaml once
|
|
1004
|
+
# \`.github/workflows/build.yml\` has pushed an image, and set MEITH_IMAGE to
|
|
1005
|
+
# what its Summary printed. This trades the quick-start's heavier
|
|
1006
|
+
# build-on-every-deploy for a low-spec build server or a faster deploy — see
|
|
1007
|
+
# README.md, "Deploy".
|
|
1008
|
+
#
|
|
1009
|
+
# Same shape as the meith repository's own docker/compose.coolify.yml: db,
|
|
1010
|
+
# migrate, web, worker.
|
|
795
1011
|
#
|
|
796
1012
|
# No published ports — Coolify's proxy routes to the container and issues
|
|
797
1013
|
# the certificate. The two secrets and the database password are Coolify's
|
|
@@ -823,8 +1039,12 @@ services:
|
|
|
823
1039
|
# always applied before the first request rather than racing it.
|
|
824
1040
|
migrate:
|
|
825
1041
|
image: \${MEITH_IMAGE:?set this to the image the build workflow's Summary just printed, e.g. ghcr.io/<you>/${name}:latest}
|
|
1042
|
+
# Pull the tag on every deploy. Compose keeps an image it already has, so
|
|
1043
|
+
# a rebuilt \`:latest\` is otherwise never fetched and a redeploy quietly
|
|
1044
|
+
# runs the old code.
|
|
1045
|
+
pull_policy: always
|
|
826
1046
|
environment:
|
|
827
|
-
|
|
1047
|
+
MEITH_ROLE: migrate
|
|
828
1048
|
DATABASE_URL: postgres://community:$SERVICE_PASSWORD_POSTGRES@postgres:5432/community
|
|
829
1049
|
AUTH_SECRET: $SERVICE_BASE64_64_AUTH
|
|
830
1050
|
TICK_SECRET: $SERVICE_BASE64_64_TICK
|
|
@@ -835,9 +1055,21 @@ services:
|
|
|
835
1055
|
|
|
836
1056
|
web:
|
|
837
1057
|
image: \${MEITH_IMAGE:?set this to the image the build workflow's Summary just printed, e.g. ghcr.io/<you>/${name}:latest}
|
|
1058
|
+
pull_policy: always
|
|
838
1059
|
restart: unless-stopped
|
|
839
1060
|
mem_limit: \${WEB_MEM_LIMIT:-1g}
|
|
840
1061
|
cpus: \${WEB_CPUS:-2}
|
|
1062
|
+
# A readiness probe Coolify gates a rolling deploy on: with "Rolling
|
|
1063
|
+
# update" enabled on the resource, the new container must answer
|
|
1064
|
+
# /api/ready before the old one is retired, so a redeploy swaps in with no
|
|
1065
|
+
# gap. Without it Coolify recreates the stack — old removed, then new
|
|
1066
|
+
# started — and the board is down while the new web boots.
|
|
1067
|
+
healthcheck:
|
|
1068
|
+
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/api/ready').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
|
1069
|
+
interval: 30s
|
|
1070
|
+
timeout: 5s
|
|
1071
|
+
start_period: 20s
|
|
1072
|
+
retries: 3
|
|
841
1073
|
environment:
|
|
842
1074
|
# Ask Coolify for a domain on port 3000, then hand the board the same
|
|
843
1075
|
# thing with a scheme in front.
|
|
@@ -906,15 +1138,55 @@ A forum, built on [Meith](${repositoryUrl}).
|
|
|
906
1138
|
|
|
907
1139
|
## Deploy
|
|
908
1140
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
server
|
|
913
|
-
one
|
|
1141
|
+
Two paths onto [Coolify](https://coolify.io), both ending at the same
|
|
1142
|
+
\`/install\`. **Quick start** is the default and needs nothing but a push;
|
|
1143
|
+
**advanced/prebuilt** moves the build off the server, onto GitHub Actions, for
|
|
1144
|
+
a low-spec build server or a faster deploy. Pick one — a board only ever runs
|
|
1145
|
+
one of them at a time.
|
|
1146
|
+
|
|
1147
|
+
### Quick start (default)
|
|
1148
|
+
|
|
1149
|
+
Coolify builds the image itself, from this repository, every time it
|
|
1150
|
+
deploys — there is nothing to push anywhere first and no image tag to paste
|
|
1151
|
+
in. Two steps:
|
|
1152
|
+
|
|
1153
|
+
1. **Push this repository to GitHub.**
|
|
1154
|
+
|
|
1155
|
+
2. **Point Coolify at \`docker-compose.yaml\`** — a **Public Git repository**
|
|
1156
|
+
resource with **Docker Compose** as its build pack, this repository as its
|
|
1157
|
+
source. The name is Coolify's own default, so its **Compose file** field is
|
|
1158
|
+
already right when the form opens, and the file already carries Coolify's
|
|
1159
|
+
own "magic variables" for \`AUTH_SECRET\`, \`TICK_SECRET\` and the database
|
|
1160
|
+
password, generated on the first deploy and never typed in. Nothing else to
|
|
1161
|
+
set: \`docker-compose.yaml\` builds \`web\` and \`migrate\` from \`Dockerfile\`
|
|
1162
|
+
itself, so there is no \`MEITH_IMAGE\` here at all.
|
|
1163
|
+
|
|
1164
|
+
3. **Deploy, then \`/install\` on your own domain.** Coolify issues the
|
|
1165
|
+
certificate; the installer from there is the one
|
|
1166
|
+
[docs/getting-started/deployment/coolify.md](${repositoryUrl}/blob/main/docs/getting-started/deployment/coolify.md#4-run-the-installer)
|
|
1167
|
+
walks through, screen for screen. It seals itself when it finishes, and
|
|
1168
|
+
\`/install\` answers 404 from then on — run it **against the database you
|
|
1169
|
+
are going to keep**. Every push to \`main\` after this is picked up the next
|
|
1170
|
+
time Coolify's own **Redeploy** button runs — pushing alone does not
|
|
1171
|
+
rebuild it.
|
|
1172
|
+
|
|
1173
|
+
The trade for that zero setup is a heavier build: \`Dockerfile\` installs this
|
|
1174
|
+
board's full dependency closure on the server itself, on every deploy, rather
|
|
1175
|
+
than starting from a warm base image. A 2 GB VPS can OOM on it. If that is
|
|
1176
|
+
your server, use the advanced path below instead.
|
|
1177
|
+
|
|
1178
|
+
A quick-start board never needs \`Dockerfile.prebuilt\`,
|
|
1179
|
+
\`docker-compose.prebuilt.yaml\` or \`.github/workflows/build.yml\` — delete all
|
|
1180
|
+
three.
|
|
1181
|
+
|
|
1182
|
+
### Advanced / prebuilt — for a low-spec server or a faster deploy
|
|
1183
|
+
|
|
1184
|
+
Something else builds the image ahead of time; the server only ever pulls
|
|
1185
|
+
one. Three steps, nothing to configure by hand beyond one value only you know:
|
|
914
1186
|
|
|
915
1187
|
1. **Push this repository to GitHub.** \`.github/workflows/build.yml\` builds
|
|
916
|
-
\`Dockerfile\` on every push to \`main\` and pushes the result to your
|
|
917
|
-
GitHub Container Registry, \`ghcr.io/<you>/${name}\` — using only the
|
|
1188
|
+
\`Dockerfile.prebuilt\` on every push to \`main\` and pushes the result to your
|
|
1189
|
+
own GitHub Container Registry, \`ghcr.io/<you>/${name}\` — using only the
|
|
918
1190
|
\`GITHUB_TOKEN\` every GitHub Actions run already carries. No secret to
|
|
919
1191
|
add, no registry account beyond the GitHub account you already have.
|
|
920
1192
|
|
|
@@ -925,29 +1197,26 @@ one value only you know:
|
|
|
925
1197
|
repository usually lands public already, and a private one fails
|
|
926
1198
|
Coolify's pull with an authentication error no operator can act on.
|
|
927
1199
|
|
|
928
|
-
2. **Point
|
|
1200
|
+
2. **Point Coolify at \`docker-compose.prebuilt.yaml\`** — a
|
|
929
1201
|
**Public Git repository** resource with **Docker Compose** as its build
|
|
930
|
-
pack, this repository as its source
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
\`AUTH_SECRET\`, \`TICK_SECRET\` and the database password,
|
|
934
|
-
the first deploy and never typed in. The one thing Coolify
|
|
935
|
-
generate is the image step 1 just pushed: set \`MEITH_IMAGE\` in the
|
|
1202
|
+
pack, this repository as its source, and its **Compose file** field
|
|
1203
|
+
changed from Coolify's default of \`docker-compose.yaml\` to
|
|
1204
|
+
\`docker-compose.prebuilt.yaml\`. That file carries Coolify's own "magic
|
|
1205
|
+
variables" for \`AUTH_SECRET\`, \`TICK_SECRET\` and the database password,
|
|
1206
|
+
generated on the first deploy and never typed in. The one thing Coolify
|
|
1207
|
+
cannot generate is the image step 1 just pushed: set \`MEITH_IMAGE\` in the
|
|
936
1208
|
resource's own environment to one of the two values that run's Summary
|
|
937
|
-
printed (\`docker-compose.yaml\` refuses to start without it, with
|
|
938
|
-
message saying why). \`ghcr.io/<you>/${name}:\${{ github.sha }}\` names
|
|
1209
|
+
printed (\`docker-compose.prebuilt.yaml\` refuses to start without it, with
|
|
1210
|
+
a message saying why). \`ghcr.io/<you>/${name}:\${{ github.sha }}\` names
|
|
939
1211
|
that one build and nothing else, ever; \`ghcr.io/<you>/${name}:latest\`
|
|
940
1212
|
follows \`main\` instead, so installing a plugin later is a push and a
|
|
941
|
-
**Redeploy** — the trade
|
|
942
|
-
|
|
1213
|
+
**Redeploy** — the trade this path takes, at the cost of an unrelated
|
|
1214
|
+
redeploy pulling whatever \`main\` most recently built.
|
|
943
1215
|
|
|
944
|
-
3. **Deploy, then \`/install\` on your own domain.**
|
|
945
|
-
certificate; the installer from there is the one
|
|
1216
|
+
3. **Deploy, then \`/install\` on your own domain.** Same installer, same
|
|
946
1217
|
[docs/getting-started/deployment/coolify.md](${repositoryUrl}/blob/main/docs/getting-started/deployment/coolify.md#4-run-the-installer)
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
are going to keep**. Every push to \`main\` after this rebuilds the
|
|
950
|
-
image; Coolify's own **Redeploy** button is what actually pulls it —
|
|
1218
|
+
walk-through, same one-time seal. Every push to \`main\` after this rebuilds
|
|
1219
|
+
the image; Coolify's own **Redeploy** button is what actually pulls it —
|
|
951
1220
|
pushing alone does not.
|
|
952
1221
|
|
|
953
1222
|
No Docker Hub, no paid CI: GitHub Actions' free tier and GHCR are the whole
|
|
@@ -955,26 +1224,27 @@ build side of this, for a board of any size.
|
|
|
955
1224
|
|
|
956
1225
|
**Building it yourself**: works on any machine with Docker, if you would
|
|
957
1226
|
rather not use GitHub Actions for the build — push the result wherever
|
|
958
|
-
\`docker-compose.yaml\`'s \`MEITH_IMAGE\` can reach.
|
|
1227
|
+
\`docker-compose.prebuilt.yaml\`'s \`MEITH_IMAGE\` can reach.
|
|
959
1228
|
|
|
960
1229
|
\`\`\`sh
|
|
961
|
-
docker build --build-arg MEITH_VERSION=$(node -p "require('./package.json').dependencies['@meith/web']") -t ${name} .
|
|
1230
|
+
docker build -f Dockerfile.prebuilt --build-arg MEITH_VERSION=$(node -p "require('./package.json').dependencies['@meith/web']") -t ${name} .
|
|
962
1231
|
\`\`\`
|
|
963
1232
|
|
|
964
1233
|
**Without a panel**: [docs/getting-started/deployment/docker-compose.md](${repositoryUrl}/blob/main/docs/getting-started/deployment/docker-compose.md)
|
|
965
1234
|
is the same four containers by hand — your own \`.env\`, a reverse proxy you
|
|
966
1235
|
already run, no Coolify. \`Dockerfile\` and \`docker-compose.yaml\` here are this
|
|
967
|
-
board's own version of exactly that shape.
|
|
1236
|
+
board's own version of exactly that shape (or \`Dockerfile.prebuilt\` and
|
|
1237
|
+
\`docker-compose.prebuilt.yaml\`, for the advanced path).
|
|
968
1238
|
|
|
969
|
-
Two things nothing configures for you:
|
|
1239
|
+
Two things nothing configures for you, on either path:
|
|
970
1240
|
|
|
971
1241
|
- **Mail.** Until \`MAIL_DRIVER\` and its three settings exist, every message is
|
|
972
1242
|
written to the log and delivered to nobody, so password reset fails silently.
|
|
973
|
-
- **The tick.**
|
|
1243
|
+
- **The tick.** The compose file's \`worker\` service drives it here — a small
|
|
974
1244
|
loop calling \`/api/system/tick\` once a minute, since \`@meith/web\`'s own
|
|
975
1245
|
worker package is not something a board outside the meith monorepo can
|
|
976
1246
|
depend on yet. Deploy some other way and something still has to call that
|
|
977
|
-
route (or run \`
|
|
1247
|
+
route (or run \`meith task:run\`) every minute, or nothing catches up
|
|
978
1248
|
and nothing errors.
|
|
979
1249
|
|
|
980
1250
|
## Local
|
|
@@ -992,8 +1262,8 @@ Posting needs Postgres. Copy \`.env.example\` to \`.env.local\`, set
|
|
|
992
1262
|
\`DATABASE_URL\` and the two secrets in it, then:
|
|
993
1263
|
|
|
994
1264
|
\`\`\`sh
|
|
995
|
-
npm run
|
|
996
|
-
echo "<password>" | npm run
|
|
1265
|
+
npm run meith -- migrate
|
|
1266
|
+
echo "<password>" | npm run meith -- user:create --username <name> --email <address> --group administrators
|
|
997
1267
|
\`\`\`
|
|
998
1268
|
|
|
999
1269
|
## Configuring
|
|
@@ -1004,9 +1274,51 @@ echo "<password>" | npm run community -- user:create --username <name> --email <
|
|
|
1004
1274
|
- **\`/admin\`** — settings, forums, groups, members, themes, maintenance. An
|
|
1005
1275
|
administrator re-enters their password to get in, and again for anything
|
|
1006
1276
|
destructive.
|
|
1007
|
-
- **\`npm run
|
|
1277
|
+
- **\`npm run meith -- --help\`** — the operator CLI. Everything the panel does
|
|
1008
1278
|
and a few things it cannot, without a browser.
|
|
1009
1279
|
|
|
1280
|
+
## Installing plugins and themes
|
|
1281
|
+
|
|
1282
|
+
Nothing installs into a running container — a plugin or theme has to be
|
|
1283
|
+
built into the image, the same as any other dependency:
|
|
1284
|
+
|
|
1285
|
+
1. **In this repository**, install it:
|
|
1286
|
+
|
|
1287
|
+
\`\`\`sh
|
|
1288
|
+
npm install --save-exact @meith/plugin-dues
|
|
1289
|
+
\`\`\`
|
|
1290
|
+
|
|
1291
|
+
(a theme is the same command with its own package, e.g.
|
|
1292
|
+
\`@meith/theme-midnight\`).
|
|
1293
|
+
|
|
1294
|
+
2. **Register it.** A **theme** goes in \`meith.config.ts\`, in the \`themes\`
|
|
1295
|
+
map, following the shape of the \`default\` entry already there. A
|
|
1296
|
+
**plugin** goes in \`meith.plugins.ts\`: import its \`plugin\` and
|
|
1297
|
+
\`messages\` exports and add \`{ key, enabled: true, plugin, messages }\`
|
|
1298
|
+
to \`INSTALLED_PLUGINS\` — or run
|
|
1299
|
+
|
|
1300
|
+
\`\`\`sh
|
|
1301
|
+
npm run meith -- plugin:add @meith/plugin-dues
|
|
1302
|
+
\`\`\`
|
|
1303
|
+
|
|
1304
|
+
which edits \`board.plugins.json\` and regenerates \`meith.plugins.ts\`
|
|
1305
|
+
for you.
|
|
1306
|
+
|
|
1307
|
+
3. **Commit and push**, then **Redeploy** from Coolify — pushing alone does
|
|
1308
|
+
not rebuild. Quick start builds the new image on that redeploy; advanced/prebuilt
|
|
1309
|
+
waits for \`.github/workflows/build.yml\` to finish first, and Redeploy is
|
|
1310
|
+
what actually pulls the result.
|
|
1311
|
+
|
|
1312
|
+
4. **Once it is up, run its migrations one time:**
|
|
1313
|
+
|
|
1314
|
+
\`\`\`sh
|
|
1315
|
+
docker compose run --rm web meith upgrade
|
|
1316
|
+
\`\`\`
|
|
1317
|
+
|
|
1318
|
+
See [docs/customization/plugins.md](${repositoryUrl}/blob/main/docs/customization/plugins.md)
|
|
1319
|
+
and [docs/customization/themes.md](${repositoryUrl}/blob/main/docs/customization/themes.md)
|
|
1320
|
+
for the full reference.
|
|
1321
|
+
|
|
1010
1322
|
## Upgrading
|
|
1011
1323
|
|
|
1012
1324
|
\`\`\`sh
|
|
@@ -1031,19 +1343,23 @@ weekly pull request bumping the actions pinned in
|
|
|
1031
1343
|
\`.github/workflows/build.yml\`, which is a safe, independent update the two
|
|
1032
1344
|
commands above never touch.
|
|
1033
1345
|
|
|
1034
|
-
|
|
1035
|
-
\`
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1346
|
+
On the quick-start path there is no version to keep in sync by hand:
|
|
1347
|
+
\`Dockerfile\` runs \`npm install\` straight from this \`package.json\` on every
|
|
1348
|
+
build, so a rebuild always picks up whatever is pinned there. On the
|
|
1349
|
+
advanced/prebuilt path, that \`package.json\` change is the whole pin:
|
|
1350
|
+
\`Dockerfile.prebuilt\`'s own \`FROM\` line takes the version as a build argument,
|
|
1351
|
+
and \`.github/workflows/build.yml\` reads it straight out of \`package.json\`'s
|
|
1352
|
+
own \`@meith/web\` dependency when it rebuilds — nothing in
|
|
1353
|
+
\`Dockerfile.prebuilt\` itself to keep in sync by hand. \`--save-exact\` matters
|
|
1354
|
+
either way: npm's default \`save-prefix\` is \`^\`, and a caret range is not a
|
|
1355
|
+
legal Docker image tag for the advanced path — without it, this exact command
|
|
1356
|
+
would write \`"^0.18.0"\` and the next \`Dockerfile.prebuilt\` build would fail
|
|
1357
|
+
with \`invalid reference format\` instead of building. This
|
|
1042
1358
|
project's own \`.npmrc\` sets \`save-exact=true\` for the same reason, so an
|
|
1043
1359
|
\`npm install\` of anything else here — a plugin, say — stays pinned too; the
|
|
1044
1360
|
build workflow also refuses to build from anything but an exact version, as
|
|
1045
1361
|
a second line of defense. Once the rebuilt image is deployed, run
|
|
1046
|
-
\`npm run
|
|
1362
|
+
\`npm run meith -- upgrade\` against it for the plugin migrations — see
|
|
1047
1363
|
[the operator CLI](${repositoryUrl}/blob/main/docs/guides/operations/operating.md#the-operator-cli)
|
|
1048
1364
|
for running it against this deployment.
|
|
1049
1365
|
|
|
@@ -1303,9 +1619,9 @@ A board must stay movable, and the Blob store is the one part of this shape that
|
|
|
1303
1619
|
is not portable: Neon and Upstash hand out ordinary Postgres and Redis strings
|
|
1304
1620
|
that any host accepts, but a Vercel Blob store is reachable only through Vercel's
|
|
1305
1621
|
own API and there is no bucket to sync out of it. **The uploads are the thing you
|
|
1306
|
-
have to carry out deliberately, and \`
|
|
1622
|
+
have to carry out deliberately, and \`meith backup\` is how.**
|
|
1307
1623
|
|
|
1308
|
-
Under \`FILESTORE_DRIVER=blob\`, \`
|
|
1624
|
+
Under \`FILESTORE_DRIVER=blob\`, \`meith backup\` includes the uploads **by
|
|
1309
1625
|
default** — it walks the Blob store, pulls every object, and puts them in the
|
|
1310
1626
|
bundle beside the database dump. This is the opposite of the \`s3\` default, which
|
|
1311
1627
|
skips them, because a bucket has its own backup story you can drive yourself and
|
|
@@ -1316,7 +1632,7 @@ DATABASE_URL=… # Neon's pooled string
|
|
|
1316
1632
|
DIRECT_DATABASE_URL=… # Neon's DATABASE_URL_UNPOOLED
|
|
1317
1633
|
FILESTORE_DRIVER=blob
|
|
1318
1634
|
BLOB_READ_WRITE_TOKEN=… # create one on the store; see below
|
|
1319
|
-
npm run
|
|
1635
|
+
npm run meith -- backup
|
|
1320
1636
|
\`\`\`
|
|
1321
1637
|
|
|
1322
1638
|
Run that from a checkout of this repository, with those four values in the
|
|
@@ -1337,10 +1653,10 @@ so the same bundle moves the board either onward or away:
|
|
|
1337
1653
|
|
|
1338
1654
|
\`\`\`sh
|
|
1339
1655
|
# onto a self-hosted board with a bucket
|
|
1340
|
-
FILESTORE_DRIVER=s3 S3_BUCKET=… RESTORE_DATABASE_URL=… npm run
|
|
1656
|
+
FILESTORE_DRIVER=s3 S3_BUCKET=… RESTORE_DATABASE_URL=… npm run meith -- restore bundle.tar.gz
|
|
1341
1657
|
|
|
1342
1658
|
# onto a board that keeps uploads on its own disk
|
|
1343
|
-
RESTORE_DATABASE_URL=… npm run
|
|
1659
|
+
RESTORE_DATABASE_URL=… npm run meith -- restore bundle.tar.gz --uploads-dir ./uploads
|
|
1344
1660
|
\`\`\`
|
|
1345
1661
|
|
|
1346
1662
|
Take one before you need it. A Blob store deleted with the Vercel project takes
|