This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM node:22-alpine AS deps
|
||||
WORKDIR /app
|
||||
|
||||
COPY .npmrc package.json package-lock.json ./
|
||||
RUN npm install -g npm@11.6.4 \
|
||||
&& npm ci --prefer-offline --no-audit --no-fund
|
||||
|
||||
FROM deps AS build
|
||||
WORKDIR /app
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
COPY next-env.d.ts next.config.ts tsconfig.json vitest.config.ts ./
|
||||
COPY src src
|
||||
COPY public public
|
||||
RUN npm run ci:quality
|
||||
|
||||
FROM node:22-alpine AS runtime
|
||||
ENV NODE_ENV=production \
|
||||
APP_ENV=production \
|
||||
NEXT_TELEMETRY_DISABLED=1 \
|
||||
VIDEOREG_DB_FILE=/data/videoreg.json \
|
||||
PORT=3000
|
||||
WORKDIR /app
|
||||
|
||||
RUN addgroup -S nodeapp && adduser -S nodeapp -G nodeapp && mkdir -p /data && chown nodeapp:nodeapp /data
|
||||
|
||||
COPY --from=build /app/.next/standalone ./
|
||||
COPY --from=build /app/.next/static ./.next/static
|
||||
COPY --from=build /app/public ./public
|
||||
|
||||
USER nodeapp
|
||||
EXPOSE 3000
|
||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=20s --retries=3 \
|
||||
CMD wget -qO- http://127.0.0.1:3000/health/ready >/dev/null || exit 1
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
Executable
+174
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
log() {
|
||||
printf '{"event":"%s","environment":"%s","stack":"%s","releaseId":"%s","commitSha":"%s"}\n' \
|
||||
"$1" "${ENVIRONMENT:-unknown}" "${STACK_NAME:-unknown}" "${RELEASE_ID:-unknown}" "${COMMIT_SHA:-unknown}"
|
||||
}
|
||||
|
||||
require_var() {
|
||||
local name="$1"
|
||||
if [ -z "${!name:-}" ]; then
|
||||
printf 'Required environment variable %s is missing\n' "$name" >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
require_var ENVIRONMENT
|
||||
require_var STACK_NAME
|
||||
require_var RELEASE_ID
|
||||
require_var COMMIT_SHA
|
||||
require_var WEB_IMAGE
|
||||
require_var PUBLIC_HOST
|
||||
require_var TRAEFIK_NETWORK
|
||||
|
||||
REMOTE_DIR="${REMOTE_DEPLOY_DIR:-/tmp/videoreg-deploy}"
|
||||
LOCK_FILE="${RELEASE_LOCK_FILE:-/tmp/${STACK_NAME}-${ENVIRONMENT}.lock}"
|
||||
|
||||
exec 9>"$LOCK_FILE"
|
||||
if ! flock -n 9; then
|
||||
echo "Another deployment is already running for ${STACK_NAME}/${ENVIRONMENT}" >&2
|
||||
exit 75
|
||||
fi
|
||||
|
||||
cleanup_release_images() {
|
||||
if [ "${DEPLOY_CLEANUP_FAILED_IMAGES:-true}" != "true" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Cleaning up unused release image after failed deploy." >&2
|
||||
docker image rm "$WEB_IMAGE" >/dev/null 2>&1 || true
|
||||
|
||||
if [ -n "${COMMIT_SHA:-}" ]; then
|
||||
docker image prune -af --filter "label=org.opencontainers.image.revision=${COMMIT_SHA}" >&2 || true
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup_on_exit() {
|
||||
local status=$?
|
||||
trap - EXIT
|
||||
|
||||
if [ "$status" -ne 0 ]; then
|
||||
cleanup_release_images
|
||||
fi
|
||||
|
||||
exit "$status"
|
||||
}
|
||||
|
||||
trap cleanup_on_exit EXIT
|
||||
|
||||
docker_disk_paths() {
|
||||
local docker_root
|
||||
docker_root="$(docker info --format '{{.DockerRootDir}}' 2>/dev/null || true)"
|
||||
|
||||
if [ -n "$docker_root" ] && [ -d "$docker_root" ]; then
|
||||
printf '%s\n' "$docker_root"
|
||||
fi
|
||||
|
||||
if [ -d /var/lib/containerd ]; then
|
||||
printf '%s\n' /var/lib/containerd
|
||||
fi
|
||||
}
|
||||
|
||||
prune_docker_before_pull() {
|
||||
if [ "${DEPLOY_PRUNE_BEFORE_PULL:-false}" != "true" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local prune_until="${DEPLOY_PRUNE_UNTIL:-168h}"
|
||||
echo "Pruning unused Docker images older than ${prune_until} before pulling release image." >&2
|
||||
docker image prune -af --filter "until=${prune_until}" >&2 || true
|
||||
docker builder prune -af --filter "until=${prune_until}" >&2 || true
|
||||
}
|
||||
|
||||
check_docker_disk_space() {
|
||||
local min_mb="${DEPLOY_MIN_FREE_MB:-2048}"
|
||||
if [[ ! "$min_mb" =~ ^[0-9]+$ ]]; then
|
||||
echo "DEPLOY_MIN_FREE_MB must be an integer MiB value, got: ${min_mb}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
local min_kb=$((min_mb * 1024))
|
||||
local failed=0
|
||||
local path available_kb available_mb
|
||||
|
||||
while IFS= read -r path; do
|
||||
available_kb="$(df -Pk "$path" | awk 'NR == 2 { print $4 }')"
|
||||
if [ -z "$available_kb" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "$available_kb" -lt "$min_kb" ]; then
|
||||
available_mb=$((available_kb / 1024))
|
||||
echo "Docker storage path ${path} has ${available_mb} MiB free; need at least ${min_mb} MiB before pulling release image." >&2
|
||||
failed=1
|
||||
fi
|
||||
done < <(docker_disk_paths | awk '!seen[$0]++')
|
||||
|
||||
if [ "$failed" -ne 0 ]; then
|
||||
docker system df >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
wait_service() {
|
||||
local service="$1"
|
||||
local timeout="${DEPLOY_WAIT_TIMEOUT_SECONDS:-300}"
|
||||
local started_at
|
||||
started_at="$(date +%s)"
|
||||
|
||||
while true; do
|
||||
local update_state
|
||||
update_state="$(docker service inspect --format '{{if .UpdateStatus}}{{.UpdateStatus.State}}{{end}}' "$service" 2>/dev/null || true)"
|
||||
if [ "$update_state" = "rollback_started" ] || [ "$update_state" = "rollback_completed" ] || [ "$update_state" = "paused" ]; then
|
||||
docker service ps --no-trunc "$service" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local desired running failed
|
||||
desired="$(docker service inspect --format '{{if .Spec.Mode.Replicated}}{{.Spec.Mode.Replicated.Replicas}}{{else}}1{{end}}' "$service")"
|
||||
running="$(docker service ps "$service" --filter desired-state=running --format '{{.CurrentState}}' | grep -c '^Running' || true)"
|
||||
failed="$(docker service ps "$service" --no-trunc --format '{{.CurrentState}} {{.Error}}' | grep -Ec 'Rejected|Failed|No such image|unhealthy' || true)"
|
||||
|
||||
if [ "$failed" -gt 0 ]; then
|
||||
docker service ps --no-trunc "$service" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$running" -ge "$desired" ]; then
|
||||
log "service_converged"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$(( $(date +%s) - started_at ))" -ge "$timeout" ]; then
|
||||
docker service ps --no-trunc "$service" >&2 || true
|
||||
echo "Timed out waiting for ${service} convergence" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
|
||||
log deploy_start
|
||||
|
||||
if [ -n "${REGISTRY:-}" ] && [ -n "${REGISTRY_USERNAME:-}" ] && [ -n "${REGISTRY_PASSWORD:-}" ]; then
|
||||
printf '%s' "$REGISTRY_PASSWORD" | docker login "$REGISTRY" --username "$REGISTRY_USERNAME" --password-stdin
|
||||
fi
|
||||
|
||||
prune_docker_before_pull
|
||||
check_docker_disk_space
|
||||
|
||||
docker image pull "$WEB_IMAGE"
|
||||
|
||||
export ENVIRONMENT STACK_NAME RELEASE_ID COMMIT_SHA WEB_IMAGE PUBLIC_HOST TRAEFIK_NETWORK
|
||||
export REGION="${REGION:-}" TEAM_ID="${TEAM_ID:-}" WEB_REPLICAS="${WEB_REPLICAS:-1}"
|
||||
export WEB_PLACEMENT_CONSTRAINT="${WEB_PLACEMENT_CONSTRAINT:-node.role == manager}"
|
||||
export TRAEFIK_ENTRYPOINTS="${TRAEFIK_ENTRYPOINTS:-websecure}" TRAEFIK_TLS="${TRAEFIK_TLS:-true}"
|
||||
export VIDEOREG_DB_MEMORY_TTL_MS="${VIDEOREG_DB_MEMORY_TTL_MS:-3600000}"
|
||||
export VIDEOREG_NEXT_CACHE_REVALIDATE_SECONDS="${VIDEOREG_NEXT_CACHE_REVALIDATE_SECONDS:-3600}"
|
||||
|
||||
docker stack deploy --with-registry-auth --prune -c "$REMOTE_DIR/docker-stack.yml" "$STACK_NAME"
|
||||
wait_service "${STACK_NAME}_web"
|
||||
|
||||
log deploy_converged
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
require_var() {
|
||||
local name="$1"
|
||||
if [ -z "${!name:-}" ]; then
|
||||
printf 'Required environment variable %s is missing\n' "$name" >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
require_var ENVIRONMENT
|
||||
require_var STACK_NAME
|
||||
|
||||
LOCK_FILE="${RELEASE_LOCK_FILE:-/tmp/${STACK_NAME}-${ENVIRONMENT}.lock}"
|
||||
exec 9>"$LOCK_FILE"
|
||||
flock -n 9 || {
|
||||
echo "Another deployment operation is already running for ${STACK_NAME}/${ENVIRONMENT}" >&2
|
||||
exit 75
|
||||
}
|
||||
|
||||
printf '{"event":"rollback_start","environment":"%s","stack":"%s"}\n' "$ENVIRONMENT" "$STACK_NAME"
|
||||
docker service rollback "${STACK_NAME}_web" || true
|
||||
printf '{"event":"rollback_requested","environment":"%s","stack":"%s"}\n' "$ENVIRONMENT" "$STACK_NAME"
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
require_var() {
|
||||
local name="$1"
|
||||
if [ -z "${!name:-}" ]; then
|
||||
printf 'Required environment variable %s is missing\n' "$name" >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
quote() {
|
||||
printf '%q' "$1"
|
||||
}
|
||||
|
||||
require_var SWARM_MANAGER_HOST
|
||||
require_var SWARM_SSH_USER
|
||||
require_var ENVIRONMENT
|
||||
require_var STACK_NAME
|
||||
require_var RELEASE_ID
|
||||
require_var COMMIT_SHA
|
||||
require_var WEB_IMAGE
|
||||
require_var PUBLIC_HOST
|
||||
require_var TRAEFIK_NETWORK
|
||||
|
||||
SSH_PORT="${SWARM_SSH_PORT:-22}"
|
||||
SSH_TARGET="${SWARM_SSH_USER}@${SWARM_MANAGER_HOST}"
|
||||
REMOTE_DEPLOY_DIR="${REMOTE_DEPLOY_DIR:-/tmp/videoreg-deploy}"
|
||||
|
||||
ssh -p "$SSH_PORT" "$SSH_TARGET" "mkdir -p $(quote "$REMOTE_DEPLOY_DIR")"
|
||||
scp -P "$SSH_PORT" deploy/swarm/docker-stack.yml "$SSH_TARGET:$REMOTE_DEPLOY_DIR/docker-stack.yml"
|
||||
scp -P "$SSH_PORT" deploy/remote/deploy-stack.sh "$SSH_TARGET:$REMOTE_DEPLOY_DIR/deploy-stack.sh"
|
||||
|
||||
remote_vars=(
|
||||
ENVIRONMENT STACK_NAME RELEASE_ID COMMIT_SHA WEB_IMAGE PUBLIC_HOST TRAEFIK_NETWORK
|
||||
REGION TEAM_ID WEB_REPLICAS WEB_PLACEMENT_CONSTRAINT TRAEFIK_ENTRYPOINTS TRAEFIK_TLS REGISTRY REGISTRY_USERNAME REGISTRY_PASSWORD
|
||||
RELEASE_LOCK_FILE REMOTE_DEPLOY_DIR DEPLOY_MIN_FREE_MB DEPLOY_PRUNE_BEFORE_PULL DEPLOY_PRUNE_UNTIL
|
||||
DEPLOY_CLEANUP_FAILED_IMAGES VIDEOREG_DB_MEMORY_TTL_MS VIDEOREG_NEXT_CACHE_REVALIDATE_SECONDS
|
||||
)
|
||||
|
||||
remote_command=""
|
||||
for name in "${remote_vars[@]}"; do
|
||||
if [ -n "${!name:-}" ]; then
|
||||
remote_command+="$name=$(quote "${!name}") "
|
||||
fi
|
||||
done
|
||||
remote_command+="bash $(quote "$REMOTE_DEPLOY_DIR/deploy-stack.sh")"
|
||||
|
||||
ssh -p "$SSH_PORT" "$SSH_TARGET" "$remote_command"
|
||||
@@ -0,0 +1,32 @@
|
||||
const required = [
|
||||
'ENVIRONMENT',
|
||||
'STACK_NAME',
|
||||
'RELEASE_ID',
|
||||
'COMMIT_SHA',
|
||||
'WEB_IMAGE',
|
||||
'PUBLIC_HOST',
|
||||
'TRAEFIK_NETWORK',
|
||||
'SWARM_MANAGER_HOST',
|
||||
'SWARM_SSH_USER',
|
||||
];
|
||||
|
||||
const missing = required.filter((name) => !process.env[name]);
|
||||
if (missing.length > 0) {
|
||||
console.error(`Missing required deploy variables: ${missing.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!/@sha256:[a-f0-9]{64}$/i.test(process.env.WEB_IMAGE ?? '')) {
|
||||
console.error(`WEB_IMAGE must be pinned by immutable digest, got: ${process.env.WEB_IMAGE}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
event: 'predeploy_check_passed',
|
||||
environment: process.env.ENVIRONMENT,
|
||||
stack: process.env.STACK_NAME,
|
||||
releaseId: process.env.RELEASE_ID,
|
||||
commitSha: process.env.COMMIT_SHA,
|
||||
}),
|
||||
);
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
require_var() {
|
||||
local name="$1"
|
||||
if [ -z "${!name:-}" ]; then
|
||||
printf 'Required environment variable %s is missing\n' "$name" >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
quote() {
|
||||
printf '%q' "$1"
|
||||
}
|
||||
|
||||
require_var SWARM_MANAGER_HOST
|
||||
require_var SWARM_SSH_USER
|
||||
require_var ENVIRONMENT
|
||||
require_var STACK_NAME
|
||||
|
||||
SSH_PORT="${SWARM_SSH_PORT:-22}"
|
||||
SSH_TARGET="${SWARM_SSH_USER}@${SWARM_MANAGER_HOST}"
|
||||
REMOTE_DEPLOY_DIR="${REMOTE_DEPLOY_DIR:-/tmp/videoreg-deploy}"
|
||||
|
||||
ssh -p "$SSH_PORT" "$SSH_TARGET" "mkdir -p $(quote "$REMOTE_DEPLOY_DIR")"
|
||||
scp -P "$SSH_PORT" deploy/remote/rollback-stack.sh "$SSH_TARGET:$REMOTE_DEPLOY_DIR/rollback-stack.sh"
|
||||
|
||||
remote_command="ENVIRONMENT=$(quote "$ENVIRONMENT") STACK_NAME=$(quote "$STACK_NAME") "
|
||||
if [ -n "${RELEASE_LOCK_FILE:-}" ]; then
|
||||
remote_command+="RELEASE_LOCK_FILE=$(quote "$RELEASE_LOCK_FILE") "
|
||||
fi
|
||||
remote_command+="bash $(quote "$REMOTE_DEPLOY_DIR/rollback-stack.sh")"
|
||||
|
||||
ssh -p "$SSH_PORT" "$SSH_TARGET" "$remote_command"
|
||||
@@ -0,0 +1,138 @@
|
||||
import { writeFileSync } from 'node:fs';
|
||||
|
||||
const baseUrl = process.env.BASE_URL?.replace(/\/$/, '');
|
||||
const expectedReleaseId = process.env.EXPECTED_RELEASE_ID;
|
||||
const smokeWrite = process.env.SMOKE_WRITE === 'true';
|
||||
const outputPath = process.env.SMOKE_RESULT_PATH;
|
||||
const timeoutMs = Number(process.env.SMOKE_TIMEOUT_MS ?? 5000);
|
||||
const retryAttempts = Number(process.env.SMOKE_RETRY_ATTEMPTS ?? 12);
|
||||
const retryDelayMs = Number(process.env.SMOKE_RETRY_DELAY_MS ?? 5000);
|
||||
const retryableStatuses = new Set([404, 408, 425, 429, 500, 502, 503, 504]);
|
||||
|
||||
if (!baseUrl) {
|
||||
console.error('BASE_URL is required for smoke checks');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
async function requestOnce(path, options = {}) {
|
||||
const response = await fetch(`${baseUrl}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
const text = await response.text();
|
||||
let body;
|
||||
try {
|
||||
body = text ? JSON.parse(text) : undefined;
|
||||
} catch {
|
||||
body = text;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const error = new Error(`${path} returned ${response.status}: ${text}`);
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
async function request(path, options = {}) {
|
||||
let lastError;
|
||||
|
||||
for (let attempt = 1; attempt <= retryAttempts; attempt += 1) {
|
||||
try {
|
||||
return await requestOnce(path, options);
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
const status = error?.status;
|
||||
const retryableStatus = typeof status === 'number' && retryableStatuses.has(status);
|
||||
const retryableNetworkError = error?.name === 'TimeoutError' || error?.cause?.code;
|
||||
|
||||
if (attempt >= retryAttempts || (!retryableStatus && !retryableNetworkError)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await sleep(retryDelayMs);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
function assertRelease(body) {
|
||||
if (!expectedReleaseId) return;
|
||||
const actual = body?.metadata?.releaseId;
|
||||
if (actual !== expectedReleaseId) {
|
||||
throw new Error(`service reports release ${actual}, expected ${expectedReleaseId}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const startedAt = Date.now();
|
||||
const checks = [];
|
||||
|
||||
const live = await request('/health/live');
|
||||
assertRelease(live);
|
||||
checks.push({ name: 'web_live', status: 'ok' });
|
||||
|
||||
const ready = await request('/health/ready');
|
||||
assertRelease(ready);
|
||||
checks.push({ name: 'web_ready', status: 'ok' });
|
||||
|
||||
const registry = await request('/api/videos');
|
||||
if (!registry.stats || !Array.isArray(registry.videos)) {
|
||||
throw new Error('Video registry API returned an invalid shape');
|
||||
}
|
||||
checks.push({ name: 'registry_read', status: 'ok' });
|
||||
|
||||
if (smokeWrite) {
|
||||
await request('/api/videos', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
title: `Smoke video ${Date.now()}`,
|
||||
owner: 'Deploy smoke',
|
||||
sourceUrl: 'https://example.com/smoke',
|
||||
status: 'draft',
|
||||
}),
|
||||
});
|
||||
checks.push({ name: 'registry_write', status: 'ok' });
|
||||
}
|
||||
|
||||
const result = {
|
||||
status: 'ok',
|
||||
baseUrl,
|
||||
expectedReleaseId,
|
||||
checks,
|
||||
durationMs: Date.now() - startedAt,
|
||||
};
|
||||
|
||||
if (outputPath) {
|
||||
writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`);
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({ event: 'smoke_check_success', ...result }));
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
const result = {
|
||||
status: 'error',
|
||||
baseUrl,
|
||||
expectedReleaseId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
if (outputPath) {
|
||||
writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`);
|
||||
}
|
||||
console.error(JSON.stringify({ event: 'smoke_check_failure', ...result }));
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
function requireEnv(name) {
|
||||
const value = process.env[name];
|
||||
if (!value) {
|
||||
throw new Error(`${name} is required`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function readJson(path) {
|
||||
return JSON.parse(readFileSync(path, 'utf8'));
|
||||
}
|
||||
|
||||
const smoke = readJson(requireEnv('SMOKE_RESULT_PATH'));
|
||||
|
||||
const manifest = {
|
||||
commitSha: requireEnv('COMMIT_SHA'),
|
||||
releaseId: requireEnv('RELEASE_ID'),
|
||||
webImageDigest: requireEnv('WEB_IMAGE_DIGEST'),
|
||||
deployTimestamp: new Date().toISOString(),
|
||||
smokeResult: smoke,
|
||||
workflowRunId: requireEnv('WORKFLOW_RUN_ID'),
|
||||
};
|
||||
|
||||
if (!/@sha256:[a-f0-9]{64}$/i.test(manifest.webImageDigest)) {
|
||||
throw new Error('webImageDigest must be digest-pinned');
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(manifest, null, 2));
|
||||
@@ -0,0 +1,58 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: ${WEB_IMAGE}
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
APP_ENV: ${ENVIRONMENT}
|
||||
PORT: "3000"
|
||||
VIDEOREG_DB_FILE: /data/videoreg.json
|
||||
VIDEOREG_DB_MEMORY_TTL_MS: ${VIDEOREG_DB_MEMORY_TTL_MS:-3600000}
|
||||
VIDEOREG_NEXT_CACHE_REVALIDATE_SECONDS: ${VIDEOREG_NEXT_CACHE_REVALIDATE_SECONDS:-3600}
|
||||
COMMIT_SHA: ${COMMIT_SHA}
|
||||
RELEASE_ID: ${RELEASE_ID}
|
||||
REGION: ${REGION:-}
|
||||
TEAM_ID: ${TEAM_ID:-}
|
||||
volumes:
|
||||
- videoreg_data:/data
|
||||
networks:
|
||||
traefik: {}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/health/ready >/dev/null || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
start_period: 20s
|
||||
deploy:
|
||||
replicas: ${WEB_REPLICAS:-1}
|
||||
placement:
|
||||
constraints:
|
||||
- ${WEB_PLACEMENT_CONSTRAINT:-node.role == manager}
|
||||
update_config:
|
||||
order: start-first
|
||||
parallelism: 1
|
||||
failure_action: rollback
|
||||
monitor: 45s
|
||||
rollback_config:
|
||||
order: start-first
|
||||
parallelism: 1
|
||||
monitor: 45s
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=${TRAEFIK_NETWORK}"
|
||||
- "traefik.http.routers.${STACK_NAME}-web.rule=Host(`${PUBLIC_HOST}`) && PathPrefix(`/`)"
|
||||
- "traefik.http.routers.${STACK_NAME}-web.entrypoints=${TRAEFIK_ENTRYPOINTS:-websecure}"
|
||||
- "traefik.http.routers.${STACK_NAME}-web.tls=${TRAEFIK_TLS:-true}"
|
||||
- "traefik.http.services.${STACK_NAME}-web.loadbalancer.server.port=3000"
|
||||
stop_grace_period: 30s
|
||||
|
||||
volumes:
|
||||
videoreg_data:
|
||||
|
||||
networks:
|
||||
traefik:
|
||||
external: true
|
||||
name: ${TRAEFIK_NETWORK}
|
||||
Reference in New Issue
Block a user