Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ ENV NODE_ENV=production \

RUN corepack enable

# git is for a container that runs from a checkout on a volume and updates
# itself (TSBB_CHECKOUT_DIR — see bin/entrypoint.sh). An image without it can
# only ever be replaced by another image.
RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates \
&& rm -rf /var/lib/apt/lists/*

# The whole tree is copied before installing rather than listing each workspace
# manifest by hand. Listing them is faster to rebuild and silently wrong: a new
# package is invisible to pnpm until it happens to gain an external dependency.
Expand All @@ -23,5 +30,7 @@ RUN pnpm install --frozen-lockfile --ignore-scripts
EXPOSE 3000

# Migrations run at boot inside the server, so a deploy can never leave new code
# running against an old schema.
CMD ["node", "apps/server/src/index.ts"]
# running against an old schema. The entrypoint runs the image's own code
# unless TSBB_CHECKOUT_DIR says to run — and keep updating — a checkout on a
# volume instead.
CMD ["sh", "bin/entrypoint.sh"]
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ one with a fresh session secret and whatever you already had set.
| `TSBB_UPDATES` | on | Set to `off` and the board never checks for a release |
| `TSBB_RESTART` | `respawn` | `exit` under a supervisor that restarts the process itself |
| `TSBB_UPDATE_REPO` | `profullstack/tsbb` | A fork that publishes its own releases |
| `TSBB_CHECKOUT_DIR` | — | A directory on a volume; the container runs, and updates, a checkout there |

The worker runs inside the server by default, so email works from one command.

Expand All @@ -284,10 +285,19 @@ out, and a **Check now** button; with automatic updates off it also offers
**Update now**, and `tsbb update` does the same from a shell.

Two things it will not do. It never touches a checkout with local changes —
that is somebody's work, and it stops and says so. And it never updates a
container: the Docker image has no `.git` to move, so a board on Railway, Fly
or a plain `docker run` sees the notice and is updated by redeploying the image,
which is how those platforms expect it anyway.
that is somebody's work, and it stops and says so. And it never updates an
image: the Docker image has no `.git` to move, so a board running the image's
own code sees the notice and is updated by redeploying, which is how those
platforms expect it anyway.

A container *can* keep itself current if it has a volume. Set
`TSBB_CHECKOUT_DIR` to a directory on it (`/app/data/app`, say) and the
container clones the repository there on first boot — at the image's own
commit, with dependencies linked from the image's package store, so the first
start is as fast as any other — and runs from that checkout instead. Each release is then
fetched into the volume and the server restarted from it, and a container
restart comes back on the release it had, not the image it was built from.
`bbs.hqtui.com` runs this way.

On restart the board spawns a fresh copy of itself with the same command line
and exits, so `pnpm start` in a terminal or under nohup carries on with the new
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@profullstack/tsbb",
"version": "0.3.0",
"version": "0.4.0",
"description": "A TypeScript bulletin board. Forums, plugins, avatars, signatures and a terminal client.",
"type": "module",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion apps/mcp/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@profullstack/tsbb-mcp",
"version": "0.3.0",
"version": "0.4.0",
"description": "MCP server for a tsbb bulletin board. Lets an AI assistant read and post on any board, as the member whose token it holds.",
"type": "module",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tsbb/server",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"type": "module",
"dependencies": {
Expand Down
2 changes: 1 addition & 1 deletion apps/tui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@profullstack/tsbb-tui",
"version": "0.3.0",
"version": "0.4.0",
"description": "Terminal client for a tsbb bulletin board. Reads and posts against any centralised install.",
"type": "module",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion apps/worker/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tsbb/worker",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"type": "module",
"dependencies": {
Expand Down
63 changes: 63 additions & 0 deletions bin/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/bin/sh
#
# How the container starts.
#
# With no TSBB_CHECKOUT_DIR it runs the code baked into the image, which is
# what an image is for: it is updated by building and deploying a new one.
#
# With TSBB_CHECKOUT_DIR pointing into a volume, the container instead keeps a
# git checkout THERE and runs from it. On first boot it clones the repository
# at the image's own commit and installs from the image's own package store,
# so the first start needs no download and passes the healthcheck like any
# other. From then on the board updates itself: the server fetches each new
# release into the checkout, exits, and this script starts it again from the
# new code — so a board on a platform that only redeploys images still follows
# releases within minutes, and survives a container restart because the
# checkout is on the volume, not in the container.
set -eu

IMAGE_ROOT="${TSBB_IMAGE_ROOT:-/app}"
DIR="${TSBB_CHECKOUT_DIR:-}"
REPO="${TSBB_UPDATE_REPO:-profullstack/tsbb}"

if [ -z "$DIR" ]; then
cd "$IMAGE_ROOT"
exec node apps/server/src/index.ts
fi

if [ ! -d "$DIR/.git" ]; then
echo "[tsbb] first boot: cloning $REPO into $DIR"
rm -rf "$DIR"
git clone --quiet "https://github.com/$REPO" "$DIR"
# Start from exactly the commit this image was built from, so the checkout
# and the image agree, so the install below finds everything in the store.
if [ -n "${RAILWAY_GIT_COMMIT_SHA:-}" ]; then
git -C "$DIR" -c advice.detachedHead=false checkout --quiet "$RAILWAY_GIT_COMMIT_SHA" \
|| echo "[tsbb] commit ${RAILWAY_GIT_COMMIT_SHA} is not in the clone; staying on the default branch"
fi
# The image's install left every package in pnpm's store, so linking them
# into the new checkout is seconds and needs no network. Copying the image's
# node_modules instead does not work: pnpm sees a tree made for another path
# and stops to ask whether it may rebuild it. CI=true is the "yes" it wants
# if the lockfile has drifted and it must.
echo "[tsbb] installing dependencies from the image's store"
( cd "$DIR" && CI=true pnpm install --frozen-lockfile --ignore-scripts --offline ) \
|| ( cd "$DIR" && CI=true pnpm install --frozen-lockfile --ignore-scripts )
fi

cd "$DIR"
echo "[tsbb] running from $DIR at $(git rev-parse --short HEAD) ($(git describe --tags --always 2>/dev/null))"

# The updater exits after installing a release; this loop is the supervisor
# that brings the new code up. A crash waits a few seconds so a broken build
# cannot spin the container.
export TSBB_RESTART=exit
while :; do
if node apps/server/src/index.ts; then
echo "[tsbb] server exited; starting again from $(git rev-parse --short HEAD)"
else
code=$?
echo "[tsbb] server exited with status $code; starting again in 5s"
sleep 5
fi
done
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tsbb",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"type": "module",
"packageManager": "pnpm@11.18.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tsbb/client",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"type": "module",
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tsbb/core",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"type": "module",
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion packages/db/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tsbb/db",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"type": "module",
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion packages/design-tokens/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tsbb/design-tokens",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"type": "module",
"exports": { "./tokens.css": "./src/tokens.css" }
Expand Down
2 changes: 1 addition & 1 deletion packages/mail/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tsbb/mail",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"type": "module",
"exports": { ".": "./src/index.ts" },
Expand Down
2 changes: 1 addition & 1 deletion packages/markup/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tsbb/markup",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"type": "module",
"exports": { ".": "./src/index.ts" }
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tsbb/mcp",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"type": "module",
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tsbb/plugin-api",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"type": "module",
"exports": { ".": "./src/index.ts" }
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-host/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tsbb/plugin-host",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"type": "module",
"exports": { ".": "./src/index.ts" },
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tsbb/ui",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"type": "module",
"exports": { ".": "./src/index.ts", "./styles": "./src/styles/index.ts" },
Expand Down
4 changes: 2 additions & 2 deletions railway.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
"dockerfilePath": "Dockerfile"
},
"deploy": {
"startCommand": "node apps/server/src/index.ts",
"startCommand": "sh bin/entrypoint.sh",
"healthcheckPath": "/healthz",
"healthcheckTimeout": 120,
"healthcheckTimeout": 300,
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 10,
"numReplicas": 1
Expand Down
Loading