Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
# syntax=docker/dockerfile:1

# Build the Astro app with the standalone Node adapter.
# --platform=$BUILDPLATFORM: dist/ is architecture-independent (pure JS — the
# node build bundles every dependency, no native modules), so run the build
# once natively instead of once per target platform under QEMU emulation.
# Build the Astro app with the standalone Node adapter (used everywhere except
# on Netlify).
# --platform=$BUILDPLATFORM: dist/ is architecture-independent (pure JS), so
# run the build once natively instead of once per target platform under QEMU.
FROM --platform=$BUILDPLATFORM node:24-alpine AS build
ENV DEPLOY_TARGET=node
WORKDIR /app

# All app config (NOINDEX, API_AUTHORITY) is runtime-only (astro:env secrets):
Expand All @@ -19,13 +18,19 @@ RUN corepack enable
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
RUN pnpm install --frozen-lockfile

# Build the standalone server output into dist/. The node target bundles all
# dependencies into dist/ (vite.ssr.noExternal in astro.config.mjs), so the
# runtime image needs no node_modules.
# Build the standalone server output into dist/.
COPY . .
RUN pnpm build

# Runtime image: just Node + the self-contained server bundle.
# Production dependencies the server imports at runtime. Runs on the target
# platform so packages with native binaries get the right ones.
FROM node:24-alpine AS deps
WORKDIR /app
RUN corepack enable
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
RUN pnpm install --frozen-lockfile --prod --ignore-scripts

# Runtime image: Node, the server output and its production dependencies.
FROM node:24-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
Expand All @@ -36,6 +41,7 @@ ENV PORT=4321
# tini reaps zombies and forwards signals so `docker stop` shuts down cleanly.
RUN apk add --no-cache tini

COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./package.json

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Set `API_AUTHORITY` (and `NOINDEX=false` for production) in the Netlify site's *

## 🐳 Deploying as a Docker image

