# ═══════════════════════════════════════════════════════════════════════════ # n8n + FFmpeg — Production Dockerfile # n8n version : 1.90.0 (latest stable 1.x as of April 2026) # Base : Official n8nio/n8n:1.90.0 (Alpine-based, Node.js 20) # Target : 1 CPU / 2 GB RAM (Railway / CloudStation) # # WHY 1.90.0 AND NOT 2.x: # n8n 2.0 disabled ExecuteCommand and LocalFileTrigger nodes BY DEFAULT. # Your FFmpeg Assembly workflow uses Execute Command nodes for every # phase (FFprobe, Phase1, Phase2, Assembly A/B/C). Using 2.x would break # the entire Assembly pipeline without a major workflow rewrite. # 1.90.0 is the latest stable 1.x release — safe, production-tested, # with all the bug fixes from 1.88 and 1.89. # # CHANGES FROM YOUR OLD DOCKERFILE (1.88.0): # 1. Pinned to 1.90.0 — fixes editor lock bug + task runner improvements # 2. N8N_RUNNERS_ENABLED=true — task runner support (stable in 1.90) # 3. NODE_OPTIONS heap capped at 512MB — was uncapped → OOM kills # 4. EXECUTIONS_PROCESS=main — no worker fork → saves ~200MB RAM # 5. N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true → fixes editor lock # 6. WEBHOOK_URL correctly set → fixes OAuth callback URI mismatch # 7. N8N_EDITOR_BASE_URL set → fixes redirect URI shown in credentials UI # 8. N8N_ENCRYPTION_KEY as explicit env → credentials survive restarts # 9. Execution pruning enabled → SQLite stays small # 10. Healthcheck uses correct /healthz endpoint # ═══════════════════════════════════════════════════════════════════════════ FROM n8nio/n8n:1.90.0 # ── Root only for package installation ─────────────────────────────────── USER root # ── Install system dependencies in a single layer ──────────────────────── # # ffmpeg — video encoding: zoompan, drawtext, libx264, aac, concat # wget — Phase 1: download images from imgbb/external URLs # fontconfig — fc-list: font path discovery for drawtext subtitle filter # font-dejavu — DejaVuSans-Bold.ttf: used in Assembly B/C subtitle burn # ca-certificates — HTTPS requests to Google APIs, imgbb without SSL errors # # Alpine's ffmpeg package includes: libx264, aac, wav/pcm, png codecs, # zoompan filter, drawtext filter, thumbnail filter — everything the # Assembly workflow needs. Codecs not used (libvpx, x265 etc.) are compiled # in but do not consume RAM until invoked. RUN apk add --no-cache \ ffmpeg \ wget \ fontconfig \ font-dejavu \ ca-certificates \ && fc-cache -f \ && echo "=== BUILD VERIFY ===" \ && ffmpeg -version 2>&1 | head -1 \ && ffprobe -version 2>&1 | head -1 \ && fc-list | grep -i "DejaVu" | head -2 \ && echo "=== DONE ===" # ── FFmpeg working directory ────────────────────────────────────────────── # Assembly workflow writes to /tmp/sfcm/{run_id}/ # sticky bit allows node user to create subdirectories RUN mkdir -p /tmp/sfcm \ && chmod 1777 /tmp/sfcm \ && chown node:node /tmp/sfcm # ── Back to unprivileged node user ──────────────────────────────────────── USER node # ═══════════════════════════════════════════════════════════════════════════ # ENVIRONMENT VARIABLES — Memory # ═══════════════════════════════════════════════════════════════════════════ # Cap Node.js JS heap at 512MB. # Without this, n8n defaults to ~1.5GB on a 2GB system. # During FFmpeg zoompan (Phase 2), FFmpeg itself uses 400-500MB. # Total without cap: 1500 + 500 = OOM. Total with cap: 512 + 500 = 1012MB. ENV NODE_OPTIONS="--max-old-space-size=512" # ═══════════════════════════════════════════════════════════════════════════ # ENVIRONMENT VARIABLES — n8n Execution Engine # ═══════════════════════════════════════════════════════════════════════════ # Run executions in the main process — no worker subprocess spawning. # Saves ~150-200MB vs forked worker mode. Essential on 2GB RAM. ENV EXECUTIONS_PROCESS=main # Task runners: stable in 1.90, required for Code node isolation. # Internal mode (default) — no separate runner container needed. ENV N8N_RUNNERS_ENABLED=true ENV N8N_RUNNERS_MODE=internal # Store binary file data on disk, not in SQLite. # Without this, video files (~50-200MB) pass through the database → crash. ENV N8N_DEFAULT_BINARY_DATA_MODE=filesystem # Prevent the "editor locked" bug. # This happens when n8n cannot write its settings file on startup. ENV N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true # ═══════════════════════════════════════════════════════════════════════════ # ENVIRONMENT VARIABLES — Execution Pruning # ═══════════════════════════════════════════════════════════════════════════ # Without pruning, SQLite grows forever → disk full → all workflows fail. # Keep 72 hours of history, max 200 executions stored at once. ENV EXECUTIONS_DATA_PRUNE=true ENV EXECUTIONS_DATA_MAX_AGE=72 ENV EXECUTIONS_DATA_PRUNE_MAX_COUNT=200 # ═══════════════════════════════════════════════════════════════════════════ # ENVIRONMENT VARIABLES — Network / OAuth (THE GOOGLE CREDENTIAL FIX) # ═══════════════════════════════════════════════════════════════════════════ # ── WEBHOOK_URL ─────────────────────────────────────────────────────────── # THE most important setting. Set this to your actual public URL. # This is the root cause of your Google credential expiry issue: # # If WEBHOOK_URL is wrong or missing: # → n8n shows "http://localhost:5678/rest/oauth2-credential/callback" # as the redirect URI in the credentials screen # → You register a DIFFERENT URL in Google Cloud Console # → OAuth handshake fails silently on first refresh attempt # → n8n falls back to stored token which expires after 7 days (Testing mode) # or 1 hour (if no valid refresh token was ever stored) # → You see "credential expired" every N hours/days # # FIX: Set WEBHOOK_URL to your exact Railway/CloudStation domain. # Override this at deploy time via your hosting platform's env vars. # Do NOT hardcode the real URL here if this file is in a git repo. ENV WEBHOOK_URL=https://your-n8n-domain.up.railway.app # Must match WEBHOOK_URL exactly. # n8n uses this to build the OAuth redirect URI shown in the credentials # setup modal — the one you paste into Google Cloud Console. ENV N8N_EDITOR_BASE_URL=https://your-n8n-domain.up.railway.app # ═══════════════════════════════════════════════════════════════════════════ # ENVIRONMENT VARIABLES — Credential Encryption Key # ═══════════════════════════════════════════════════════════════════════════ # CRITICAL: Without a fixed encryption key, n8n generates a NEW random key # on every container restart. All saved credentials become unreadable. # You then see "could not decrypt credentials" — which looks like expiry but # is actually a key rotation problem. # # HOW TO GENERATE: Run this once locally: # openssl rand -hex 32 # Copy the output. Set it as N8N_ENCRYPTION_KEY in Railway/CloudStation # environment variables. Replace the placeholder below with your key, # OR better: leave the placeholder here and set the real value only in # your hosting platform's secret manager (never commit the real key). ENV N8N_ENCRYPTION_KEY=REPLACE_WITH_YOUR_32_BYTE_HEX_KEY # ═══════════════════════════════════════════════════════════════════════════ # ENVIRONMENT VARIABLES — Timezone + Port # ═══════════════════════════════════════════════════════════════════════════ # Pakistan Standard Time (UTC+5) — affects Schedule Trigger node timing ENV GENERIC_TIMEZONE=Asia/Karachi ENV TZ=Asia/Karachi ENV N8N_PORT=5678 # ═══════════════════════════════════════════════════════════════════════════ # ENVIRONMENT VARIABLES — Reduce Noise # ═══════════════════════════════════════════════════════════════════════════ ENV N8N_DIAGNOSTICS_ENABLED=false ENV N8N_VERSION_NOTIFICATIONS_ENABLED=false ENV N8N_LOG_LEVEL=info # ═══════════════════════════════════════════════════════════════════════════ # PORT + HEALTHCHECK # ═══════════════════════════════════════════════════════════════════════════ EXPOSE 5678 # /healthz is the correct health endpoint for n8n 1.x # start-period=90s gives n8n time to initialize SQLite and load workflows HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \ CMD wget -qO- http://localhost:5678/healthz || exit 1 # The n8nio/n8n base image already sets: # ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"] # CMD ["n8n"] # No override needed.