Stack Focus: Node.js / TypeScript, Prisma, PostgreSQL, Redis, Docker, GitHub Actions, Low-Memory VMs (1GB RAM).
This playbook synthesizes critical lessons learned from resolving cascading deployment failures across Docker containers, Prisma ORM migrations, PostgreSQL authentication persistence, and low-memory Linux virtual machines.
Use this guide as a Day-One Blueprint for future projects to eliminate configuration drift, Out-Of-Memory (OOM) crashes, and CI/CD bottlenecks.
- Core Architectural Rules
- Detailed Post-Mortem & Technical Lessons
- 1. Package Dependency & Runtime Pruning
- 2. Cross-Platform Scripting & Shell Compatibility
- 3. Database Password Persistence & Volume Freezing
- 4. Multi-File Prisma Schema & Runtime Docker Baking
- 5. Host-to-Container Volume Mount Pitfalls
- 6. Service Readiness vs. Container Running State
- 7. Memory Overhead & OOM (Exit Status 137)
- 8. GitHub Actions Commit Revision & Re-Run Traps
- Production-Ready Configuration Templates
- Day-One Pre-Flight Checklist
-
Bake, Don't Mount: Always copy Prisma schemas and configuration files directly into Docker runtime images during the build phase. Do not rely on relative host volume mounts (
-v ./prisma:...) in production or CI/CD. -
Single Source of Credential Truth: Passwords must flow strictly from
.envor CI Secrets down todocker-compose.ymlusing${POSTGRES_PASSWORD}syntax. Never hardcode fallback strings in shell scripts. -
Control Memory Execution: On 1GB RAM instances, never execute
docker compose up -dfor the entire stack at once. Use staggered startup (Dependencies$\rightarrow$ Migrations$\rightarrow$ Apps) and always enable 2GB swap space. -
Direct Binary Execution: Run pre-installed node binaries directly (
./node_modules/.bin/prisma) inside production containers. Avoidnpx,pnpm exec, orcorepackat runtime to prevent RAM spikes. -
Verify Commit SHAs in CI: Never use GitHub UI's "Re-run failed jobs" after pushing new commits. Always confirm that the running workflow's Git commit SHA matches your latest remote commit (
git rev-parse HEAD).
- The Error:
Process exited with status 127(Command / Binary not found). - Root Cause: Moving
prismatodevDependenciescausedpnpm prune --prodto remove the./node_modules/.bin/prismaexecutable during the production Docker build stage. - Prevention: Any CLI utility executed inside production containers (e.g.,
prisma db push,knex migrate, DB seeds) must reside in maindependenciesinpackage.json.
- The Error:
scriptsci-local.cmd: not foundor syntax failure on Linux runners. - Root Cause:
package.jsoncalled Windows-specific backslash paths (scripts\ci-local.cmd). Linux environments interpret backslashes as escape characters rather than directory separators. - Prevention:
- Standardize package scripts on POSIX syntax:
"ci:local": "sh scripts/ci-local.sh". - Maintain OS-neutral bash scripts for local and CI workflows, or use cross-platform Node.js runners (
zx,tsx).
- Standardize package scripts on POSIX syntax:
- The Error:
P1000: Authentication failed against database server. - Root Cause: The official PostgreSQL Docker image initializes its database directory (
/var/lib/postgresql/data) and credentials only on the first run. Updating.envordocker-compose.ymlpasswords afterwards has no effect on an existing named volume (postgres_data). - Prevention:
- Local Development: Reset volumes explicitly when credentials change:
docker compose down -v. - Production: Never run
down -v. ExecuteALTER USER postgres WITH PASSWORD 'new_pass';directly inside the running container, then update environment secrets. - Eliminate hardcoded fallback credentials in helper scripts (e.g.,
ci-local.sh).
- Local Development: Reset volumes explicitly when credentials change:
- The Error:
Could not find Prisma Schema that is required for this command. - Root Cause: Projects using custom/multi-file schema layouts (
schema: "prisma/schema") require both the schema directory andprisma.config.ts. In multi-stage Docker builds, build stage assets do not automatically transfer to the final runtime stage unless explicitly copied. - Prevention: Ensure the final stage in your
Dockerfileexplicitly includes:COPY --from=build /app/prisma.config.ts ./ COPY prisma ./prisma
- The Error:
prisma/schema.prisma: file not foundduringdocker compose run. - Root Cause: Relative host volume paths (
-v ./prisma:/app/prisma) resolve differently across local OS, GitHub Codespaces, and remote cloud VMs. If the parent config or schema fragment is missing from the mount, Prisma fails completely. - Prevention: Eliminate volume mounts for code and config files in deployment scripts. Rely exclusively on files baked into the image.
- The Error:
P1001: Can't reach database server. - Root Cause: Running
docker compose up -d postgresstarts the container process, but the PostgreSQL database process takes several seconds to accept incoming socket connections. - Prevention:
- Use healthchecks with
condition: service_healthyordocker compose up -d --wait postgres. - Wrap migration commands in a retry loop:
until ./node_modules/.bin/prisma db push; do echo "Waiting for PostgreSQL connection..." sleep 2 done
- Use healthchecks with
- The Error:
Process exited with status 137(SIGKILL by Linux OOM Killer). - Root Cause: Concurrent builds and simultaneous container startups (
app,worker,postgres,redis,migrate) exceed 1GB RAM. Dynamic package runners (npx,corepack) also introduce significant memory spikes. - Prevention:
- Configure Swap: Allocate a 2GB
/swapfileon low-cost single-core VMs. - Direct Binaries: Execute
./node_modules/.bin/prismadirectly. - Stagger Startup: Launch infrastructure containers first, run migrations in isolation, and start application services last.
- Configure Swap: Allocate a 2GB
- The Error: Fixes committed locally fail to reflect during remote deployment.
- Root Cause: Clicking "Re-run jobs" on a failed GitHub Actions run re-executes the historical commit SHA associated with that run, ignoring newer commits pushed to
main. - Prevention:
- Always verify the Git commit hash in deployment logs:
HEAD is now at <commit_sha>. - Always push a new commit to trigger a fresh GitHub Actions workflow run instead of re-running stale jobs.
- Always verify the Git commit hash in deployment logs:
# Stage 1: Base
FROM node:20-alpine AS base
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
# Stage 2: Dependencies
FROM base AS deps
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml* ./
COPY prisma ./prisma
COPY prisma.config.ts tsconfig.prisma.json ./
RUN pnpm install --frozen-lockfile
# Stage 3: Build
FROM deps AS build
COPY tsconfig.json ./
COPY src ./src
RUN pnpm prisma generate
RUN pnpm build
# Stage 4: Production Dependencies Prune
FROM deps AS prod-deps
RUN pnpm prune --prod
# Stage 5: Final Runtime Image
FROM base AS runner
ENV NODE_ENV=production
# Copy node_modules and built dist
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
# CRITICAL: Copy Prisma schemas and config files for runtime migrations
COPY --from=build /app/prisma ./prisma
COPY --from=build /app/prisma.config.ts ./prisma.config.ts
EXPOSE 5000
CMD ["node", "dist/server.js"]version: '3.8'
services:
postgres:
image: postgres:16-alpine
restart: always
ports:
- "5432:5432"
env_file:
- .env
environment:
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres123}
POSTGRES_DB: ${POSTGRES_DB:-ticket_db}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-ticket_db}"]
interval: 3s
timeout: 3s
retries: 10
start_period: 5s
redis:
image: redis:7-alpine
restart: always
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 3s
timeout: 3s
retries: 10
app:
build: .
restart: always
ports:
- "5000:5000"
env_file:
- .env
environment:
DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres123}@postgres:5432/${POSTGRES_DB:-ticket_db}?schema=public
REDIS_URL: redis://redis:6379
NODE_ENV: production
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
worker:
build: .
restart: always
command: ["node", "dist/workers/worker.js"]
env_file:
- .env
environment:
DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres123}@postgres:5432/${POSTGRES_DB:-ticket_db}?schema=public
REDIS_URL: redis://redis:6379
NODE_ENV: production
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
volumes:
postgres_data:
redis_data:#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="$HOME/distributed-ticket-engine-redis"
cd "$PROJECT_DIR"
echo "=== 1. Cleaning Git Locks and Syncing Code ==="
rm -f .git/refs/remotes/origin/main.lock .git/index.lock .git/FETCH_HEAD || true
git fetch origin main --prune --force
git reset --hard origin/main
echo "=== 2. Stopping Old Containers ==="
docker compose down --remove-orphans || true
echo "=== 3. Starting Database & Redis (Low-Memory Stagger) ==="
docker compose up -d --build postgres redis
echo "=== 4. Waiting for PostgreSQL Readiness ==="
until docker compose exec -T postgres pg_isready -U postgres; do
echo "Waiting for database socket..."
sleep 2
done
echo "=== 5. Running Database Migrations in Isolated Container ==="
docker compose run --rm app ./node_modules/.bin/prisma db push
echo "=== 6. Launching Application & Worker Services ==="
docker compose up -d app worker
echo "=== 7. Cleaning Build Cache ==="
docker builder prune -f
echo "✅ Deployment completed successfully!"Before pushing new features or deploying to production, verify every item:
-
"prisma"is listed in"dependencies"(not"devDependencies"). - All package scripts use POSIX-compliant paths (
sh scripts/ci-local.sh). - No Windows-specific batch scripts (
.cmd) are invoked inside Linux pipelines.
-
Dockerfilecopiesprisma/schema folder into the final runtime image. -
Dockerfilecopies custom configuration files (prisma.config.ts) into the final runtime image. - Direct binaries (
./node_modules/.bin/prisma) are used instead ofnpxorpnpm exec.
-
docker-compose.ymluses${POSTGRES_PASSWORD}variables matching.env. - Fallback connection strings in shell scripts match
.envdefault values. - Local database credential updates are accompanied by
docker compose down -v.
- Server has a 2GB Swap file configured (
swapon --show). - Deployment script executes in staggered order: DB/Redis
$\rightarrow$ Migration$\rightarrow$ App/Worker. - Database readiness (
pg_isready) is confirmed before migration execution. - Latest Git commit SHA matches the running GitHub Actions workflow execution log.