create-meith 0.26.0 → 0.26.1

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 CHANGED
@@ -326,7 +326,9 @@ var SELF_HOST_DEPLOY_KIT = [
326
326
  ".github/dependabot.yml",
327
327
  ".github/workflows/build.yml",
328
328
  "Dockerfile",
329
+ "Dockerfile.prebuilt",
329
330
  "docker-compose.yaml",
331
+ "docker-compose.prebuilt.yaml",
330
332
  "docker-entrypoint.sh",
331
333
  "docker-healthcheck.sh"
332
334
  ];
@@ -535,7 +537,87 @@ updates:
535
537
  "Dockerfile",
536
538
  `# syntax=docker/dockerfile:1.7-labs
537
539
  # check=skip=InvalidDefaultArgInFrom
538
- # ${name}'s deploy image.
540
+ # ${name}'s quick-start deploy image \u2014 built from source, with nothing to
541
+ # set up first.
542
+ #
543
+ # FROM node:26-alpine directly rather than a published Meith base image:
544
+ # Coolify (or a plain \`docker build\`) builds this from this repository, so
545
+ # there is no registry account, no image tag to paste anywhere, and no
546
+ # \`.github/workflows/build.yml\` run to wait on. The cost of that zero setup
547
+ # is that this installs the board's full dependency closure itself (see the
548
+ # \`npm install\` below), so a build here is heavier than \`Dockerfile.prebuilt\`'s
549
+ # thin delta \u2014 that image, pulled rather than built, is the trade the advanced
550
+ # path takes for a low-spec build server or a faster deploy (see \`README.md\`
551
+ # and, in the meith repository, docs/getting-started/deployment/docker-compose.md,
552
+ # "Custom boards").
553
+ #
554
+ # Two stages, not three: unlike the official image, this does not prune down
555
+ # to Next's own standalone output. The migrate role below runs \`community
556
+ # migrate\`, and \`community\` materializes @meith/cli's sources and runs them
557
+ # with tsx at the moment it runs (see the meith repository's
558
+ # docs/contributing/development.md, "Consuming the board from a workspace") \u2014 it needs
559
+ # the full, un-pruned node_modules tree this board installed, not what Next
560
+ # traced as reachable from the web server alone. The tick itself is driven
561
+ # by docker-compose.yaml's own \`worker\` service \u2014 a lightweight loop against
562
+ # /api/system/tick, not a compiled worker process, because @meith/worker is
563
+ # not published (see the meith repository's docs/contributing/release.md).
564
+ FROM node:26-alpine@sha256:aadf416b2cdce311a8811ba3f0608a61b77dbf997500e2eafe781b51f6a0b019 AS deps
565
+ WORKDIR /board
566
+
567
+ # This board's own manifest, cached independently of its source \u2014 editing
568
+ # meith.config.ts should not re-run npm install. Nothing warms node_modules
569
+ # ahead of this the way \`Dockerfile.prebuilt\`'s base image does: the full
570
+ # @meith/web, @meith/cli and @meith/theme-default closure this board depends
571
+ # on is installed here, from scratch, which is the heavier half of the
572
+ # quick-start trade.
573
+ COPY package.json ./
574
+ RUN npm install
575
+
576
+ FROM deps AS runtime
577
+ WORKDIR /board
578
+ COPY . .
579
+
580
+ ENV NEXT_TELEMETRY_DISABLED=1
581
+ ENV NODE_ENV=production
582
+
583
+ # DATA_SOURCE is scoped to this one RUN, not declared with ENV \u2014 an ENV
584
+ # persists into every container started from this image afterward, and this
585
+ # Dockerfile has no later stage to reset it in (see "Two stages, not three"
586
+ # above). The build needs neither a database nor a production secret (see
587
+ # the meith repository's docs/contributing/development.md, "Fixture mode"), but baking
588
+ # DATA_SOURCE=fixture into the image itself would silently force fixture
589
+ # mode \u2014 and with it the in-memory queue driver \u2014 at runtime too, no matter
590
+ # what DATABASE_URL an operator supplies to \`docker run\`.
591
+ RUN DATA_SOURCE=fixture npx forum-web build
592
+
593
+ ENV PORT=3000
594
+ ENV HOSTNAME=0.0.0.0
595
+ EXPOSE 3000
596
+
597
+ # node:alpine already carries a non-root "node" user; the board's own files
598
+ # are copied in as root above, so they need handing over before this drops
599
+ # privilege.
600
+ RUN chown -R node:node /board
601
+ USER node
602
+
603
+ COPY --chown=node:node docker-entrypoint.sh docker-healthcheck.sh ./
604
+ RUN chmod +x docker-entrypoint.sh docker-healthcheck.sh
605
+
606
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \\
607
+ CMD ["./docker-healthcheck.sh"]
608
+
609
+ ENTRYPOINT ["./docker-entrypoint.sh"]
610
+ `
611
+ );
612
+ files.set(
613
+ "Dockerfile.prebuilt",
614
+ `# syntax=docker/dockerfile:1.7-labs
615
+ # check=skip=InvalidDefaultArgInFrom
616
+ # ${name}'s advanced deploy image \u2014 built by \`.github/workflows/build.yml\` and
617
+ # pulled by \`docker-compose.prebuilt.yaml\`. A quick-start board never builds
618
+ # this file directly; it can delete this file, \`docker-compose.prebuilt.yaml\`
619
+ # and \`.github/workflows/build.yml\` outright and keep only \`Dockerfile\` and
620
+ # \`docker-compose.yaml\` (see README.md, "Deploy").
539
621
  #
540
622
  # FROM the published framework base image \u2014 deps + framework layers only,
541
623
  # locked to this exact release (see the meith repository's
@@ -666,10 +748,10 @@ node -e "fetch('http://127.0.0.1:3000/api/ready').then(r=>process.exit(r.ok?0:1)
666
748
  );
667
749
  files.set(
668
750
  ".github/workflows/build.yml",
669
- `# Builds this board's image and pushes it to your own GHCR, on every push to
670
- # main. No secret to configure: GITHUB_TOKEN is provided automatically by
671
- # GitHub Actions and is enough to push to ghcr.io/<this repository>. See
672
- # README.md for the rest of the three-step deploy story.
751
+ `# The advanced/prebuilt path (see README.md, "Deploy"). Builds this board's
752
+ # Dockerfile.prebuilt and pushes it to your own GHCR, on every push to main.
753
+ # No secret to configure: GITHUB_TOKEN is provided automatically by GitHub
754
+ # Actions and is enough to push to ghcr.io/<this repository>.
673
755
  name: Build and push
674
756
 
675
757
  on:
@@ -704,7 +786,7 @@ jobs:
704
786
  echo "::error::@meith/web in package.json is '$MEITH_VERSION', not an exact X.Y.Z version \u2014 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."
705
787
  exit 1
706
788
  fi
707
- docker build --build-arg MEITH_VERSION="$MEITH_VERSION" -t "$IMAGE:\${{ github.sha }}" -t "$IMAGE:latest" .
789
+ docker build -f Dockerfile.prebuilt --build-arg MEITH_VERSION="$MEITH_VERSION" -t "$IMAGE:\${{ github.sha }}" -t "$IMAGE:latest" .
708
790
  docker push "$IMAGE:\${{ github.sha }}"
709
791
  docker push "$IMAGE:latest"
710
792
 
@@ -744,9 +826,141 @@ jobs:
744
826
  );
745
827
  files.set(
746
828
  "docker-compose.yaml",
747
- `# ${name}, deployed by Coolify \u2014 the same shape as the meith repository's own
748
- # docker/compose.coolify.yml: db, migrate, web, worker. See README.md for
749
- # the three-step deploy story this file is the last step of.
829
+ `# ${name}, quick-start deployed by Coolify \u2014 the same shape as the meith
830
+ # repository's own docker/compose.coolify.yml: db, migrate, web, worker. See
831
+ # README.md for the deploy story this file is the last step of.
832
+ #
833
+ # Coolify builds \`Dockerfile\` from this repository itself, so there is no
834
+ # MEITH_IMAGE to set here \u2014 every deploy is a build from source. For a
835
+ # low-spec build server or a faster deploy, use \`docker-compose.prebuilt.yaml\`
836
+ # instead, which pulls the image \`.github/workflows/build.yml\` pushes to GHCR.
837
+ #
838
+ # No published ports \u2014 Coolify's proxy routes to the container and issues
839
+ # the certificate. The two secrets and the database password are Coolify's
840
+ # own "magic variables": it fills them in on the first deploy and shows them
841
+ # in the panel, so nothing here needs a value typed into it. Requires Coolify
842
+ # v4.0.0-beta.411 or newer, which is when magic variables in a compose file
843
+ # from a Git source arrived.
844
+ services:
845
+ postgres:
846
+ image: postgres:18-alpine@sha256:d3e1620b530c944afa6e887d22eb899824da68e19c52024bf98f5220c88a65b2
847
+ restart: unless-stopped
848
+ mem_limit: \${POSTGRES_MEM_LIMIT:-1g}
849
+ cpus: \${POSTGRES_CPUS:-1}
850
+ environment:
851
+ POSTGRES_USER: community
852
+ POSTGRES_PASSWORD: $SERVICE_PASSWORD_POSTGRES
853
+ POSTGRES_DB: community
854
+ volumes:
855
+ - pgdata:/var/lib/postgresql
856
+ healthcheck:
857
+ test: ['CMD-SHELL', 'pg_isready -U community -d community']
858
+ interval: 10s
859
+ timeout: 5s
860
+ retries: 5
861
+
862
+ # Runs to completion, then exits. web waits for it, so the schema is
863
+ # always applied before the first request rather than racing it.
864
+ migrate:
865
+ build: .
866
+ image: ${name}
867
+ environment:
868
+ COMMUNITY_ROLE: migrate
869
+ DATABASE_URL: postgres://community:$SERVICE_PASSWORD_POSTGRES@postgres:5432/community
870
+ AUTH_SECRET: $SERVICE_BASE64_64_AUTH
871
+ TICK_SECRET: $SERVICE_BASE64_64_TICK
872
+ depends_on:
873
+ postgres:
874
+ condition: service_healthy
875
+ restart: 'no'
876
+
877
+ web:
878
+ build: .
879
+ image: ${name}
880
+ restart: unless-stopped
881
+ mem_limit: \${WEB_MEM_LIMIT:-1g}
882
+ cpus: \${WEB_CPUS:-2}
883
+ # A readiness probe Coolify gates a rolling deploy on: with "Rolling
884
+ # update" enabled on the resource, the new container must answer
885
+ # /api/ready before the old one is retired, so a redeploy swaps in with no
886
+ # gap. Without it Coolify recreates the stack \u2014 old removed, then new
887
+ # started \u2014 and the board is down while the new web boots.
888
+ healthcheck:
889
+ 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))"]
890
+ interval: 30s
891
+ timeout: 5s
892
+ start_period: 20s
893
+ retries: 3
894
+ environment:
895
+ # Ask Coolify for a domain on port 3000, then hand the board the same
896
+ # thing with a scheme in front.
897
+ - SERVICE_FQDN_WEB_3000
898
+ - APP_URL=$SERVICE_URL_WEB
899
+ - DATABASE_URL=postgres://community:$SERVICE_PASSWORD_POSTGRES@postgres:5432/community
900
+ - AUTH_SECRET=$SERVICE_BASE64_64_AUTH
901
+ - TICK_SECRET=$SERVICE_BASE64_64_TICK
902
+ - QUEUE_DRIVER=postgres
903
+ - CACHE_DRIVER=next
904
+ - FILESTORE_DRIVER=local
905
+ # Left unset, mail is configured on the board itself \u2014 the installer
906
+ # asks on first run. Set MAIL_DRIVER here and this file wins instead.
907
+ - MAIL_DRIVER=\${MAIL_DRIVER:-log}
908
+ - MAIL_SMTP_HOST=\${MAIL_SMTP_HOST:-}
909
+ - MAIL_SMTP_PORT=\${MAIL_SMTP_PORT:-}
910
+ - MAIL_SMTP_SECURITY=\${MAIL_SMTP_SECURITY:-}
911
+ - MAIL_SMTP_USERNAME=\${MAIL_SMTP_USERNAME:-}
912
+ - MAIL_SMTP_PASSWORD=\${MAIL_SMTP_PASSWORD:-}
913
+ - MAIL_FROM=\${MAIL_FROM:-}
914
+ volumes:
915
+ - uploads:/app/.uploads
916
+ depends_on:
917
+ postgres:
918
+ condition: service_healthy
919
+ migrate:
920
+ condition: service_completed_successfully
921
+
922
+ # @meith/worker is not published (see the meith repository's
923
+ # docs/contributing/release.md), so there is no compiled worker binary a scaffolded
924
+ # board can run \u2014 this drives the tick the alternative way the meith
925
+ # repository documents in docs/getting-started/deployment/docker-compose.md, "Running the tick without
926
+ # a second set of credentials": a small loop calling /api/system/tick.
927
+ worker:
928
+ image: alpine:3.24@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b
929
+ restart: unless-stopped
930
+ mem_limit: \${WORKER_MEM_LIMIT:-64m}
931
+ cpus: \${WORKER_CPUS:-0.25}
932
+ environment:
933
+ TICK_SECRET: $SERVICE_BASE64_64_TICK
934
+ command:
935
+ - sh
936
+ - -c
937
+ - |
938
+ apk add --no-cache curl >/dev/null
939
+ while true; do
940
+ curl -fsS -m 55 -H "Authorization: Bearer $$TICK_SECRET" \\
941
+ http://web:3000/api/system/tick >/dev/null 2>&1 \\
942
+ || echo "tick failed at $$(date -Is)"
943
+ sleep 60
944
+ done
945
+ depends_on:
946
+ - web
947
+
948
+ volumes:
949
+ pgdata:
950
+ uploads:
951
+ `
952
+ );
953
+ files.set(
954
+ "docker-compose.prebuilt.yaml",
955
+ `# ${name}, deployed by Coolify from a prebuilt image \u2014 the advanced path: point
956
+ # Coolify's compose-file at this file instead of docker-compose.yaml once
957
+ # \`.github/workflows/build.yml\` has pushed an image, and set MEITH_IMAGE to
958
+ # what its Summary printed. This trades the quick-start's heavier
959
+ # build-on-every-deploy for a low-spec build server or a faster deploy \u2014 see
960
+ # README.md, "Deploy".
961
+ #
962
+ # Same shape as the meith repository's own docker/compose.coolify.yml: db,
963
+ # migrate, web, worker.
750
964
  #
751
965
  # No published ports \u2014 Coolify's proxy routes to the container and issues
752
966
  # the certificate. The two secrets and the database password are Coolify's
@@ -778,6 +992,10 @@ services:
778
992
  # always applied before the first request rather than racing it.
779
993
  migrate:
780
994
  image: \${MEITH_IMAGE:?set this to the image the build workflow's Summary just printed, e.g. ghcr.io/<you>/${name}:latest}
995
+ # Pull the tag on every deploy. Compose keeps an image it already has, so
996
+ # a rebuilt \`:latest\` is otherwise never fetched and a redeploy quietly
997
+ # runs the old code.
998
+ pull_policy: always
781
999
  environment:
782
1000
  COMMUNITY_ROLE: migrate
783
1001
  DATABASE_URL: postgres://community:$SERVICE_PASSWORD_POSTGRES@postgres:5432/community
@@ -790,9 +1008,21 @@ services:
790
1008
 
791
1009
  web:
792
1010
  image: \${MEITH_IMAGE:?set this to the image the build workflow's Summary just printed, e.g. ghcr.io/<you>/${name}:latest}
1011
+ pull_policy: always
793
1012
  restart: unless-stopped
794
1013
  mem_limit: \${WEB_MEM_LIMIT:-1g}
795
1014
  cpus: \${WEB_CPUS:-2}
1015
+ # A readiness probe Coolify gates a rolling deploy on: with "Rolling
1016
+ # update" enabled on the resource, the new container must answer
1017
+ # /api/ready before the old one is retired, so a redeploy swaps in with no
1018
+ # gap. Without it Coolify recreates the stack \u2014 old removed, then new
1019
+ # started \u2014 and the board is down while the new web boots.
1020
+ healthcheck:
1021
+ 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))"]
1022
+ interval: 30s
1023
+ timeout: 5s
1024
+ start_period: 20s
1025
+ retries: 3
796
1026
  environment:
797
1027
  # Ask Coolify for a domain on port 3000, then hand the board the same
798
1028
  # thing with a scheme in front.
@@ -860,15 +1090,55 @@ A forum, built on [Meith](${repositoryUrl}).
860
1090
 
861
1091
  ## Deploy
862
1092
 
863
- Nothing here builds on your own server \u2014 a 2 GB VPS OOMs on a Next.js build,
864
- which is the whole reason \`Dockerfile\`, \`docker-compose.yaml\` and
865
- \`.github/workflows/build.yml\` exist: something else builds the image, the
866
- server only ever pulls one. Three steps, nothing to configure by hand beyond
867
- one value only you know:
1093
+ Two paths onto [Coolify](https://coolify.io), both ending at the same
1094
+ \`/install\`. **Quick start** is the default and needs nothing but a push;
1095
+ **advanced/prebuilt** moves the build off the server, onto GitHub Actions, for
1096
+ a low-spec build server or a faster deploy. Pick one \u2014 a board only ever runs
1097
+ one of them at a time.
1098
+
1099
+ ### Quick start (default)
1100
+
1101
+ Coolify builds the image itself, from this repository, every time it
1102
+ deploys \u2014 there is nothing to push anywhere first and no image tag to paste
1103
+ in. Two steps:
1104
+
1105
+ 1. **Push this repository to GitHub.**
1106
+
1107
+ 2. **Point Coolify at \`docker-compose.yaml\`** \u2014 a **Public Git repository**
1108
+ resource with **Docker Compose** as its build pack, this repository as its
1109
+ source. The name is Coolify's own default, so its **Compose file** field is
1110
+ already right when the form opens, and the file already carries Coolify's
1111
+ own "magic variables" for \`AUTH_SECRET\`, \`TICK_SECRET\` and the database
1112
+ password, generated on the first deploy and never typed in. Nothing else to
1113
+ set: \`docker-compose.yaml\` builds \`web\` and \`migrate\` from \`Dockerfile\`
1114
+ itself, so there is no \`MEITH_IMAGE\` here at all.
1115
+
1116
+ 3. **Deploy, then \`/install\` on your own domain.** Coolify issues the
1117
+ certificate; the installer from there is the one
1118
+ [docs/getting-started/deployment/coolify.md](${repositoryUrl}/blob/main/docs/getting-started/deployment/coolify.md#4-run-the-installer)
1119
+ walks through, screen for screen. It seals itself when it finishes, and
1120
+ \`/install\` answers 404 from then on \u2014 run it **against the database you
1121
+ are going to keep**. Every push to \`main\` after this is picked up the next
1122
+ time Coolify's own **Redeploy** button runs \u2014 pushing alone does not
1123
+ rebuild it.
1124
+
1125
+ The trade for that zero setup is a heavier build: \`Dockerfile\` installs this
1126
+ board's full dependency closure on the server itself, on every deploy, rather
1127
+ than starting from a warm base image. A 2 GB VPS can OOM on it. If that is
1128
+ your server, use the advanced path below instead.
1129
+
1130
+ A quick-start board never needs \`Dockerfile.prebuilt\`,
1131
+ \`docker-compose.prebuilt.yaml\` or \`.github/workflows/build.yml\` \u2014 delete all
1132
+ three.
1133
+
1134
+ ### Advanced / prebuilt \u2014 for a low-spec server or a faster deploy
1135
+
1136
+ Something else builds the image ahead of time; the server only ever pulls
1137
+ one. Three steps, nothing to configure by hand beyond one value only you know:
868
1138
 
869
1139
  1. **Push this repository to GitHub.** \`.github/workflows/build.yml\` builds
870
- \`Dockerfile\` on every push to \`main\` and pushes the result to your own
871
- GitHub Container Registry, \`ghcr.io/<you>/${name}\` \u2014 using only the
1140
+ \`Dockerfile.prebuilt\` on every push to \`main\` and pushes the result to your
1141
+ own GitHub Container Registry, \`ghcr.io/<you>/${name}\` \u2014 using only the
872
1142
  \`GITHUB_TOKEN\` every GitHub Actions run already carries. No secret to
873
1143
  add, no registry account beyond the GitHub account you already have.
874
1144
 
@@ -879,29 +1149,26 @@ one value only you know:
879
1149
  repository usually lands public already, and a private one fails
880
1150
  Coolify's pull with an authentication error no operator can act on.
881
1151
 
882
- 2. **Point [Coolify](https://coolify.io) at \`docker-compose.yaml\`** \u2014 a
1152
+ 2. **Point Coolify at \`docker-compose.prebuilt.yaml\`** \u2014 a
883
1153
  **Public Git repository** resource with **Docker Compose** as its build
884
- pack, this repository as its source. The name is Coolify's own default,
885
- so its **Compose file** field is already right when the form opens, and
886
- the file already carries Coolify's own "magic variables" for
887
- \`AUTH_SECRET\`, \`TICK_SECRET\` and the database password, generated on
888
- the first deploy and never typed in. The one thing Coolify cannot
889
- generate is the image step 1 just pushed: set \`MEITH_IMAGE\` in the
1154
+ pack, this repository as its source, and its **Compose file** field
1155
+ changed from Coolify's default of \`docker-compose.yaml\` to
1156
+ \`docker-compose.prebuilt.yaml\`. That file carries Coolify's own "magic
1157
+ variables" for \`AUTH_SECRET\`, \`TICK_SECRET\` and the database password,
1158
+ generated on the first deploy and never typed in. The one thing Coolify
1159
+ cannot generate is the image step 1 just pushed: set \`MEITH_IMAGE\` in the
890
1160
  resource's own environment to one of the two values that run's Summary
891
- printed (\`docker-compose.yaml\` refuses to start without it, with a
892
- message saying why). \`ghcr.io/<you>/${name}:\${{ github.sha }}\` names
1161
+ printed (\`docker-compose.prebuilt.yaml\` refuses to start without it, with
1162
+ a message saying why). \`ghcr.io/<you>/${name}:\${{ github.sha }}\` names
893
1163
  that one build and nothing else, ever; \`ghcr.io/<you>/${name}:latest\`
894
1164
  follows \`main\` instead, so installing a plugin later is a push and a
895
- **Redeploy** \u2014 the trade the quickstart takes, at the cost of an
896
- unrelated redeploy pulling whatever \`main\` most recently built.
1165
+ **Redeploy** \u2014 the trade this path takes, at the cost of an unrelated
1166
+ redeploy pulling whatever \`main\` most recently built.
897
1167
 
898
- 3. **Deploy, then \`/install\` on your own domain.** Coolify issues the
899
- certificate; the installer from there is the one
1168
+ 3. **Deploy, then \`/install\` on your own domain.** Same installer, same
900
1169
  [docs/getting-started/deployment/coolify.md](${repositoryUrl}/blob/main/docs/getting-started/deployment/coolify.md#4-run-the-installer)
901
- walks through, screen for screen. It seals itself when it finishes, and
902
- \`/install\` answers 404 from then on \u2014 run it **against the database you
903
- are going to keep**. Every push to \`main\` after this rebuilds the
904
- image; Coolify's own **Redeploy** button is what actually pulls it \u2014
1170
+ walk-through, same one-time seal. Every push to \`main\` after this rebuilds
1171
+ the image; Coolify's own **Redeploy** button is what actually pulls it \u2014
905
1172
  pushing alone does not.
906
1173
 
907
1174
  No Docker Hub, no paid CI: GitHub Actions' free tier and GHCR are the whole
@@ -909,22 +1176,23 @@ build side of this, for a board of any size.
909
1176
 
910
1177
  **Building it yourself**: works on any machine with Docker, if you would
911
1178
  rather not use GitHub Actions for the build \u2014 push the result wherever
912
- \`docker-compose.yaml\`'s \`MEITH_IMAGE\` can reach.
1179
+ \`docker-compose.prebuilt.yaml\`'s \`MEITH_IMAGE\` can reach.
913
1180
 
914
1181
  \`\`\`sh
915
- docker build --build-arg MEITH_VERSION=$(node -p "require('./package.json').dependencies['@meith/web']") -t ${name} .
1182
+ docker build -f Dockerfile.prebuilt --build-arg MEITH_VERSION=$(node -p "require('./package.json').dependencies['@meith/web']") -t ${name} .
916
1183
  \`\`\`
917
1184
 
918
1185
  **Without a panel**: [docs/getting-started/deployment/docker-compose.md](${repositoryUrl}/blob/main/docs/getting-started/deployment/docker-compose.md)
919
1186
  is the same four containers by hand \u2014 your own \`.env\`, a reverse proxy you
920
1187
  already run, no Coolify. \`Dockerfile\` and \`docker-compose.yaml\` here are this
921
- board's own version of exactly that shape.
1188
+ board's own version of exactly that shape (or \`Dockerfile.prebuilt\` and
1189
+ \`docker-compose.prebuilt.yaml\`, for the advanced path).
922
1190
 
923
- Two things nothing configures for you:
1191
+ Two things nothing configures for you, on either path:
924
1192
 
925
1193
  - **Mail.** Until \`MAIL_DRIVER\` and its three settings exist, every message is
926
1194
  written to the log and delivered to nobody, so password reset fails silently.
927
- - **The tick.** \`docker-compose.yaml\`'s \`worker\` service drives it here \u2014 a small
1195
+ - **The tick.** The compose file's \`worker\` service drives it here \u2014 a small
928
1196
  loop calling \`/api/system/tick\` once a minute, since \`@meith/web\`'s own
929
1197
  worker package is not something a board outside the meith monorepo can
930
1198
  depend on yet. Deploy some other way and something still has to call that
@@ -985,14 +1253,18 @@ weekly pull request bumping the actions pinned in
985
1253
  \`.github/workflows/build.yml\`, which is a safe, independent update the two
986
1254
  commands above never touch.
987
1255
 
988
- That \`package.json\` change is the whole pin: \`Dockerfile\`'s own
989
- \`FROM\` line takes the version as a build argument, and
990
- \`.github/workflows/build.yml\` reads it straight out of \`package.json\`'s
991
- own \`@meith/web\` dependency when it rebuilds \u2014 nothing in \`Dockerfile\`
992
- itself to keep in sync by hand. \`--save-exact\` matters: npm's default
993
- \`save-prefix\` is \`^\`, and a caret range is not a legal Docker image tag \u2014
994
- without it, this exact command would write \`"^0.18.0"\` and the next build
995
- would fail with \`invalid reference format\` instead of building. This
1256
+ On the quick-start path there is no version to keep in sync by hand:
1257
+ \`Dockerfile\` runs \`npm install\` straight from this \`package.json\` on every
1258
+ build, so a rebuild always picks up whatever is pinned there. On the
1259
+ advanced/prebuilt path, that \`package.json\` change is the whole pin:
1260
+ \`Dockerfile.prebuilt\`'s own \`FROM\` line takes the version as a build argument,
1261
+ and \`.github/workflows/build.yml\` reads it straight out of \`package.json\`'s
1262
+ own \`@meith/web\` dependency when it rebuilds \u2014 nothing in
1263
+ \`Dockerfile.prebuilt\` itself to keep in sync by hand. \`--save-exact\` matters
1264
+ either way: npm's default \`save-prefix\` is \`^\`, and a caret range is not a
1265
+ legal Docker image tag for the advanced path \u2014 without it, this exact command
1266
+ would write \`"^0.18.0"\` and the next \`Dockerfile.prebuilt\` build would fail
1267
+ with \`invalid reference format\` instead of building. This
996
1268
  project's own \`.npmrc\` sets \`save-exact=true\` for the same reason, so an
997
1269
  \`npm install\` of anything else here \u2014 a plugin, say \u2014 stays pinned too; the
998
1270
  build workflow also refuses to build from anything but an exact version, as
@@ -1890,7 +2162,7 @@ async function run(argv, version) {
1890
2162
  }
1891
2163
 
1892
2164
  // src/bin.ts
1893
- var result = await run(process.argv.slice(2), "0.26.0");
2165
+ var result = await run(process.argv.slice(2), "0.26.1");
1894
2166
  for (const line of result.lines) {
1895
2167
  if (result.code === 0) console.log(line);
1896
2168
  else console.error(line);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-meith",
3
- "version": "0.26.0",
3
+ "version": "0.26.1",
4
4
  "description": "Scaffold a Meith board, plugin or theme — npx create-meith <name> writes a deployable board workspace; --plugin and --theme write extension workspaces built on the published kits.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/bin.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { run } from './cli'
3
3
 
4
- const result = await run(process.argv.slice(2), '0.26.0')
4
+ const result = await run(process.argv.slice(2), '0.26.1')
5
5
  for (const line of result.lines) {
6
6
  if (result.code === 0) console.log(line)
7
7
  else console.error(line)
package/src/scaffold.ts CHANGED
@@ -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
@@ -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 \`community
596
+ # migrate\`, and \`community\` 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
@@ -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
- `# Builds this board's image and pushes it to your own GHCR, on every push to
714
- # main. No secret to configure: GITHUB_TOKEN is provided automatically by
715
- # GitHub Actions and is enough to push to ghcr.io/<this repository>. See
716
- # README.md for the rest of the three-step deploy story.
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 repository's own
793
- # docker/compose.coolify.yml: db, migrate, web, worker. See README.md for
794
- # the three-step deploy story this file is the last step of.
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
+ COMMUNITY_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,6 +1039,10 @@ 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
  COMMUNITY_ROLE: migrate
828
1048
  DATABASE_URL: postgres://community:$SERVICE_PASSWORD_POSTGRES@postgres:5432/community
@@ -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
- Nothing here builds on your own server a 2 GB VPS OOMs on a Next.js build,
910
- which is the whole reason \`Dockerfile\`, \`docker-compose.yaml\` and
911
- \`.github/workflows/build.yml\` exist: something else builds the image, the
912
- server only ever pulls one. Three steps, nothing to configure by hand beyond
913
- one value only you know:
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 own
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 [Coolify](https://coolify.io) at \`docker-compose.yaml\`** — a
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. The name is Coolify's own default,
931
- so its **Compose file** field is already right when the form opens, and
932
- the file already carries Coolify's own "magic variables" for
933
- \`AUTH_SECRET\`, \`TICK_SECRET\` and the database password, generated on
934
- the first deploy and never typed in. The one thing Coolify cannot
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 a
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 the quickstart takes, at the cost of an
942
- unrelated redeploy pulling whatever \`main\` most recently built.
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.** Coolify issues the
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
- walks through, screen for screen. It seals itself when it finishes, and
948
- \`/install\` answers 404 from then on run it **against the database you
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,22 +1224,23 @@ 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.** \`docker-compose.yaml\`'s \`worker\` service drives it here — a small
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
@@ -1031,14 +1301,18 @@ weekly pull request bumping the actions pinned in
1031
1301
  \`.github/workflows/build.yml\`, which is a safe, independent update the two
1032
1302
  commands above never touch.
1033
1303
 
1034
- That \`package.json\` change is the whole pin: \`Dockerfile\`'s own
1035
- \`FROM\` line takes the version as a build argument, and
1036
- \`.github/workflows/build.yml\` reads it straight out of \`package.json\`'s
1037
- own \`@meith/web\` dependency when it rebuilds nothing in \`Dockerfile\`
1038
- itself to keep in sync by hand. \`--save-exact\` matters: npm's default
1039
- \`save-prefix\` is \`^\`, and a caret range is not a legal Docker image tag —
1040
- without it, this exact command would write \`"^0.18.0"\` and the next build
1041
- would fail with \`invalid reference format\` instead of building. This
1304
+ On the quick-start path there is no version to keep in sync by hand:
1305
+ \`Dockerfile\` runs \`npm install\` straight from this \`package.json\` on every
1306
+ build, so a rebuild always picks up whatever is pinned there. On the
1307
+ advanced/prebuilt path, that \`package.json\` change is the whole pin:
1308
+ \`Dockerfile.prebuilt\`'s own \`FROM\` line takes the version as a build argument,
1309
+ and \`.github/workflows/build.yml\` reads it straight out of \`package.json\`'s
1310
+ own \`@meith/web\` dependency when it rebuilds nothing in
1311
+ \`Dockerfile.prebuilt\` itself to keep in sync by hand. \`--save-exact\` matters
1312
+ either way: npm's default \`save-prefix\` is \`^\`, and a caret range is not a
1313
+ legal Docker image tag for the advanced path — without it, this exact command
1314
+ would write \`"^0.18.0"\` and the next \`Dockerfile.prebuilt\` build would fail
1315
+ with \`invalid reference format\` instead of building. This
1042
1316
  project's own \`.npmrc\` sets \`save-exact=true\` for the same reason, so an
1043
1317
  \`npm install\` of anything else here — a plugin, say — stays pinned too; the
1044
1318
  build workflow also refuses to build from anything but an exact version, as