TL;DR: The article walks you through automating blog image creation using n8n and AI. Instead of making images manually, the workflow reads your article, generates smart image prompts, creates AI images, and then automatically adds your branding and Open Graph header. In the end, you get polished, consistent images ready to use for every blog post—without lifting a finger.
I do a lot of video automation in n8n, and at some point I realised the tooling wasn’t the hard part. The friction came from everything around it: missing FFmpeg, unclear file paths, instance data scattered somewhere inside Docker, and setups that were annoying to move or duplicate.
This is the local workflow I’ve settled on. It’s simple, predictable, and designed so I don’t have to think about it once it’s running.
What this setup gives you (the non-negotiables)
There are three things I care about, and this setup guarantees all of them.
First, FFmpeg is always installed. It’s baked directly into the Docker image via the Dockerfile. Any Execute Command node can assume FFmpeg exists.
Second, there’s one obvious place for files on my machine: .n8n_files/
Inputs go in there. Outputs come out there. No guessing.
Third, there’s one obvious place for the entire n8n instance: .n8n_docker/.n8n/
That folder contains workflows, credentials, users, settings, the SQLite database, everything.
Inside the container, the mapping stays boring and predictable:
.n8n_files/→/home/node/.n8n-files.n8n_docker/.n8n/→/home/node/.n8n
Those two mounts are the backbone of the whole setup, and they’re defined in docker-compose.yml.
Why I stopped using default setups
Cloud n8n and stock Docker installs work fine until you start doing real file-based work. Video makes every weak assumption obvious. Files aren’t where you expect them to be. FFmpeg may or may not exist. Backups feel risky.
What I wanted was control without complexity. If something breaks, I want to be able to see why by looking at a folder on my machine.
Step-by-step: how I set this up
This is the exact flow I use whenever I spin up a new local instance.
Step 0 — Prerequisites
You just need Docker Desktop. Docker Compose comes with it.
Install it for your platform:
- Mac: Docker Desktop for Mac
- Windows: Docker Desktop for Windows
- Linux: Docker Desktop for Linux
Once Docker is running, you’re good.
Follow the docs here: https://docs.docker.com/compose/gettingstarted/
Step 1 — Open the project folder
Create a new directory anywhere on your system, I'll use:
n8n-portable-video-local-workflow/
Create two files:
# Stage 1: Get apk from alpine
FROM alpine:latest AS alpine
# Stage 2: Build the custom n8n image
FROM n8nio/n8n:latest
# Copy apk and its dependencies from Alpine stage
COPY --from=alpine /sbin/apk /sbin/apk
COPY --from=alpine /usr/lib/libapk.so* /usr/lib/
# Switch to root user to install ffmpeg
USER root
# Install ffmpeg using apk and clean up
RUN apk update && apk add --no-cache ffmpeg && rm -rf /var/cache/apk/*
# Switch back to the non-root 'node' user for security
USER nodeversion: "3.8"
services:
n8n:
build: .
container_name: n8n
ports:
- "5678:5678"
env_file:
- .env
environment:
- GENERIC_TIMEZONE=Australia/Sydney
- TZ=Australia/Sydney
- NODES_EXCLUDE="[]"
- N8N_ENV_ACCESS_ALLOW_LIST=EXAMPLE_API_KEY
- N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES=false
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=false
- N8N_FILESYSTEM_ALLOW_LIST=/files
- N8N_RUNNERS_ENABLED=true
- N8N_HOST=localhost
- N8N_BLOCK_ENV_ACCESS_IN_NODE=false
- N8N_PAYLOAD_SIZE_MAX=250
- N8N_RUNNERS_TASK_TIMEOUT=2400
- N8N_RUNNERS_TASK_REQUEST_TIMEOUT=240
- N8N_DIAGNOSTICS_ENABLED=false
- N8N_VERSION_NOTIFICATIONS_ENABLED=false
- N8N_TEMPLATES_ENABLED=false
- EXTERNAL_FRONTEND_HOOKS_URLS=
- N8N_DIAGNOSTICS_CONFIG_FRONTEND=
- N8N_DIAGNOSTICS_CONFIG_BACKEND=
volumes:
- ./.n8n_docker/.n8n:/home/node/.n8n
- ./.n8n-files:/home/node/.n8n-files
restart: unless-stoppedStep 2 — Create your env
Create a file called .env next to docker-compose.yml.
It can be empty to start. The important thing is that it exists and is ignored by Git, so you can safely add secrets later.
If you want a placeholder to confirm everything is wired correctly, you can add something like:
EXAMPLE_API_KEY=TEST_VALUEYou don’t need to use it yet. It’s just there when you need it.
Step 3 — Start n8n (builds the FFmpeg image)
From the project folder, run:
docker compose up --build
The first run takes a little longer because Docker builds the image and installs FFmpeg.
Once it’s up, open:
http://localhost:5678
Create your owner account when prompted. From this point on, your instance data persists because it lives in .n8n_docker.
Step 4 — Know where things live (so paths stay boring)
This is the part that makes everything easier later.
- Working files (input and output)
Local:.n8n_files/
Inside n8n:/home/node/.n8n-files - Instance data (portable)
Local:.n8n_docker/.n8n/
This is your entire n8n instance in a folder.
Step 5 — Sanity-check FFmpeg
I always do this once on a fresh setup.
Drop a video into .n8n_files/ on your machine, for example input.mov.
In n8n, create a workflow with an Execute Command node.
Use this command:
ffmpeg -y -i /home/node/.n8n-files/input.mov -c:v libx264 -c:a aac /home/node/.n8n-files/output.mp4
Run the workflow.
Check .n8n_files/ on your machine.
If output.mp4 appears, everything is working: mounts, permissions, FFmpeg, all of it.
Day-to-day usage
Most days, I only use a handful of commands:
- Start:
docker compose up - Start in background:
docker compose up -d - Stop:
docker compose down - View logs:
docker compose logs -f
Nothing exotic. Easy to remember.
The real payoff: multiple instances without pain
This is where the setup really pays off.
Because the entire instance lives in .n8n_docker/.n8n/, switching projects is just folder management.
Create a fresh instance
docker compose down
mv .n8n_docker .n8n_docker_clientA
mkdir -p .n8n_docker/.n8n
docker compose up -dYou’re now running a completely clean n8n instance.
Switch back later
docker compose down
mv .n8n_docker .n8n_docker_clientB
mv .n8n_docker_clientA .n8n_docker
docker compose up -dNo exports or migrations, just simple folders doing exactly what you expect.
Why I’ve stuck with this
There’s nothing clever here, and that’s the point.
FFmpeg is always available. File paths don’t change. Backups are trivial. If something breaks, I can usually tell why by looking at a directory instead of digging through Docker internals.
If you’re serious about video automation in n8n, this kind of setup removes a lot of unnecessary friction.
I’ve bundled this exact workflow in this Github Repository, but even if you build your own version, the core idea is what matters: keep it local, keep it boring, and make everything easy to replace.
