Skip to content

Commit eb8fc48

Browse files
limit cpu on docker deploy
1 parent 045a2af commit eb8fc48

4 files changed

Lines changed: 51 additions & 11 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,10 @@ jobs:
7474
script: |
7575
set -euo pipefail
7676
cd "${{ secrets.DEPLOY_APP_DIR }}"
77-
git fetch origin main
78-
git checkout main
79-
git pull --ff-only origin main
77+
echo "→ Syncing main"
78+
git fetch -q origin main
79+
git checkout -q main
80+
git pull -q --ff-only origin main
81+
echo "✓ At $(git rev-parse --short HEAD)"
8082
./deploy/write-env.sh
8183
./deploy/deploy.sh

AGENTS.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,17 @@
55
- Keep the work page intro prominent; list past tech roles in a minimal, de-emphasized way with no company logos.
66
- Avoid implicit `any`; type route `+page.ts` loaders with `PageLoad` from `./$types`.
77
- Keep thoughts as local markdown on disk; syndicate to the ATmosphere with standard.site, do not render posts from the PDS.
8+
- Use `@lucide/svelte` for UI icons; keep GitHub and Bluesky brand logos in `BrandIcon` (Lucide excludes brand icons).
89

910
## Learned Workspace Facts
1011

11-
- SvelteKit site at the repo root: `@sveltejs/adapter-node` (hybrid — most routes prerendered, `/listening` is SSR), `$lib/content/*` plus `$lib/config/*` for posts, work, games, and site config. Thoughts are local markdown (`src/content/thoughts/*.md`, `markdown-it`, `js-yaml` v3 frontmatter); syndicate to standard.site via `@ewanc26/svelte-standard-site` (`pnpm publish-publication`, `publish-thought`, `publish-thoughts`, `write-well-known`; markpub.at content). Verification `<link>` tags on individual thought pages only; `pnpm-workspace.yaml` pins `@ewanc26/*` overrides.
12+
- SvelteKit site at the repo root: `@sveltejs/adapter-node` (hybrid — most routes prerendered; Node server required for `/listening`, `/api/status`, `/api/cron/sync-listening`, and `hooks.server.ts` SQLite/cron — not removable without architecture change), `$lib/content/*` plus `$lib/config/*` for posts, work, games, and site config. Thoughts are local markdown (`src/content/thoughts/*.md`, `markdown-it`, `js-yaml` v3 frontmatter); syndicate to standard.site via `@ewanc26/svelte-standard-site` (`pnpm publish-publication`, `publish-thought`, `publish-thoughts`, `write-well-known`; markpub.at content). Verification `<link>` tags on individual thought pages only; `pnpm-workspace.yaml` pins `@ewanc26/*` overrides.
1213
- `/listening` reads recent Last.fm scrobbles from SQLite (`better-sqlite3`); sync runs via in-process `node-cron` (~5×/day at server startup) and stale refresh on page load; `GET /api/cron/sync-listening` (Bearer `CRON_SECRET`) is optional for external/manual HTTP sync only.
1314
- Homepage status line reads `com.michaelschultz.status/self` from Bluesky via `/api/status` (60s server cache); publish with `pnpm status` using `BLUESKY_APP_PASSWORD` locally (not needed at runtime).
14-
- Docker deployment: multi-stage `Dockerfile` + `docker-compose.yml` on external network `pangolin` for reverse-proxy routing; shared pnpm store cache mount with separate `prod-deps` and `build` stages (runtime copies prod `node_modules` from `prod-deps`, `build/` from `build`); `pnpm-workspace.yaml` sets `trustLockfile: true`; production image is built in GitHub Actions and pushed to `ghcr.io/michaelwschultz/michaelschultz.com`; VPS runs `deploy/deploy.sh` (`docker compose pull` + `up --no-build`); SQLite at `DATABASE_PATH` (default `/app/data/listening.db`); `deploy/docker-entrypoint.sh` chowns the `listening-data` volume.
15-
- Production deploy is automated via `.github/workflows/deploy.yml` (GH Actions build/push to GHCR, then SSH pull + `deploy/write-env.sh` + `deploy/deploy.sh`); app and infrastructure settings live in GitHub Environment secrets, not in the repo.
15+
- Docker deployment: multi-stage `Dockerfile` + `docker-compose.yml` on external network `pangolin` for reverse-proxy routing; shared pnpm store cache mount with separate `prod-deps` and `build` stages (runtime copies prod `node_modules` from `prod-deps`, `build/` from `build`); `pnpm-workspace.yaml` sets `trustLockfile: true`; production image is built in GitHub Actions (`.github/workflows/deploy.yml`) and pushed to `ghcr.io/michaelwschultz/michaelschultz.com`; VPS runs `deploy/deploy.sh` (`docker compose pull` + `up --no-build`, no on-host build); do not pass `--config.child-concurrency=2` to `pnpm install` in the Dockerfile (pnpm 11 lifecycle-hook bug); SQLite at `DATABASE_PATH` (default `/app/data/listening.db`); `deploy/docker-entrypoint.sh` chowns the `listening-data` volume; app and infrastructure settings live in GitHub Environment secrets, not in the repo.
16+
- Thought read-aloud: client-side Kokoro TTS (`kokoro-js`) on `/thoughts/[slug]/` only; engine in `$lib/page-reader/*` (ported from standard-reader); `thoughts/[slug]/+layout.svelte` lazy-loads the engine and `PageReaderBar`; attributes standard-reader.app; word highlighting via CSS `::highlight(reader-word)` in `static/reader-highlight.css` (outside Tailwind pipeline).
1617
- Static assets live under `static/static/` so URLs stay `/static/images/...`.
17-
- Search is Pagefind: `pnpm build` runs `vite build` then `scripts/postbuild.mjs` to index `build/`; UI uses the `pagefind-modal` web component; dev search needs a production build (index not available in `vite dev`).
18-
- `export const trailingSlash = 'always'` in `src/routes/+layout.ts` emits directory-style HTML (`work/index.html`) for correct Pagefind paths.
19-
- Layout sets `data-pagefind-meta` with clean route URLs (e.g. `url:/work`); `content-check` routes use `data-pagefind-ignore` so they are not indexed.
18+
- Search is Pagefind: `pnpm build` runs `vite build` then `scripts/postbuild.mjs` to index `build/`; UI uses the `pagefind-modal` web component; dev search needs a production build (index not available in `vite dev`); `export const trailingSlash = 'always'` in `src/routes/+layout.ts` emits directory-style HTML for correct paths; layout sets `data-pagefind-meta` with clean route URLs (e.g. `url:/work`); `content-check` routes use `data-pagefind-ignore` so they are not indexed.
2019
- Node is locked to 24.x (`engines` in `package.json`, `.nvmrc`, `.node-version`).
2120
- Open Graph and Twitter meta come from `SocialMeta` in the root layout and `getPageSocialMeta` in `$lib/utils/social-meta.ts`; default image is `site.socialBanner` (`/static/images/michael-schultz-social.jpg`).
2221
- `pnpm check` runs `svelte-check` then `tsc --noEmit` so untyped route loaders are caught in CI and locally.

