diff --git a/.dockerignore b/.dockerignore index 23b0b28..8c68c5c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,5 @@ .git .gitea -.next node_modules data/projects/* !.gitkeep diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index e1f1a43..0f644f0 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -31,6 +31,7 @@ jobs: STUDIO_USERNAME: studio STUDIO_PASSWORD: ${{ secrets.STUDIO_PASSWORD }} DEPLOY_PORT: ${{ vars.DEPLOY_PORT }} + DEPLOY_DOCKERFILE: Dockerfile.deploy GITHUB_SHA: ${{ gitea.sha }} run: | if [ -z "$STUDIO_PASSWORD" ]; then diff --git a/Dockerfile.deploy b/Dockerfile.deploy new file mode 100644 index 0000000..90b1f4d --- /dev/null +++ b/Dockerfile.deploy @@ -0,0 +1,17 @@ +FROM node:24-bookworm-slim +WORKDIR /app +ENV NODE_ENV=production \ + NEXT_TELEMETRY_DISABLED=1 \ + PORT=3000 \ + HOSTNAME=0.0.0.0 +RUN groupadd --system --gid 1001 studio && useradd --system --uid 1001 --gid studio studio +COPY --chown=studio:studio .next/standalone ./ +COPY --chown=studio:studio .next/static ./.next/static +COPY --chown=studio:studio templates ./templates +COPY --chown=studio:studio vendor ./vendor +RUN mkdir -p /app/data/projects && chown -R studio:studio /app/data +USER studio +EXPOSE 3000 +VOLUME ["/app/data/projects"] +HEALTHCHECK --interval=5s --timeout=3s --start-period=15s --retries=8 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"] diff --git a/scripts/deploy.mjs b/scripts/deploy.mjs index 04030bf..d9443c1 100644 --- a/scripts/deploy.mjs +++ b/scripts/deploy.mjs @@ -12,6 +12,8 @@ const image = `${service}:${sha}`; const port = process.env.DEPLOY_PORT || "3000"; const password = process.env.STUDIO_PASSWORD; const username = process.env.STUDIO_USERNAME || "studio"; +const dockerfile = process.env.DEPLOY_DOCKERFILE || "Dockerfile"; +const usePrebuiltOutput = dockerfile === "Dockerfile.deploy"; if (!password) throw new Error("STUDIO_PASSWORD secret is required"); if (!/^\d{2,5}$/.test(port) || Number(port) > 65535) throw new Error("DEPLOY_PORT must be a valid TCP port"); @@ -40,19 +42,22 @@ async function waitForHealth(container, timeoutMs = 90_000) { } async function followBuild(stream) { - await new Promise((resolve, reject) => { - docker.modem.followProgress(stream, (error, output) => error ? reject(error) : resolve(output), (event) => { + const output = await new Promise((resolve, reject) => { + docker.modem.followProgress(stream, (error, events) => error ? reject(error) : resolve(events), (event) => { if (event.stream) process.stdout.write(event.stream); if (event.error) process.stderr.write(`${event.error}\n`); }); }); + const failure = output.find((event) => event.error || event.errorDetail?.message); + if (failure) throw new Error(failure.error || failure.errorDetail.message); } console.log(`Building ${image}`); const context = tar.pack(process.cwd(), { - ignore: (name) => [".git", ".next", "node_modules"].some((part) => name === part || name.includes(`/${part}/`)), + ignore: (name) => [".git", "node_modules", ...usePrebuiltOutput ? [] : [".next"]] + .some((part) => name === part || name.includes(`/${part}/`)), }); -await followBuild(await docker.buildImage(context, { t: image, dockerfile: "Dockerfile" })); +await followBuild(await docker.buildImage(context, { t: image, dockerfile })); try { await docker.createVolume({ Name: volume }); } catch (error) { if (error.statusCode !== 409) throw error; } await removeContainer(candidate);