From 8bba60ea2bf3c5f01de59bad60f4e109ca493849 Mon Sep 17 00:00:00 2001 From: Abhishek Krishna Date: Tue, 26 May 2026 09:38:27 +0530 Subject: [PATCH 1/2] =?UTF-8?q?chore(deploy):=20add=20deploy/deploy.sh=20?= =?UTF-8?q?=E2=80=94=20prod=20deploy=20for=20muzix.kcolbchain.com?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codifies the existing manual deploy steps so the next push doesn't need hand-rolling. The non-obvious step is that Next.js standalone output deliberately omits web/public/ and web/.next/static — they have to be copied into the standalone tree after every build, otherwise the labs (/mixdown.html, /labelton.html) 404 in prod. Pipeline: git pull origin/main → npm install → npm run build → cp web/public → standalone/public → cp .next/static → standalone/web/.next/static → systemctl restart muzix-web → verify is-active deploy/README.md documents the on-VPS layout for future operators. --- deploy/README.md | 48 +++++++++++++++++++++++++++++++++++++++++ deploy/deploy.sh | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 deploy/README.md create mode 100755 deploy/deploy.sh diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 00000000..e48b7276 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,48 @@ +# Muzix deploy + +`muzix.kcolbchain.com` is a Next.js standalone build behind Caddy on the +shared kcolbchain VPS. There is no GitHub Action and no webhook — +deploys are triggered by SSHing into the VPS and running this script. + +## Layout on the VPS + +| Path | What | +|------|------| +| `/opt/muzix/` | Git checkout of `kcolbchain/muzix` (the `main` branch is prod) | +| `/opt/muzix/web/.next/standalone/` | Next.js standalone build (`server.js`) — what systemd runs | +| `/opt/muzix/web/.next/standalone/public/` | **Hand-wired** — Next.js does not copy `public/` into standalone output | +| `/opt/muzix/web/.next/standalone/web/.next/static/` | **Hand-wired** — Next.js does not copy `.next/static` into standalone output | +| `/etc/systemd/system/muzix-web.service` | systemd unit — `node /opt/muzix/web/.next/standalone/server.js` on `127.0.0.1:3700` | +| `/etc/caddy/conf.d/muzix.caddy` | Caddy reverse-proxy to `127.0.0.1:3700` | + +## Deploy + +```bash +ssh user@ '/opt/muzix/deploy/deploy.sh' +``` + +The script: + +1. `git fetch && git reset --hard origin/main` in `/opt/muzix` +2. `npm install` + `npm run build` in `/opt/muzix/web` +3. Copies `web/public/` → `web/.next/standalone/public/` +4. Copies `web/.next/static/` → `web/.next/standalone/web/.next/static/` +5. `systemctl restart muzix-web` +6. Verifies the service is active; on failure prints the last 30 journal lines and exits non-zero + +## Why two hand-wired copies? + +Next.js standalone output deliberately omits `public/` and +`.next/static/` to keep the standalone tarball minimal — the docs tell +you to copy them in for production. See +https://nextjs.org/docs/app/api-reference/config/next-config-js/output. + +The labs (`/mixdown.html`, `/labelton.html`) live in `web/public/`, so +forgetting the public-copy step is how they 404 in prod. + +## Adding new sub-routes / static assets + +- Static one-pagers under `web/public/` reach prod automatically once + this script runs. +- New Next.js routes under `web/app/` need a fresh `npm run build`, + which the script already does. diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100755 index 00000000..1b2fc528 --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# ============================================================= +# Muzix — prod deploy +# Runs on the VPS that serves muzix.kcolbchain.com (port 3700, +# behind Caddy). Pulls main, rebuilds the Next.js standalone, +# wires in the assets standalone doesn't bundle automatically, +# and restarts the systemd service. +# +# Usage: ssh user@ '/opt/muzix/deploy/deploy.sh' +# ============================================================= + +set -euo pipefail + +REPO=/opt/muzix +SERVICE=muzix-web +SUBJECT=muzix.kcolbchain.com +STANDALONE="$REPO/web/.next/standalone" + +echo "==> Deploying ${SUBJECT} (origin/main → prod)..." + +cd "$REPO" +git fetch origin --quiet +git reset --hard origin/main +HEAD_SHA="$(git rev-parse --short HEAD)" +echo " Source at ${HEAD_SHA}: $(git log -1 --pretty='%s')" + +cd "$REPO/web" +echo "==> npm install..." +npm install --no-audit --no-fund --prefer-offline > /dev/null + +echo "==> next build..." +npm run build > /dev/null + +# Next.js standalone output does NOT copy these by design — the docs +# explicitly tell you to wire them in for production. See: +# https://nextjs.org/docs/app/api-reference/config/next-config-js/output +echo "==> wiring public/ and .next/static into standalone..." +rm -rf "$STANDALONE/public" +mkdir -p "$STANDALONE/public" +cp -r "$REPO/web/public/." "$STANDALONE/public/" + +rm -rf "$STANDALONE/web/.next/static" +mkdir -p "$STANDALONE/web/.next" +cp -r "$REPO/web/.next/static" "$STANDALONE/web/.next/static" + +echo "==> systemctl restart ${SERVICE}..." +sudo systemctl restart "$SERVICE" +sleep 2 + +if systemctl is-active --quiet "$SERVICE"; then + echo "==> Deploy complete at ${HEAD_SHA}. ${SERVICE} is active." +else + echo "!! ${SERVICE} failed to start. Recent logs:" + journalctl -u "$SERVICE" -n 30 --no-pager + exit 1 +fi From 6899e5a788330aac6d97765921c684415afedeff Mon Sep 17 00:00:00 2001 From: Abhishek Krishna Date: Tue, 26 May 2026 10:13:48 +0530 Subject: [PATCH 2/2] fix(deploy): write .next/static to standalone/.next/static, not standalone/web/.next/static MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit server.js does `process.chdir(__dirname)` where __dirname is the standalone root, then resolves static assets from `./.next/static`. The previous script mirrored the source layout (web/.next/static), which silently broke every /_next/static/* request — homepage HTML rendered but with no CSS and no client JS. Verified on prod: with the corrected path, /_next/static/css/*.css returns 200 and the homepage renders fully styled. --- deploy/README.md | 4 ++-- deploy/deploy.sh | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/deploy/README.md b/deploy/README.md index e48b7276..ef2a5618 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -11,7 +11,7 @@ deploys are triggered by SSHing into the VPS and running this script. | `/opt/muzix/` | Git checkout of `kcolbchain/muzix` (the `main` branch is prod) | | `/opt/muzix/web/.next/standalone/` | Next.js standalone build (`server.js`) — what systemd runs | | `/opt/muzix/web/.next/standalone/public/` | **Hand-wired** — Next.js does not copy `public/` into standalone output | -| `/opt/muzix/web/.next/standalone/web/.next/static/` | **Hand-wired** — Next.js does not copy `.next/static` into standalone output | +| `/opt/muzix/web/.next/standalone/.next/static/` | **Hand-wired** — Next.js does not copy `.next/static` into standalone output. Note: this lives at `standalone/.next/static`, **not** `standalone/web/.next/static` — the source dir-layout is misleading. | | `/etc/systemd/system/muzix-web.service` | systemd unit — `node /opt/muzix/web/.next/standalone/server.js` on `127.0.0.1:3700` | | `/etc/caddy/conf.d/muzix.caddy` | Caddy reverse-proxy to `127.0.0.1:3700` | @@ -26,7 +26,7 @@ The script: 1. `git fetch && git reset --hard origin/main` in `/opt/muzix` 2. `npm install` + `npm run build` in `/opt/muzix/web` 3. Copies `web/public/` → `web/.next/standalone/public/` -4. Copies `web/.next/static/` → `web/.next/standalone/web/.next/static/` +4. Copies `web/.next/static/` → `web/.next/standalone/.next/static/` 5. `systemctl restart muzix-web` 6. Verifies the service is active; on failure prints the last 30 journal lines and exits non-zero diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 1b2fc528..541fa4f7 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -34,14 +34,19 @@ npm run build > /dev/null # Next.js standalone output does NOT copy these by design — the docs # explicitly tell you to wire them in for production. See: # https://nextjs.org/docs/app/api-reference/config/next-config-js/output +# Both destinations are RELATIVE TO THE STANDALONE ROOT — NOT under web/. +# server.js does process.chdir(__dirname) where __dirname is the standalone +# root, then looks for `./.next/static` and `./public`. Writing them under +# `standalone/web/.next/static` (a mirror of the source layout) is wrong: +# the homepage will render but every /_next/static/* request will 404, +# silently breaking all styling. echo "==> wiring public/ and .next/static into standalone..." rm -rf "$STANDALONE/public" mkdir -p "$STANDALONE/public" cp -r "$REPO/web/public/." "$STANDALONE/public/" -rm -rf "$STANDALONE/web/.next/static" -mkdir -p "$STANDALONE/web/.next" -cp -r "$REPO/web/.next/static" "$STANDALONE/web/.next/static" +rm -rf "$STANDALONE/.next/static" +cp -r "$REPO/web/.next/static" "$STANDALONE/.next/static" echo "==> systemctl restart ${SERVICE}..." sudo systemctl restart "$SERVICE"