assembly / Dockerfile
Echo010's picture
Create Dockerfile
3a4589a verified
# ═══════════════════════════════════════════════════════════════════════════
# 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.