deploy/deploy.sh

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,47 @@ cd "$ROOT_DIR"
66

77
export WEB_IMAGE="${WEB_IMAGE:-ghcr.io/michaelwschultz/michaelschultz.com:latest}"
88

9+
# Cap dockerd during pull/up (layer extract runs in the daemon, not the compose CLI).
10+
# On a 1-vCPU host, 80% keeps headroom for SSH/system. On N vCPUs use N×80% (e.g. 160%).
11+
DEPLOY_DOCKER_CPU_QUOTA="${DEPLOY_DOCKER_CPU_QUOTA:-80%}"
12+
docker_cpu_limited=0
13+
14+
reset_docker_cpu_quota() {
15+
if [[ "$docker_cpu_limited" -eq 1 ]]; then
16+
systemctl set-property docker.service --reset-property=CPUQuota 2>/dev/null \
17+
|| sudo -n systemctl set-property docker.service --reset-property=CPUQuota 2>/dev/null \
18+
|| true
19+
docker_cpu_limited=0
20+
fi
21+
}
22+
23+
limit_docker_cpu_quota() {
24+
if [[ -z "$DEPLOY_DOCKER_CPU_QUOTA" ]]; then
25+
return 0
26+
fi
27+
if systemctl set-property docker.service "CPUQuota=${DEPLOY_DOCKER_CPU_QUOTA}" 2>/dev/null \
28+
|| sudo -n systemctl set-property docker.service "CPUQuota=${DEPLOY_DOCKER_CPU_QUOTA}" 2>/dev/null; then
29+
docker_cpu_limited=1
30+
echo "→ Docker daemon CPU capped at ${DEPLOY_DOCKER_CPU_QUOTA} for deploy"
31+
fi
32+
}
33+
34+
trap reset_docker_cpu_quota EXIT
35+
936
if [[ -n "${GHCR_TOKEN:-}" && -n "${GHCR_USERNAME:-}" ]]; then
10-
echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GHCR_USERNAME" --password-stdin
37+
echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GHCR_USERNAME" --password-stdin >/dev/null
1138
fi
1239

13-
docker compose pull web
40+
limit_docker_cpu_quota
41+
42+
echo "→ Pulling ${WEB_IMAGE}"
43+
docker compose pull --quiet web
44+
image_id="$(docker image inspect "$WEB_IMAGE" --format '{{.Id}}')"
45+
echo "✓ Image ready (${image_id})"
46+
1447
docker compose up -d --no-build
48+
container_id="$(docker compose ps -q web)"
49+
echo "✓ web running (${container_id})"
50+
51+
reset_docker_cpu_quota
52+
trap - EXIT

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ services:
22
web:
33
image: ${WEB_IMAGE:-michaelschultzcom-web}
44
build: .
5+
cpus: 0.8
56
restart: unless-stopped
67
volumes:
78
- listening-data:/app/data

0 commit comments

Comments
 (0)