diff --git a/Dockerfile.prod b/Dockerfile.prod index 62f7e5b..648d438 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -27,8 +27,8 @@ RUN cd web && bun run build FROM oven/bun:latest AS production WORKDIR /app -# Create non-root user for security -RUN groupadd --system appgroup && useradd --system --gid appgroup appuser +# Create non-root user for security (bun user already exists with 1000:1000) +# No need to create user/group # Install runtime dependencies for update/deploy commands RUN apt-get update && apt-get install -y \ @@ -43,18 +43,18 @@ RUN apt-get update && apt-get install -y \ && git config --system --add safe.directory /app/deploy # Copy only what's needed for production -COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules -COPY --from=builder --chown=appuser:appgroup /app/web/node_modules ./web/node_modules -COPY --from=builder --chown=appuser:appgroup /app/web/dist ./web/dist -COPY --from=builder --chown=appuser:appgroup /app/web/src ./web/src -COPY --from=builder --chown=appuser:appgroup /app/bot ./bot -COPY --from=builder --chown=appuser:appgroup /app/shared ./shared -COPY --from=builder --chown=appuser:appgroup /app/package.json . -COPY --from=builder --chown=appuser:appgroup /app/drizzle.config.ts . -COPY --from=builder --chown=appuser:appgroup /app/tsconfig.json . +COPY --from=builder --chown=bun:bun /app/node_modules ./node_modules +COPY --from=builder --chown=bun:bun /app/web/node_modules ./web/node_modules +COPY --from=builder --chown=bun:bun /app/web/dist ./web/dist +COPY --from=builder --chown=bun:bun /app/web/src ./web/src +COPY --from=builder --chown=bun:bun /app/bot ./bot +COPY --from=builder --chown=bun:bun /app/shared ./shared +COPY --from=builder --chown=bun:bun /app/package.json . +COPY --from=builder --chown=bun:bun /app/drizzle.config.ts . +COPY --from=builder --chown=bun:bun /app/tsconfig.json . # Switch to non-root user -USER appuser +USER bun # Expose web dashboard port EXPOSE 3000 diff --git a/bot/commands/admin/update.ts b/bot/commands/admin/update.ts index f4f8d19..15b2548 100644 --- a/bot/commands/admin/update.ts +++ b/bot/commands/admin/update.ts @@ -213,6 +213,18 @@ async function handleDeploy(interaction: any) { }); if (result.success) { + // Save restart context so we can notify on startup + await UpdateService.prepareRestartContext({ + channelId: interaction.channelId, + userId: interaction.user.id, + timestamp: Date.now(), + runMigrations: true, // Always check migrations on deploy + installDependencies: false, // Handled by Docker build + buildWebAssets: false, // Handled by Docker build + previousCommit: result.previousCommit, + newCommit: result.newCommit + }); + await interaction.editReply({ embeds: [getDeploySuccessEmbed( result.previousCommit, diff --git a/shared/modules/admin/update.service.ts b/shared/modules/admin/update.service.ts index 7b503a9..88c5bb3 100644 --- a/shared/modules/admin/update.service.ts +++ b/shared/modules/admin/update.service.ts @@ -1,6 +1,7 @@ import { exec, type ExecException } from "child_process"; import { promisify } from "util"; import { writeFile, readFile, unlink } from "fs/promises"; +import * as path from "path"; import { Client, TextChannel } from "discord.js"; import { getPostRestartEmbed, getPostRestartProgressEmbed, type PostRestartProgress } from "@/modules/admin/update.view"; import type { PostRestartResult } from "@/modules/admin/update.view"; @@ -270,7 +271,8 @@ export class UpdateService { * Prepare restart context with rollback info */ static async prepareRestartContext(context: RestartContext): Promise { - await writeFile(this.CONTEXT_FILE, JSON.stringify(context)); + const filePath = path.join(this.getDeployDir(), this.CONTEXT_FILE); + await writeFile(filePath, JSON.stringify(context)); } /** @@ -325,7 +327,8 @@ export class UpdateService { private static async loadRestartContext(): Promise { try { - const contextData = await readFile(this.CONTEXT_FILE, "utf-8"); + const filePath = path.join(this.getDeployDir(), this.CONTEXT_FILE); + const contextData = await readFile(filePath, "utf-8"); return JSON.parse(contextData) as RestartContext; } catch { return null; @@ -475,7 +478,8 @@ export class UpdateService { private static async cleanupContext(): Promise { try { - await unlink(this.CONTEXT_FILE); + const filePath = path.join(this.getDeployDir(), this.CONTEXT_FILE); + await unlink(filePath); } catch { // File may not exist, ignore }