profileShare

rasmusjy / roundtable

Read-only snapshot

No repository description.

main default branch 181 files Expires Sep 13, 2026, 9:06 AM

Commit

Add the container build and deploy script

The deploy target is not in the repository: this code is shared read-only with
people outside the project, and the server address and login are not theirs to
have. Set VPS=user@host in your shell before running deploy.sh.
commit dc4d3c3

5 changed files with +197 and −5

Jump to a changed file
  1. Dockerfile +19 −4
  2. deploy.sh +91 −0
  3. docker-compose.vps.yml +86 −0
  4. docker/entrypoint.sh +1 −1
  5. public/.gitkeep +0 −0
modified Dockerfile +19 −4
@@ -30,6 +30,20 @@ENV BUILD_STANDALONE=1
30 30 RUN pnpm prisma generate
31 31 RUN pnpm build
32 32
33 +# --- Prisma CLI for the entrypoint ----------------------------------------
34 +# The entrypoint applies the schema at boot, which needs the CLI and its whole
35 +# dependency tree. Lifting that out of the pnpm store means chasing symlinks
36 +# through one store directory per transitive package, so it is installed once
37 +# more here with npm, whose flat layout copies as a single directory.
38 +#
39 +# The version is read from package.json, so it can never drift from the client
40 +# the app was generated against.
41 +FROM base AS prismacli
42 +WORKDIR /prisma-cli
43 +COPY package.json ./
44 +RUN npm install --omit=dev --no-package-lock \
45 + "prisma@$(node -p "require('./package.json').devDependencies.prisma")"
46 +
33 47 # --- Runtime image ---------------------------------------------------------
34 48 FROM base AS runner
35 49 ENV NODE_ENV=production
@@ -44,11 +58,12 @@COPY --from=builder /app/public ./public
44 58 COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
45 59 COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
46 60
47 -# Prisma CLI + engine + schema so the entrypoint can apply the schema at boot.
61 +# Schema plus the CLI that applies it at boot. The CLI stays outside
62 +# ./node_modules on purpose: a second @prisma directory beside the standalone
63 +# bundle would shadow the generated client Next already traced into it, and the
64 +# app would start against an empty client.
48 65 COPY --from=builder /app/prisma ./prisma
49 -COPY --from=builder /app/node_modules/prisma ./node_modules/prisma
50 -COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma
51 -COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
66 +COPY --from=prismacli /prisma-cli/node_modules /prisma-cli/node_modules
52 67
53 68 COPY --chown=nextjs:nodejs docker/entrypoint.sh ./entrypoint.sh
54 69 RUN chmod +x ./entrypoint.sh
added deploy.sh +91 −0
@@ -0,0 +1,91 @@
1 +#!/usr/bin/env bash
2 +# One-command redeploy for roundtable.rasmusj.com.
3 +#
4 +# Ships the source to the VPS, builds the image there and restarts the stack
5 +# described by docker-compose.vps.yml. Run from Git Bash on Windows: ./deploy.sh
6 +#
7 +# Two things this script deliberately never touches:
8 +#
9 +# .env lives on the server only. It holds ENCRYPTION_KEY, which every
10 +# saved BYOK key is encrypted with: overwrite it and those keys
11 +# can never be read back.
12 +# Caddyfile the shared reverse proxy at /opt/caddy serves every site on the
13 +# box, so a routine redeploy cannot take the others down with it.
14 +#
15 +# Postgres lives in a named Docker volume and survives every rebuild.
16 +#
17 +# ./deploy.sh build, restart, wait for health
18 +# ./deploy.sh --seed the same, then re-seed the public /demo debates
19 +set -euo pipefail
20 +
21 +# Deploy target. Kept out of the repository on purpose: this code is shared
22 +# read-only with people outside the project, and the server address and
23 +# login are not theirs to have. Set it once in your shell:
24 +#
25 +# export VPS=user@host
26 +#
27 +VPS=${VPS:?set VPS=user@host before deploying}
28 +DEST=/opt/projects/roundtable
29 +COMPOSE="docker compose -f docker-compose.vps.yml"
30 +
31 +cd "$(dirname "$0")"
32 +
33 +SEED=0
34 +[ "${1:-}" = "--seed" ] && SEED=1
35 +
36 +echo "==> Running the test suite before shipping anything..."
37 +# Git Bash on Windows does not see a globally installed pnpm, so fall back to
38 +# the one npx fetches rather than failing before anything has been checked.
39 +if command -v pnpm >/dev/null 2>&1; then PNPM=pnpm; else PNPM="npx --yes pnpm"; fi
40 +$PNPM test
41 +
42 +echo "==> Ensuring VPS project dir exists..."
43 +ssh "$VPS" "mkdir -p '$DEST'"
44 +
45 +echo "==> Checking that .env exists on the server..."
46 +if ! ssh "$VPS" "test -f '$DEST/.env'"; then
47 + echo ""
48 + echo " $DEST/.env is missing. Create it on the server first, see DEPLOY.md."
49 + echo " Nothing has been changed."
50 + exit 1
51 +fi
52 +
53 +echo "==> Uploading source (node_modules, .next, .env and coverage stay out of it)..."
54 +tar --exclude=node_modules --exclude=.next --exclude=.env --exclude=.git \
55 + --exclude=coverage --exclude=tsconfig.tsbuildinfo -czf - . | ssh "$VPS" "
56 + rm -rf '$DEST/src.tmp' &&
57 + mkdir -p '$DEST/src.tmp' &&
58 + tar -C '$DEST/src.tmp' -xzf - &&
59 + cp '$DEST/.env' '$DEST/src.tmp/.env' &&
60 + rm -rf '$DEST/app' &&
61 + mv '$DEST/src.tmp' '$DEST/app'
62 +"
63 +
64 +echo "==> Building and restarting the stack..."
65 +ssh "$VPS" "cd '$DEST/app' && $COMPOSE up -d --build"
66 +
67 +if [ "$SEED" = 1 ]; then
68 + echo "==> Seeding the public demo debates..."
69 + # The runtime image is the pruned standalone bundle and has no tsx, so the
70 + # seed runs from the build stage instead, on the same network as Postgres.
71 + ssh "$VPS" "cd '$DEST/app' &&
72 + docker build --target builder -t roundtable-seed . &&
73 + docker run --rm --network roundtable_internal --env-file .env \
74 + -e DATABASE_URL=\"postgresql://roundtable:\$(grep '^POSTGRES_PASSWORD=' .env | cut -d= -f2-)@db:5432/roundtable?schema=public\" \
75 + roundtable-seed pnpm db:seed
76 + "
77 +fi
78 +
79 +echo "==> Waiting for the health check to go green..."
80 +ssh "$VPS" "
81 + for i in \$(seq 1 20); do
82 + state=\$(docker inspect -f '{{.State.Health.Status}}' roundtable 2>/dev/null || echo starting)
83 + [ \"\$state\" = healthy ] && echo ' healthy' && exit 0
84 + sleep 3
85 + done
86 + echo ' still not healthy, check: docker logs roundtable'
87 + exit 1
88 +"
89 +
90 +echo ""
91 +echo "==> Done. Live at https://roundtable.rasmusj.com"
added docker-compose.vps.yml +86 −0
@@ -0,0 +1,86 @@
1 +# roundtable.rasmusj.com, the shared-host variant of docker-compose.yml.
2 +#
3 +# The VPS already runs one Caddy for every site on it, so this file leaves TLS
4 +# and the domain out entirely: nothing is published to the host and the proxy
5 +# reaches the app by container name over the external `web` network. The domain
6 +# lives in /opt/caddy/Caddyfile alone.
7 +#
8 +# Postgres stays off `web`. Every other project on the box shares that network,
9 +# and a database has no business being reachable from any of them.
10 +#
11 +# Deploy with ./deploy.sh, which builds this on the server.
12 +
13 +# Named explicitly because every project on this host is deployed into a
14 +# directory called `app`, and Compose would otherwise derive the same project
15 +# name for all of them and treat the neighbours' containers as orphans.
16 +name: roundtable
17 +
18 +services:
19 + db:
20 + image: postgres:16-alpine
21 + container_name: roundtable-db
22 + restart: unless-stopped
23 + environment:
24 + POSTGRES_USER: roundtable
25 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
26 + POSTGRES_DB: roundtable
27 + volumes:
28 + - roundtable-pgdata:/var/lib/postgresql/data
29 + networks:
30 + - internal
31 + healthcheck:
32 + test: ['CMD-SHELL', 'pg_isready -U roundtable -d roundtable']
33 + interval: 5s
34 + timeout: 5s
35 + retries: 12
36 + logging:
37 + driver: json-file
38 + options:
39 + max-size: '10m'
40 + max-file: '3'
41 +
42 + app:
43 + build:
44 + context: .
45 + image: roundtable:vps
46 + container_name: roundtable
47 + restart: unless-stopped
48 + depends_on:
49 + db:
50 + condition: service_healthy
51 + environment:
52 + DATABASE_URL: postgresql://roundtable:${POSTGRES_PASSWORD}@db:5432/roundtable?schema=public
53 + ENCRYPTION_KEY: ${ENCRYPTION_KEY:?set ENCRYPTION_KEY in .env}
54 + AUTH_TRUST_HOST: 'true'
55 + OPENROUTER_APP_URL: ${OPENROUTER_APP_URL:-https://roundtable.rasmusj.com}
56 + OPENROUTER_APP_TITLE: ${OPENROUTER_APP_TITLE:-Roundtable}
57 + # Inference is always paid by the visitor's own OpenRouter key, so the
58 + # public site costs nothing to run. /demo replays seeded debates and needs
59 + # no key at all.
60 + MOCK_LLM: ${MOCK_LLM:-0}
61 + RATE_LIMIT_DEBATES_PER_HOUR: ${RATE_LIMIT_DEBATES_PER_HOUR:-10}
62 + PORT: '3000'
63 + networks:
64 + - web
65 + - internal
66 + expose:
67 + - '3000'
68 + healthcheck:
69 + test: ['CMD', 'node', '-e', "fetch('http://127.0.0.1:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
70 + interval: 30s
71 + timeout: 5s
72 + start_period: 40s
73 + retries: 3
74 + logging:
75 + driver: json-file
76 + options:
77 + max-size: '10m'
78 + max-file: '3'
79 +
80 +volumes:
81 + roundtable-pgdata:
82 +
83 +networks:
84 + web:
85 + external: true
86 + internal:
modified docker/entrypoint.sh +1 −1
@@ -5,7 +5,7 @@
5 5 set -e
6 6
7 7 echo "→ Applying database schema…"
8 -node node_modules/prisma/build/index.js db push --skip-generate
8 +node /prisma-cli/node_modules/prisma/build/index.js db push --skip-generate --schema ./prisma/schema.prisma
9 9
10 10 echo "→ Starting Roundtable on port ${PORT:-3000}…"
11 11 exec node server.js
added public/.gitkeep +0 −0

Line changes are not available for this file.