Files
Tim Krampitz 01046b01e4 Neue Skills, Referenzen & OpenWiki-Doku integriert
Umfangreiche Erweiterung der Skill-Bibliothek: Neue Skills für Humanisierung (Englisch/PT-BR), Design-Validierung, AI-SEO und Coolify-Deployment inkl. Regelwerke, Presets, Pattern-Referenzen, Testfälle und Automatisierungsskripte. Zusätzliche Skills für Revenue-Centric Design, Pier Cloud, OKF, Lebenslauf- und LinkedIn-Optimierung sowie zahlreiche Referenzdateien, Checklisten und YAML/JSON/Markdown-Templates. Einführung einer vollständigen OpenWiki-Dokumentation mit Architektur-, Domain- und Workflow-Beschreibungen, zentralem Index und automatisierten Updates. Modularer Aufbau, restriktive Lizenzen und umfassende Qualitäts- und Evaluationsmechanismen für alle neuen Inhalte.
2026-07-26 14:00:58 +02:00

416 lines
14 KiB
Markdown

# Deploying Astro on Coolify
Production-tested patterns for deploying Astro sites on self-hosted Coolify (v4.x). Based on 17+ live deployments.
---
## Build Pack Decision
| Scenario | build_pack | Notes |
|----------|-----------|-------|
| Astro v6+ (requires Node ≥22.12.0) | `dockerfile` | Nixpacks can't pin minor version |
| Astro v5 or earlier | `nixpacks` | `NIXPACKS_NODE_VERSION=22` works |
| Astro `output: 'static'` with package.json | `nixpacks` | start: `npx serve dist -l 80 -s` |
| Astro `output: 'static'` (Dockerfile) | `dockerfile` | nginx serves directly |
| HTML/CSS only (no package.json) | `static` | `static_image: nginx:alpine` |
**Rule:** For Astro v6+ and v7, always use `dockerfile`. Nixpacks resolves Node 22.11.0 from its internal nixpkgs archive, but Astro v6+ requires ≥22.12.0.
**Astro v7 Docker base image rule:** Use `node:22-slim` (Debian/glibc) for the build stage, NOT `node:22-alpine`. Sätteri's native binding only supports glibc. The runtime stage can still use Alpine/Caddy since it only serves files.
---
## Dockerfile — Astro SSR (Node Adapter)
> **Requires `@astrojs/node@^11.0.0`** for Astro v7. The v10 adapter crashes at runtime with `TypeError: app.getAdapterLogger is not a function`.
```dockerfile
# Use node:22-slim (NOT alpine) — Sätteri needs glibc for Astro v7
FROM node:22-slim AS build
WORKDIR /app
ENV NODE_OPTIONS="--max-old-space-size=512"
# Coolify injects env vars as ARG — must convert to ENV for npm run build
ARG MY_API_KEY
ARG PUBLIC_SITE_URL
ENV MY_API_KEY=$MY_API_KEY
ENV PUBLIC_SITE_URL=$PUBLIC_SITE_URL
COPY package*.json .npmrc ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
CMD ["node", "dist/server/entry.mjs"]
```
## Dockerfile — Astro Static (nginx)
```dockerfile
# Use node:22-slim (NOT alpine) — Sätteri needs glibc
FROM node:22-slim AS build
WORKDIR /app
ENV NODE_OPTIONS="--max-old-space-size=512"
COPY package*.json .npmrc ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
```
---
## Critical Gotchas
### ARG vs ENV — Build-time secrets
Coolify injects variables as Docker `ARG`. But `ARG` does NOT become an environment variable for child processes like `npm run build`. Astro/Vite resolves `import.meta.env.VAR` during build — if the variable doesn't exist in the process environment, it silently becomes `undefined`.
**Fix:** For every secret the build needs:
```dockerfile
ARG RESEND_API_KEY
ENV RESEND_API_KEY=$RESEND_API_KEY
```
### OOM on Resource-Limited Servers
The `astro build` process can die with exit code 255 and no clear error message on servers with limited RAM (~2GB).
**Fix:** Add to build stage:
```dockerfile
ENV NODE_OPTIONS="--max-old-space-size=512"
```
### Nixpacks Node Version
Nixpacks only accepts **major version**. `NIXPACKS_NODE_VERSION=22` can resolve to 22.11.0, causing:
```
Node.js v22.11.0 is not supported by Astro!
```
**Fix options:**
1. Use Dockerfile instead (recommended for Astro v6+)
2. Set `NIXPACKS_NODE_VERSION=24` (skips a major)
3. Pin nixpkgs archive via `nixpacks.toml`:
```toml
[phases.setup]
nixpkgsArchive = "5ef6c8a1bf89a0bfe4e15e7baf5bab7feeff86a5"
```
### Nixpacks Timeout
Nixpacks downloads ~600MB nixpkgs archive during build. On servers with limited bandwidth, build dies silently during `unpacking` step.
**Fix:** Switch to Dockerfile. `node:22-alpine` is ~50MB vs ~600MB.
### Sätteri Native Binding on Alpine (Astro v7)
Astro v7 uses Sätteri (Rust-based Markdown) by default. Sätteri ships native bindings but **only for glibc** (`@bruits/satteri-linux-x64-gnu`). Alpine uses musl libc — no musl binding exists, and the WASM fallback has a cpu platform check that also fails.
```
Cannot find module '@bruits/satteri-linux-x64-musl'
```
**Fix:** Use `node:22-slim` (Debian/glibc) for the build stage. The runtime stage can still use Alpine since it only serves static files:
```dockerfile
FROM node:22-slim AS build # glibc — satteri works
WORKDIR /app
COPY package*.json .npmrc ./
RUN npm ci
COPY . .
RUN npm run build
FROM caddy:2-alpine # runtime doesn't need Node
COPY --from=build /app/dist /srv
```
**Affected projects:** Any Astro v7 project using Sätteri (default) or Starlight 0.40+ on Alpine.
**Not affected:** Projects using `unified()` processor explicitly (they bypass Sätteri).
### Sätteri Native Binding on ARM64 (Cross-Platform Lockfile)
When the dev machine is x86_64 but the Coolify build server is ARM64 (e.g., OCI Ampere), `npm ci` and even `npm install --include=optional` fail with:
```
Cannot find module '@bruits/satteri-linux-arm64-gnu'
Require stack:
- /app/node_modules/satteri/index.js
```
**Root cause:** The `package-lock.json` was generated on x86_64 and only includes `@bruits/satteri-linux-x64-gnu` in its optional dependency tree. npm respects the lockfile's platform resolution even on a different architecture — this is [npm bug #4828](https://github.com/npm/cli/issues/4828).
**What does NOT work:**
- `.npmrc` with `include=optional` — npm still reads the lockfile's platform tree
- `npm install --include=optional` in Dockerfile — lockfile still constrains resolution
- Adding `@bruits/satteri-linux-arm64-gnu` to `optionalDependencies` — npm may still skip it
**Fix:** Do NOT copy `package-lock.json` into the Docker build. Let npm resolve fresh on arm64:
```dockerfile
FROM node:22-slim AS build
WORKDIR /app
ENV NODE_OPTIONS="--max-old-space-size=512"
COPY package.json .npmrc ./
# Deliberately omit package-lock.json — forces fresh resolution on arm64
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
```
**Tradeoff:** Build is slightly less deterministic (no lockfile pinning in Docker). For static sites this is acceptable. For SSR with strict reproducibility needs, generate the lockfile inside an arm64 container instead.
**Affected:** Any Astro v7 project building on ARM64 servers when lockfile was generated on x86_64.
**Confirmed working:** valeria.med.br on OCI Ampere A1 via Coolify (2026-07-03).
### legacy-peer-deps and npm ci in Docker
When using `--legacy-peer-deps` locally (required for Astro v7 due to transient peer dep conflicts in Starlight plugins), Docker's `npm ci` will fail unless the `.npmrc` is copied into the container.
**Fix:** Always copy `.npmrc` before `npm ci`:
```dockerfile
COPY package*.json .npmrc ./
RUN npm ci
```
The `.npmrc` must contain:
```
legacy-peer-deps=true
```
### @astrojs/node Must Be v11+ for Astro v7
Astro v7's runtime API changed — `app.getAdapterLogger()` was added and the standalone entry module depends on it. If `@astrojs/node` stays at v10, the container builds fine but **crashes at startup**:
```
TypeError: app.getAdapterLogger is not a function
at createAppHandler (dist/server/entry.mjs)
```
Coolify shows `restarting:unknown` or `exited:unhealthy` — the build log looks green, but the container crash-loops.
**Fix:** Always upgrade `@astrojs/node` to v11 together with Astro v7:
```bash
npm install astro@latest @astrojs/node@latest
```
**Checklist for SSR v7 migration:**
- `astro` → `^7.0.0`
- `@astrojs/node` → `^11.0.0`
- `@astrojs/mdx` → `^7.0.0` (if used)
### pnpm approve-builds in Docker (pnpm 11.9+)
pnpm 11.9+ blocks install scripts (postinstall, install) by default. Packages like `esbuild` and `sharp` need native binaries built after install. Without approval, `pnpm install --frozen-lockfile` fails:
```
[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: esbuild@0.28.1, sharp@0.34.5
Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.
```
**Fix:** Run `pnpm approve-builds` locally, which creates `pnpm-workspace.yaml` with:
```yaml
allowBuilds:
esbuild: true
sharp: true
```
Then **copy `pnpm-workspace.yaml` into the Docker container** alongside the lockfile:
```dockerfile
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
RUN pnpm install --frozen-lockfile
```
Missing this file = build fails in CI/Docker but works locally (because local node_modules already has the binaries).
### package-lock.json Desync After Major Upgrade
After `npm install --legacy-peer-deps` for a major version upgrade, the lockfile may reference packages that `npm ci` (strict mode) cannot resolve. Symptoms: `npm ci` fails with "lock file's X does not satisfy Y".
**Fix:** Delete lockfile and regenerate:
```bash
rm package-lock.json node_modules -rf
npm install --legacy-peer-deps
# Then test: npm ci must pass
```
### Integrations That Download ML Models (transformers.js, ONNX)
Integrations like `@philnash/astro-related-content` download ONNX models (~300MB) during `astro build` to generate embeddings. On Coolify servers with limited bandwidth/disk, this causes 20+ minute builds or disk exhaustion.
**Pattern:** Generate artifacts locally, commit them, skip the heavy integration in CI.
**Fix:** Dual-mode config with `ENV CI=true` in Dockerfile. See [Related Content reference](references/related-content.md) for complete implementation.
**Key principle:**
- **Local:** Integration runs fully (downloads model, generates embeddings)
- **CI/Docker:** Vite plugin serves pre-built `data.json` (zero model download)
- **Cache commitado:** `.astro-related-content/data.json` + `vectors.json` go in git (~750KB for 32 posts)
This pattern applies to ANY integration that downloads large artifacts at build time.
---
## Coolify API — Create App
```bash
COOLIFY_URL="https://cool.example.com/api/v1"
COOLIFY_KEY="your-token"
curl -sS -X POST "$COOLIFY_URL/applications/private-deploy-key" \
-H "Authorization: Bearer $COOLIFY_KEY" \
-H "Content-Type: application/json" \
-d '{
"project_uuid": "PROJECT_UUID",
"environment_name": "production",
"server_uuid": "SERVER_UUID",
"private_key_uuid": "SSH_KEY_UUID",
"git_repository": "git@gitlab.com:user/project.git",
"git_branch": "main",
"build_pack": "dockerfile",
"dockerfile_location": "/Dockerfile",
"ports_exposes": "4321",
"name": "my-astro-site"
}'
```
### Set Domain
```bash
# Use "domains", NOT "fqdn" — fqdn returns "field not allowed"
curl -sS -X PATCH "$COOLIFY_URL/applications/$APP_UUID" \
-H "Authorization: Bearer $COOLIFY_KEY" \
-H "Content-Type: application/json" \
-d '{"domains": "https://mysite.com"}'
```
### Set Environment Variables
```bash
curl -sS -X POST "$COOLIFY_URL/applications/$APP_UUID/envs" \
-H "Authorization: Bearer $COOLIFY_KEY" \
-H "Content-Type: application/json" \
-d '{"key": "MY_VAR", "value": "secret-value", "is_preview": false}'
```
> Do NOT send `is_build_time` — API rejects it.
### GitLab Webhook (auto-deploy on push)
```bash
curl -sS -X POST "https://gitlab.com/api/v4/projects/$PROJECT_ID/hooks" \
-H "PRIVATE-TOKEN: $GITLAB_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://cool.example.com/webhooks/source/gitlab/events/manual",
"token": "WEBHOOK_SECRET_FROM_APP",
"push_events": true,
"enable_ssl_verification": true
}'
```
> **Critical:** The secret goes in the `"token"` field (sent as `X-Gitlab-Token` header), NEVER as `?secret=` query parameter in the URL. With secret in URL, Coolify returns 200 but does NOT trigger deploy.
### Deploy and Validate
```bash
# Trigger deploy (Coolify 4.1+ — uses GET, NOT POST)
# The /deploy endpoint accepts uuid as query param, force=true rebuilds from scratch
curl -sS -X GET "$COOLIFY_URL/deploy?uuid=$APP_UUID&force=true" \
-H "Authorization: Bearer $COOLIFY_KEY"
# Returns: {"deployments":[{"message":"Application X deployment queued.","resource_uuid":"...","deployment_uuid":"..."}]}
# ⚠️ POST /applications/$UUID/deploy returns "Not found" in Coolify 4.1
# ⚠️ POST /applications/$UUID/restart only restarts existing container (no rebuild)
# Use restart when image is already built. Use /deploy?uuid=...&force=true for rebuild.
# Check status (~60s wait for build + container start)
curl -sS -H "Authorization: Bearer $COOLIFY_KEY" \
"$COOLIFY_URL/applications/$APP_UUID" | python3 -c "
import sys,json; d=json.load(sys.stdin); print(d['status'])"
# Verify HTTP response
curl -sS -o /dev/null -w "HTTP %{http_code}\n" https://mysite.com
```
**Status interpretation:**
| Status | Meaning |
|--------|---------|
| `running:healthy` | Container up and health check passing |
| `running:unknown` | Container up, no health check configured |
| `restarting:unknown` | Container crash-looping — check runtime logs |
| `exited:unhealthy` | Container stopped — likely build or startup failure |
---
## Astro Config for Coolify SSR
```typescript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server', // or hybrid with per-page prerender
adapter: node({ mode: 'standalone' }),
server: { host: '0.0.0.0', port: 4321 },
});
```
For static output, no adapter needed — the Dockerfile handles nginx serving.
---
## Port Configuration
| Output Mode | Port | CMD |
|-------------|------|-----|
| SSR (Node adapter) | 4321 | `node dist/server/entry.mjs` |
| Static (nginx) | 80 | nginx default |
| Static (serve) | 80 | `npx serve dist -l 80 -s` |
Set `ports_exposes` in Coolify to match.
---
## Recommended Stack (Homelab-Tested)
Based on 17 production Astro sites:
```javascript
import seoGraph from '@jdevalk/astro-seo-graph/integration';
import agentmarkup from '@agentmarkup/astro';
import UnoCSS from '@unocss/astro';
import critters from 'astro-critters';
import compress from '@playform/compress';
// Key: compress() MUST be last integration
integrations: [mdx(), UnoCSS(), sitemap(), seoGraph(), agentmarkup(), critters(), compress()]
```
| Tool | Why |
|------|-----|
| UnoCSS > Tailwind | 5x faster build, smaller bundle |
| @playform/compress > astro-compress | Better maintained |
| astro-critters | Critical CSS inlining |
| @jdevalk/astro-seo-graph | All-in-one SEO (replaces astro-seo + robots-txt + indexnow) |
| @agentmarkup/astro | LLM visibility (llms.txt, markdown mirrors) |
| Plausible > GA4 | 1kb script, no cookie banner, self-hosted |
| @philnash/astro-related-content | Semantic related posts via local embeddings (CI: use prebuilt data.json) |