build: package persistent deployment and maintenance commands
This commit is contained in:
@@ -3,6 +3,10 @@
|
||||
.next
|
||||
out
|
||||
node_modules
|
||||
data
|
||||
backups
|
||||
test-results
|
||||
playwright-report
|
||||
|
||||
.env
|
||||
.env.*
|
||||
|
||||
10
Dockerfile
10
Dockerfile
@@ -4,6 +4,9 @@ FROM node:22-alpine AS dependencies
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# npm may build the native SQLite binding when no matching prebuild is available.
|
||||
RUN apk add --no-cache python3 make g++
|
||||
|
||||
ARG NPM_REGISTRY=https://package-mirror.liara.ir/repository/npm/
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
@@ -29,7 +32,8 @@ WORKDIR /app
|
||||
ENV NODE_ENV=production \
|
||||
NEXT_TELEMETRY_DISABLED=1 \
|
||||
HOSTNAME=0.0.0.0 \
|
||||
PORT=3000
|
||||
PORT=3000 \
|
||||
DATA_DIR=/data
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs \
|
||||
&& adduser --system --uid 1001 nextjs
|
||||
@@ -37,12 +41,12 @@ RUN addgroup --system --gid 1001 nodejs \
|
||||
COPY --from=verification --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=verification --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
COPY --from=verification --chown=nextjs:nodejs /app/public ./public
|
||||
RUN mkdir -p .next/cache && chown -R nextjs:nodejs .next/cache
|
||||
RUN mkdir -p .next/cache /data && chown -R nextjs:nodejs .next/cache /data
|
||||
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
|
||||
HEALTHCHECK --interval=2s --timeout=2s --start-period=3s --retries=30 \
|
||||
CMD ["node", "-e", "fetch('http://127.0.0.1:3000/').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
|
||||
CMD ["node", "-e", "fetch('http://127.0.0.1:3000/api/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
15
compose.selfhost.yaml
Normal file
15
compose.selfhost.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
services:
|
||||
portfolio-app:
|
||||
build: .
|
||||
image: portfolio-builder:local
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:${PORT:-3000}:3000"
|
||||
environment:
|
||||
SITE_URL: ${SITE_URL:-http://localhost:3000}
|
||||
DATA_DIR: /data
|
||||
volumes:
|
||||
- ${PORTFOLIO_DATA_DIR:-./data}:/data
|
||||
- next-image-cache:/app/.next/cache
|
||||
volumes:
|
||||
next-image-cache:
|
||||
@@ -3,13 +3,16 @@ services:
|
||||
image: ${PORTFOLIO_IMAGE:-amiirkhl-portfolio:latest}
|
||||
container_name: portfolio-app
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
SITE_URL: ${SITE_URL:-https://portfolio.example.com}
|
||||
DATA_DIR: /data
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"node",
|
||||
"-e",
|
||||
"fetch('http://127.0.0.1:3000/').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))",
|
||||
"fetch('http://127.0.0.1:3000/api/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))",
|
||||
]
|
||||
interval: 2s
|
||||
timeout: 2s
|
||||
@@ -17,6 +20,7 @@ services:
|
||||
retries: 30
|
||||
volumes:
|
||||
- next-image-cache:/app/.next/cache
|
||||
- portfolio-data:/data
|
||||
networks:
|
||||
- caddy_proxy
|
||||
|
||||
@@ -27,3 +31,4 @@ networks:
|
||||
|
||||
volumes:
|
||||
next-image-cache:
|
||||
portfolio-data:
|
||||
|
||||
2
scripts/build-maintenance.mjs
Normal file
2
scripts/build-maintenance.mjs
Normal file
@@ -0,0 +1,2 @@
|
||||
import { build } from 'esbuild';
|
||||
await build({ entryPoints: ['scripts/maintenance.ts'], outfile: '.next/standalone/maintenance.mjs', bundle: true, platform: 'node', target: 'node22', format: 'esm', packages: 'external' });
|
||||
@@ -125,9 +125,14 @@ fi
|
||||
compose() {
|
||||
local directory="$1"
|
||||
shift
|
||||
local runtime_environment=()
|
||||
if [[ -f "$deploy_root/runtime.env" ]]; then
|
||||
runtime_environment=(--env-file "$deploy_root/runtime.env")
|
||||
fi
|
||||
docker compose \
|
||||
--project-name portfolio \
|
||||
--file "$directory/compose.yaml" \
|
||||
"${runtime_environment[@]}" \
|
||||
--env-file "$directory/release.env" \
|
||||
"$@"
|
||||
}
|
||||
@@ -187,12 +192,7 @@ if activate "$release_dir"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Deployment failed; attempting rollback." >&2
|
||||
if [[ -n "$previous_release" && -f "$previous_release/compose.yaml" && -f "$previous_release/release.env" ]]; then
|
||||
activate "$previous_release"
|
||||
echo "Rollback succeeded: $(basename "$previous_release")" >&2
|
||||
else
|
||||
echo "No image-based previous release is available for automatic rollback." >&2
|
||||
fi
|
||||
echo "Deployment failed. Automatic image rollback is disabled because startup may have migrated the persistent database." >&2
|
||||
echo "Inspect the failed container. Restore the pre-upgrade backup into a new data directory before activating an older image; see SELF_HOSTING.md." >&2
|
||||
|
||||
exit 1
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
import { cp } from 'node:fs/promises';
|
||||
import { cp, readdir, rm } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const projectRoot = path.resolve('.');
|
||||
const standaloneRoot = path.join(projectRoot, '.next', 'standalone');
|
||||
|
||||
// Instrumentation traces can bypass Next's route-level exclusions. Runtime
|
||||
// state belongs to the mounted volume, never to a distributable build.
|
||||
async function removeRuntimeState(directory) {
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
const target = path.resolve(directory, entry.name);
|
||||
if (!target.startsWith(`${standaloneRoot}${path.sep}`)) throw new Error('Invalid package path');
|
||||
if (entry.name === 'node_modules') continue;
|
||||
if (entry.name === 'data' || entry.name === 'backups' || entry.name === 'auth-secret' || /\.sqlite(?:-|$)/.test(entry.name) || /^\.env(?:\.|$)/.test(entry.name)) {
|
||||
await rm(target, { recursive: entry.isDirectory(), force: true });
|
||||
} else if (entry.isDirectory()) {
|
||||
await removeRuntimeState(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
await removeRuntimeState(standaloneRoot);
|
||||
|
||||
await cp(path.join(projectRoot, 'public'), path.join(standaloneRoot, 'public'), { recursive: true });
|
||||
await cp(path.join(projectRoot, 'migrations'), path.join(standaloneRoot, 'migrations'), { recursive: true });
|
||||
await cp(
|
||||
path.join(projectRoot, '.next', 'static'),
|
||||
path.join(standaloneRoot, '.next', 'static'),
|
||||
|
||||
Reference in New Issue
Block a user