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.
416 lines
14 KiB
Markdown
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) |
|