| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 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. | |