This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
.git
|
||||
.gitea
|
||||
.next
|
||||
node_modules
|
||||
data/projects/*
|
||||
!.gitkeep
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
+9
-4
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user