The app can also run as a self-contained Node server via the [`@astrojs/node`](https://docs.astro.build/en/guides/integrations-guide/node/) adapter. The adapter is selected at build time by `DEPLOY_TARGET`: it defaults to `netlify`, and the `Dockerfile` sets `DEPLOY_TARGET=node` to produce a standalone server.
The app can also run as a self-contained Node server via the [`@astrojs/node`](https://docs.astro.build/en/guides/integrations-guide/node/) adapter. This is the default adapter, used for `astro dev`, local builds and the Docker image. Netlify builds switch to the Netlify adapter automatically through the `NETLIFY` environment variable Netlify sets.

```bash
docker build -t device-database-frontend .
Expand All @@ -91,7 +91,7 @@ docker run -p 4321:4321 \

**Runtime config.** All app config is runtime-only: the server reads `API_AUTHORITY`, `CSR_API_AUTHORITY` and `NOINDEX` from the environment (`-e`), so a single image serves every environment — nothing is baked in at build time. `NOINDEX` defaults to `true` (no indexing) when unset; a flip takes effect on container restart (plus CDN cache TTL). Two consequences of prerendering: the client-side API authority is served by the `/config.js` endpoint rather than inlined into the HTML, and prerendered pages carry no `robots` meta tag or `X-Robots-Tag` header (they would freeze the build-time value) — `robots.txt`, which is server-rendered, remains the crawl gate for preview deploys.

The server listens on `HOST=0.0.0.0` and `PORT=4321` (both overridable via env vars). The runtime image ships only the bundled server output — the node build bundles all dependencies into `dist/` (`vite.ssr.noExternal`), so there is no `node_modules` in the final image. One consequence: Astro's sharp-backed `/_image` endpoint is unavailable in the container (the project doesn't use `astro:assets`, so nothing depends on it — revisit if that changes).
The server listens on `HOST=0.0.0.0` and `PORT=4321` (both overridable via env vars). The runtime image ships the server output in `dist/` plus a production-only `node_modules`. Astro and the adapter are bundled into `dist/`, so only packages the server imports at runtime belong in `dependencies`; everything else (including `astro` and build tooling) lives in `devDependencies` to keep the image small. Astro's sharp-backed `/_image` endpoint is unavailable in the container (the project doesn't use `astro:assets`, so nothing depends on it — revisit if that changes).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks claude 😁


Images are published only when a GitHub Release is published (semver tags; prereleases skip `:latest`): `.github/workflows/deploy.yml` pushes an environment-agnostic image (`NOINDEX` and `API_AUTHORITY` are provided at runtime where the container is deployed; set `NOINDEX=false` for production) to GitHub Container Registry (`ghcr.io/<owner>/<repo>`). Pull requests build the image without pushing, as a validation check.

Expand Down
19 changes: 8 additions & 11 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import node from "@astrojs/node";
import { paraglideVitePlugin } from "@inlang/paraglide-js";
import { defineConfig, envField, memoryCache } from "astro/config";

// Switch the deploy target with DEPLOY_TARGET. Defaults to "netlify" so the
// existing Netlify build is unaffected; the Docker image sets "node".
const isNodeTarget = process.env.DEPLOY_TARGET === "node";
const adapter = isNodeTarget ? node({ mode: "standalone" }) : netlify();
// Netlify sets NETLIFY=true in its builds. Everywhere else (dev, Docker) we
// run the standalone Node server.
const adapter = process.env.NETLIFY ? netlify() : node({ mode: "standalone" });

const prefixedLocales = [
"ar",
Expand Down Expand Up @@ -46,6 +45,9 @@ const prefixedLocales = [
export default defineConfig({
// `localizeHref()` emits links without a trailing slash; `/about/` redirects to `/about`.
trailingSlash: "never",
// Astro 7 defaults to "jsx" whitespace rules, which drop the space between
// text and a link on the next line. Keep the old behavior.
compressHTML: true,
build: {
// External stylesheets persist correctly across ClientRouter navigations.
inlineStylesheets: "never",
Expand Down Expand Up @@ -109,13 +111,8 @@ export default defineConfig({
],
}),
],
// The Docker image ships dist/ without node_modules, so the node build
// must bundle every dependency into the server output.
ssr: isNodeTarget ? { noExternal: true } : {},
},
experimental: {
cache: {
provider: memoryCache(),
},
cache: {
provider: memoryCache(),
},
});
16 changes: 9 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,30 +30,32 @@
]
},
"dependencies": {
"@astrojs/netlify": "^7.0.13",
"@eslint-community/eslint-plugin-eslint-comments": "^4.7.2",
"astro": "^6.4.4",
"fuse.js": "^7.5.0",
"lit": "^3.3.2",
"lucide": "^1.17.0",
"undici": "^8.10.0"
},
"devDependencies": {
"@astrojs/check": "^0.9.9",
"@astrojs/node": "^10.1.4",
"@astrojs/check": "^0.9.10",
"@astrojs/netlify": "^8.2.6",
"@astrojs/node": "^11.1.6",
"@eslint-community/eslint-plugin-eslint-comments": "^4.7.2",
"@eslint/js": "^10.0.1",
"@ianvs/prettier-plugin-sort-imports": "^4.7.1",
"@inlang/paraglide-js": "^2.23.0",
"@types/node": "^24.19.1",
"astro": "^7.3.5",
"eslint": "^10.8.0",
"eslint-plugin-astro": "^3.0.1",
"eslint-plugin-lit": "^2.3.1",
"eslint-plugin-unicorn": "^72.0.0",
"fuse.js": "^7.5.0",
"husky": "^9.1.7",
"lint-staged": "^17.2.0",
"lit": "^3.3.2",
"prettier": "^3.9.6",
"prettier-plugin-astro": "^0.14.1",
"typescript": "^6.0.3",
"typescript-eslint": "^8.65.0",
"vite": "^8.3.4",
"vitest": "^3"
}
}
Loading
Loading