FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/server.js"]
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/server.js"]
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/server.js"]
# ---- Stage 1: Install all dependencies ----
FROM node:22-alpine AS deps
WORKDIR /app # Enable pnpm via corepack
RUN corepack enable && corepack prepare pnpm@latest --activate COPY package.json pnpm-lock.yaml ./
COPY prisma ./prisma/ RUN pnpm install --frozen-lockfile
RUN pnpm prisma generate # ---- Stage 2: Build frontend and backend ----
FROM node:22-alpine AS builder
WORKDIR /app RUN corepack enable && corepack prepare pnpm@latest --activate COPY --from=deps /app/node_modules ./node_modules
COPY . . # Build Vite frontend (outputs to client/dist/)
RUN pnpm run build:client # Build Fastify backend with esbuild (outputs to dist/)
RUN pnpm run build:server # ---- Stage 3: Production image ----
FROM node:22-alpine AS production
WORKDIR /app RUN corepack enable && corepack prepare pnpm@latest --activate # Only copy production dependencies
COPY package.json pnpm-lock.yaml ./
COPY prisma ./prisma/ RUN pnpm install --frozen-lockfile --prod
RUN pnpm prisma generate # Copy built artifacts
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/client/dist ./client/dist # Non-root user
RUN addgroup -g 1001 appgroup && \ adduser -u 1001 -G appgroup -s /bin/sh -D appuser
USER appuser EXPOSE 3000 CMD ["node", "dist/server.js"]
# ---- Stage 1: Install all dependencies ----
FROM node:22-alpine AS deps
WORKDIR /app # Enable pnpm via corepack
RUN corepack enable && corepack prepare pnpm@latest --activate COPY package.json pnpm-lock.yaml ./
COPY prisma ./prisma/ RUN pnpm install --frozen-lockfile
RUN pnpm prisma generate # ---- Stage 2: Build frontend and backend ----
FROM node:22-alpine AS builder
WORKDIR /app RUN corepack enable && corepack prepare pnpm@latest --activate COPY --from=deps /app/node_modules ./node_modules
COPY . . # Build Vite frontend (outputs to client/dist/)
RUN pnpm run build:client # Build Fastify backend with esbuild (outputs to dist/)
RUN pnpm run build:server # ---- Stage 3: Production image ----
FROM node:22-alpine AS production
WORKDIR /app RUN corepack enable && corepack prepare pnpm@latest --activate # Only copy production dependencies
COPY package.json pnpm-lock.yaml ./
COPY prisma ./prisma/ RUN pnpm install --frozen-lockfile --prod
RUN pnpm prisma generate # Copy built artifacts
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/client/dist ./client/dist # Non-root user
RUN addgroup -g 1001 appgroup && \ adduser -u 1001 -G appgroup -s /bin/sh -D appuser
USER appuser EXPOSE 3000 CMD ["node", "dist/server.js"]
# ---- Stage 1: Install all dependencies ----
FROM node:22-alpine AS deps
WORKDIR /app # Enable pnpm via corepack
RUN corepack enable && corepack prepare pnpm@latest --activate COPY package.json pnpm-lock.yaml ./
COPY prisma ./prisma/ RUN pnpm install --frozen-lockfile
RUN pnpm prisma generate # ---- Stage 2: Build frontend and backend ----
FROM node:22-alpine AS builder
WORKDIR /app RUN corepack enable && corepack prepare pnpm@latest --activate COPY --from=deps /app/node_modules ./node_modules
COPY . . # Build Vite frontend (outputs to client/dist/)
RUN pnpm run build:client # Build Fastify backend with esbuild (outputs to dist/)
RUN pnpm run build:server # ---- Stage 3: Production image ----
FROM node:22-alpine AS production
WORKDIR /app RUN corepack enable && corepack prepare pnpm@latest --activate # Only copy production dependencies
COPY package.json pnpm-lock.yaml ./
COPY prisma ./prisma/ RUN pnpm install --frozen-lockfile --prod
RUN pnpm prisma generate # Copy built artifacts
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/client/dist ./client/dist # Non-root user
RUN addgroup -g 1001 appgroup && \ adduser -u 1001 -G appgroup -s /bin/sh -D appuser
USER appuser EXPOSE 3000 CMD ["node", "dist/server.js"]
import fastifyStatic from "@fastify/static";
import { join } from "path"; fastify.register(fastifyStatic, { root: join(__dirname, "../client/dist"), prefix: "/", wildcard: false,
}); // SPA fallback -- serve index.html for all non-API routes
fastify.setNotFoundHandler(async (request, reply) => { if (request.url.startsWith("/api/")) { return reply.status(404).send({ error: "Not found" }); } return reply.sendFile("index.html");
});
import fastifyStatic from "@fastify/static";
import { join } from "path"; fastify.register(fastifyStatic, { root: join(__dirname, "../client/dist"), prefix: "/", wildcard: false,
}); // SPA fallback -- serve index.html for all non-API routes
fastify.setNotFoundHandler(async (request, reply) => { if (request.url.startsWith("/api/")) { return reply.status(404).send({ error: "Not found" }); } return reply.sendFile("index.html");
});
import fastifyStatic from "@fastify/static";
import { join } from "path"; fastify.register(fastifyStatic, { root: join(__dirname, "../client/dist"), prefix: "/", wildcard: false,
}); // SPA fallback -- serve index.html for all non-API routes
fastify.setNotFoundHandler(async (request, reply) => { if (request.url.startsWith("/api/")) { return reply.status(404).send({ error: "Not found" }); } return reply.sendFile("index.html");
});
services: traefik: image: traefik:v3 command: - "--providers.docker=true" - "--providers.docker.exposedbydefault=false" - "--entrypoints.web.address=:80" - "--entrypoints.websecure.address=:443" - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web" - "[email protected]" - "--certificatesresolvers.letsencrypt.acme.storage=/certs/acme.json" - "--entrypoints.web.http.redirections.entrypoint.to=websecure" ports: - "80:80" - "443:443" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - traefik-certs:/certs app: build: context: . dockerfile: Dockerfile labels: - "traefik.enable=true" - "traefik.http.routers.app.rule=Host(`app.example.com`)" - "traefik.http.routers.app.entrypoints=websecure" - "traefik.http.routers.app.tls.certresolver=letsencrypt" - "traefik.http.services.app.loadbalancer.server.port=3000" environment: DATABASE_URL: "postgresql://postgres:secret@db:5432/myapp" NODE_ENV: production depends_on: db: condition: service_healthy db: image: postgres:16-alpine environment: POSTGRES_PASSWORD: secret POSTGRES_DB: myapp volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 3s retries: 5 volumes: pgdata: traefik-certs:
services: traefik: image: traefik:v3 command: - "--providers.docker=true" - "--providers.docker.exposedbydefault=false" - "--entrypoints.web.address=:80" - "--entrypoints.websecure.address=:443" - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web" - "[email protected]" - "--certificatesresolvers.letsencrypt.acme.storage=/certs/acme.json" - "--entrypoints.web.http.redirections.entrypoint.to=websecure" ports: - "80:80" - "443:443" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - traefik-certs:/certs app: build: context: . dockerfile: Dockerfile labels: - "traefik.enable=true" - "traefik.http.routers.app.rule=Host(`app.example.com`)" - "traefik.http.routers.app.entrypoints=websecure" - "traefik.http.routers.app.tls.certresolver=letsencrypt" - "traefik.http.services.app.loadbalancer.server.port=3000" environment: DATABASE_URL: "postgresql://postgres:secret@db:5432/myapp" NODE_ENV: production depends_on: db: condition: service_healthy db: image: postgres:16-alpine environment: POSTGRES_PASSWORD: secret POSTGRES_DB: myapp volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 3s retries: 5 volumes: pgdata: traefik-certs:
services: traefik: image: traefik:v3 command: - "--providers.docker=true" - "--providers.docker.exposedbydefault=false" - "--entrypoints.web.address=:80" - "--entrypoints.websecure.address=:443" - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web" - "[email protected]" - "--certificatesresolvers.letsencrypt.acme.storage=/certs/acme.json" - "--entrypoints.web.http.redirections.entrypoint.to=websecure" ports: - "80:80" - "443:443" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - traefik-certs:/certs app: build: context: . dockerfile: Dockerfile labels: - "traefik.enable=true" - "traefik.http.routers.app.rule=Host(`app.example.com`)" - "traefik.http.routers.app.entrypoints=websecure" - "traefik.http.routers.app.tls.certresolver=letsencrypt" - "traefik.http.services.app.loadbalancer.server.port=3000" environment: DATABASE_URL: "postgresql://postgres:secret@db:5432/myapp" NODE_ENV: production depends_on: db: condition: service_healthy db: image: postgres:16-alpine environment: POSTGRES_PASSWORD: secret POSTGRES_DB: myapp volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 3s retries: 5 volumes: pgdata: traefik-certs:
node_modules
dist
client/dist
.git
*.md
.env*
.vscode
node_modules
dist
client/dist
.git
*.md
.env*
.vscode
node_modules
dist
client/dist
.git
*.md
.env*
.vscode
generator client { provider = "prisma-client-js" binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
}
generator client { provider = "prisma-client-js" binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
}
generator client { provider = "prisma-client-js" binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
}
#!/bin/bash
set -euo pipefail SERVER="user@your-vps-ip"
APP_DIR="/opt/apps/myapp" echo "Syncing files..."
rsync -az --delete \ --exclude node_modules \ --exclude .git \ --exclude .env \ ./ "$SERVER:$APP_DIR/" echo "Building and starting..."
ssh "$SERVER" "cd $APP_DIR && docker compose up -d --build" echo "Done. Checking health..."
sleep 5
curl -sf "https://app.example.com/api/health" && echo " OK" || echo " FAILED"
#!/bin/bash
set -euo pipefail SERVER="user@your-vps-ip"
APP_DIR="/opt/apps/myapp" echo "Syncing files..."
rsync -az --delete \ --exclude node_modules \ --exclude .git \ --exclude .env \ ./ "$SERVER:$APP_DIR/" echo "Building and starting..."
ssh "$SERVER" "cd $APP_DIR && docker compose up -d --build" echo "Done. Checking health..."
sleep 5
curl -sf "https://app.example.com/api/health" && echo " OK" || echo " FAILED"
#!/bin/bash
set -euo pipefail SERVER="user@your-vps-ip"
APP_DIR="/opt/apps/myapp" echo "Syncing files..."
rsync -az --delete \ --exclude node_modules \ --exclude .git \ --exclude .env \ ./ "$SERVER:$APP_DIR/" echo "Building and starting..."
ssh "$SERVER" "cd $APP_DIR && docker compose up -d --build" echo "Done. Checking health..."
sleep 5
curl -sf "https://app.example.com/api/health" && echo " OK" || echo " FAILED" - deps -- installs all dependencies and generates the Prisma client
- builder -- compiles TypeScript and bundles the frontend
- production -- copies only what's needed to run - Host() rule routes traffic for your domain
- certresolver=letsencrypt triggers automatic certificate provisioning
- The HTTP-to-HTTPS redirect is on the Traefik entrypoint level - Separate install, build, and run into distinct stages
- Copy only artifacts into the final image (no source, no dev deps)
- Run as non-root in production
- Use .dockerignore to keep the build context small