I have a docker-compose.yml file as follows:
services:
postgres:
image: postgres:latest
container_name: postgres-demo-zenpr-net
restart: unless-stopped
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
volumes: - ./postgres_data:/var/lib/postgresql/data
ports: - “5456:5432”
n8n:
build:
context: .
dockerfile: Dockerfile
container_name: demo-zenpr-net
restart: unless-stopped
ports: - “3456:5678”
depends_on: - postgres
environment:
Variables taken from .env file (or system environment)
- N8N_HOST=${SUBDOMAIN}.${DOMAIN_NAME}
- WEBHOOK_URL=https://${SUBDOMAIN}.${DOMAIN_NAME}/
Specify DB type + Postgres connection information
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
- DB_POSTGRESDB_USER=${POSTGRES_USER}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
Enable saving files to hard drive instead of embedding in DB
- N8N_DEFAULT_BINARY_DATA_MODE=${N8N_DEFAULT_BINARY_DATA_MODE}
Tighten config file permissions (or disable warnings)
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=${N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS}
Timezone
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
Set cleanup of executions history
EXECUTIONS_DATA_PRUNE=${EXECUTIONS_DATA_PRUNE}
EXECUTIONS_DATA_MAX_AGE=${EXECUTIONS_DATA_MAX_AGE}
EXECUTIONS_DATA_PRUNE_MAX_COUNT=${EXECUTIONS_DATA_PRUNE_MAX_COUNT}
Basic auth
-
N8N_BASIC_AUTH_ACTIVE=true
-
N8N_BASIC_AUTH_USER=${N8N_BASIC_AUTH_USER}
-
N8N_BASIC_AUTH_PASSWORD=${N8N_BASIC_AUTH_PASSWORD}
volumes: -
./n8n_data:/home/node/.n8n
The .env file is as follows:
#===== Domain name information =====#
DOMAIN_NAME=zenpr.net
SUBDOMAIN=demo
When starting, n8n will listen at https://demo.zenpr.net
#===== Postgres information =====#
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_DB=
General time zone configuration
GENERIC_TIMEZONE=Asia/Ho_Chi_Minh
#===== Store binary files (attachments…) on hard drive instead of DB =====#
N8N_DEFAULT_BINARY_DATA_MODE=filesystem
#===== config file permissions =====#
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
#===== General time zone =====#
GENERIC_TIMEZONE=Asia/Ho_Chi_Minh
#===== Clean up old logs/executions =====#
EXECUTIONS_DATA_PRUNE=true
EXECUTIONS_DATA_MAX_AGE=168 # (hours)
EXECUTIONS_DATA_PRUNE_MAX_COUNT=50000
#===== Basic Auth for n8n =====#
N8N_BASIC_AUTH_USER=Admin
N8N_BASIC_AUTH_PASSWORD=
And the Dockerfile is as follows:
FROM n8nio/n8n:latest
USER root
RUN apk update && apk add ffmpeg
USER node
Describe the problem/error/question
My workflow does the following:
List and sort files in the specified google drive folder, then download and use ffmeg command to render all video sources including clip, audio, img, srt into 1 single video.
Everything works fine and the output.mp4 file is in the n8n_data folder.
The problem is I don’t know how to read that file on n8n. I tried many ways, Node Read/Write Files from Disk, Node Code and use the following code:
// Get base64 string from node stdout Execute Command
const base64Data = items[0].json.stdout;
return [
{
json: { message: "File converted to binary successfully" },
binary: {
data: {
data: base64Data,
mimeType: "video/mp4",
fileName: "output.mp4",
}
}
}
];
still cannot get or read the output.mp4 file
## What is the error message (if any)?
## Please share your workflow
{
"nodes": [
{
"parameters": {
"jsCode": "// Lấy chuỗi base64 từ stdout của node Execute Command\nconst base64Data = items[0].json.stdout;\n\nreturn [\n {\n json: { message: \"File chuyển sang binary thành công\" },\n binary: {\n data: {\n data: base64Data,\n mimeType: \"video/mp4\",\n fileName: \"output.mp4\",\n }\n }\n }\n];\n"
},
"name": "Set Binary Data1",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-580,
660
],
"id": "daa98ac1-bc1e-43f6-af70-22fa19c0d3e1"
},
{
"parameters": {
"resource": "fileFolder",
"searchMethod": "=",
"filter": {
"folderId": {
"__rl": true,
"value": "1zgW6grvT_RJvUJLP96QX3l0WUPitgyAj",
"mode": "list",
"cachedResultName": "Animated Faceless",
"cachedResultUrl": "https://drive.google.com/drive/folders/1zgW6grvT_RJvUJLP96QX3l0WUPitgyAj"
}
},
"options": {
"fields": [
"id",
"name"
]
}
},
"name": "Google Drive Get All File3",
"type": "n8n-nodes-base.googleDrive",
"typeVersion": 3,
"position": [
-1860,
580
],
"id": "a0ee40e0-1eb2-4f4d-bf7b-6b081c22f4e2",
"credentials": {
"googleDriveOAuth2Api": {
"id": "71gEopHUimnQ9srI",
"name": "Google Drive account layekaloustian15"
}
}
},
{
"parameters": {
"aggregate": "aggregateAllItemData",
"destinationFieldName": "list",
"options": {}
},
"name": "Aggregate Files2",
"type": "n8n-nodes-base.aggregate",
"typeVersion": 1,
"position": [
-1640,
580
],
"id": "aa8b3e23-f096-4b7b-a6af-2be4b0befda1"
},
{
"parameters": {
"jsCode": "// Phân loại file\nconst aggregatedList = items[0].json.list || [];\n\nlet images = [];\nlet videos = [];\nlet audios = [];\nlet subtitles = [];\n\naggregatedList.forEach(file => {\n const url = `https://drive.google.com/uc?id=${file.id}&export=download`;\n const extension = file.name.split('.').pop().toLowerCase();\n\n if ([\"jpg\", \"jpeg\", \"png\", \"gif\"].includes(extension)) {\n images.push({ name: file.name, url });\n } else if ([\"mp4\", \"mov\", \"avi\"].includes(extension)) {\n videos.push({ name: file.name, url });\n } else if ([\"mp3\", \"aac\", \"wav\"].includes(extension)) {\n audios.push({ name: file.name, url });\n } else if (extension === \"srt\") {\n subtitles.push({ name: file.name, url });\n }\n});\n\nreturn [{ images, videos, audios, subtitles }];"
},
"name": "Process Files by Type2",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1440,
580
],
"id": "0243406f-304a-4282-b238-2e27cdd19ab7"
},
{
"parameters": {
"jsCode": "// Đường dẫn làm việc (được mount volume)\nconst workDir = '/home/node/.n8n';\n\n// Lấy danh sách file từ input (mỗi mảng có thể rỗng nếu không có file nào)\nconst { images = [], videos = [], audios = [], subtitles = [] } = items[0].json;\n\n// Mảng chứa các lệnh cần thực thi tuần tự\nlet arrCommands = [];\n\n// ----------------------------------------------------------------\n// 1. TẢI CÁC FILE VỀ (video, audio, image, subtitle)\n// ----------------------------------------------------------------\nvideos.forEach((file, i) => {\n arrCommands.push(`wget \"${file.url}\" -O ${workDir}/video${i}.mp4`);\n});\n\naudios.forEach((file, i) => {\n arrCommands.push(`wget \"${file.url}\" -O ${workDir}/audio${i}.mp3`);\n});\n\nimages.forEach((file, i) => {\n arrCommands.push(`wget \"${file.url}\" -O ${workDir}/image${i}.jpg`);\n});\n\nsubtitles.forEach((file, i) => {\n arrCommands.push(`wget \"${file.url}\" -O ${workDir}/subtitle${i}.srt`);\n});\n\n// ----------------------------------------------------------------\n// 2. CHUYỂN ẢNH THÀNH VIDEO NGẮN (nếu có ảnh)\n// Mỗi ảnh sẽ được chuyển thành một đoạn video có thời lượng 5 giây\n// ----------------------------------------------------------------\nif (images.length > 0) {\n images.forEach((file, i) => {\n // Tạo video từ ảnh với thời lượng 5 giây\n arrCommands.push(`ffmpeg -loop 1 -t 5 -i ${workDir}/image${i}.jpg -c:v libx264 -pix_fmt yuv420p -y ${workDir}/imgclip${i}.mp4`);\n });\n}\n\n// ----------------------------------------------------------------\n// 3. XÂY DỰNG FILE DANH SÁCH (filelist.txt) để nối video\n//\n// - Với các clip gốc: video0.mp4, video1.mp4, …\n// - Với các đoạn video được tạo từ ảnh: imgclip0.mp4, imgclip1.mp4, …\n//\n// Ở đây thứ tự được đặt là: TẤT CẢ các video gốc trước, sau đó là các video từ ảnh.\n// (Bạn có thể thay đổi thứ tự theo yêu cầu của mình)\n// ----------------------------------------------------------------\nlet fileList = \"\";\nvideos.forEach((file, i) => {\n fileList += `file '${workDir}/video${i}.mp4'\\\\n`;\n});\nif (images.length > 0) {\n images.forEach((file, i) => {\n fileList += `file '${workDir}/imgclip${i}.mp4'\\\\n`;\n });\n}\n\n// Sử dụng printf để ghi filelist.txt với ký tự xuống dòng được xử lý đúng\narrCommands.push(`printf \"%b\" \"${fileList}\" > ${workDir}/filelist.txt`);\n\n// ----------------------------------------------------------------\n// 4. NỐI (CONCAT) CÁC ĐOẠN VIDEO lại với nhau thành combined.mp4\n// ----------------------------------------------------------------\narrCommands.push(`ffmpeg -f concat -safe 0 -i ${workDir}/filelist.txt -c copy ${workDir}/combined.mp4`);\n\n// ----------------------------------------------------------------\n// 5. GHÉP AUDIO (nếu có)\n// Giả sử dùng audio0.mp3 làm nhạc nền cho toàn bộ video\n// ----------------------------------------------------------------\nif (audios.length > 0) {\n arrCommands.push(`ffmpeg -i ${workDir}/combined.mp4 -i ${workDir}/audio0.mp3 -c:v copy -c:a aac -shortest ${workDir}/combined_audio.mp4`);\n}\n\n// ----------------------------------------------------------------\n// 6. ÁP DỤNG PHỤ ĐỀ (nếu có)\n// Sử dụng file subtitle0.srt cho toàn bộ video.\n// Chọn đầu vào là video đã ghép audio nếu có, ngược lại dùng combined.mp4\n// ----------------------------------------------------------------\nif (subtitles.length > 0) {\n if (audios.length > 0) {\n arrCommands.push(`ffmpeg -i ${workDir}/combined_audio.mp4 -vf \"subtitles=${workDir}/subtitle0.srt\" -c:v libx264 -c:a copy ${workDir}/output.mp4`);\n } else {\n arrCommands.push(`ffmpeg -i ${workDir}/combined.mp4 -vf \"subtitles=${workDir}/subtitle0.srt\" -c:v libx264 -c:a copy ${workDir}/output.mp4`);\n }\n} else {\n // Nếu không có phụ đề, chỉ cần sử dụng video đã ghép audio (nếu có) hoặc video gốc\n if (audios.length > 0) {\n arrCommands.push(`cp ${workDir}/combined_audio.mp4 ${workDir}/output.mp4`);\n } else {\n arrCommands.push(`cp ${workDir}/combined.mp4 ${workDir}/output.mp4`);\n }\n}\n\n// ----------------------------------------------------------------\n// 7. Ghép tất cả các lệnh lại với nhau, sử dụng '&&' để đảm bảo chạy tuần tự\n// ----------------------------------------------------------------\nconst finalCmd = arrCommands.join(' && ');\n\nreturn [{ command: finalCmd }];\n"
},
"name": "Build FFmpeg Command3",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1240,
580
],
"id": "789e9fd4-b644-4d56-858f-d98a1cbac991"
},
{
"parameters": {
"command": "={{ $json[\"command\"] }}"
},
"name": "Execute FFmpeg Command3",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
-1040,
580
],
"id": "de27e5ce-c037-4163-86cd-c77eee204a34"
},
{
"parameters": {
"fileSelector": "/home/node/.n8n/output.mp4",
"options": {}
},
"type": "n8n-nodes-base.readWriteFile",
"typeVersion": 1,
"position": [
-780,
520
],
"id": "39e759a7-c80d-48d3-b5b9-87fc5aebe22a",
"name": "Read/Write Files from Disk"
},
{
"parameters": {
"command": "base64 /home/node/.n8n/output.mp4"
},
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
-780,
800
],
"id": "f62a3559-f7f6-4952-a897-fa6daaa59a6e",
"name": "Execute Command"
}
],
"connections": {
"Set Binary Data1": {
"main": [
[]
]
},
"Google Drive Get All File3": {
"main": [
[
{
"node": "Aggregate Files2",
"type": "main",
"index": 0
}
]
]
},
"Aggregate Files2": {
"main": [
[
{
"node": "Process Files by Type2",
"type": "main",
"index": 0
}
]
]
},
"Process Files by Type2": {
"main": [
[
{
"node": "Build FFmpeg Command3",
"type": "main",
"index": 0
}
]
]
},
"Build FFmpeg Command3": {
"main": [
[
{
"node": "Execute FFmpeg Command3",
"type": "main",
"index": 0
}
]
]
},
"Execute FFmpeg Command3": {
"main": [
[
{
"node": "Execute Command",
"type": "main",
"index": 0
},
{
"node": "Read/Write Files from Disk",
"type": "main",
"index": 0
},
{
"node": "Set Binary Data1",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "250522b2a43a126dffad96a0033609652bd7a63892ddcb1c067e3e93979795cc"
}
}
Share the output returned by the last node
Information on your n8n setup
- **n8n version:1.77.3
- **Database (default: SQLite): image: postgres:latest
- n8n EXECUTIONS_PROCESS setting (default: own, main):
- **Running n8n via Docker Compose
- **Operating system:Ubuntu 24.04 4CPU 8GB Ram