用 Docker Compose 部署 n8n:持久化、时区、重启策略一次配对

docker run 那条命令我前后存过三份记录。第一份只有端口,第二份加了卷,第三份加了时区和密钥,三行拼起来两百多个字符,改一个参数要整条重建,重建完还得回想刚才到底改了哪。

compose 把这些参数搬进一个文件。文件能对比、能进版本库、能一次看全。代价是改完必须重建容器,光 restart 不读新值——这条我踩过,第二节后面有实测。

这篇给一份十几行的 compose 文件,把三件事一次定死:数据放哪、几点算几点、进程挂了会不会自己回来。顺手记三个我在 2.43.7 上真撞到的坑:只设 TZ 时区还是纽约、改完环境变量 restart 不生效、中途加加密密钥直接把容器打成重启循环。

前提是机器上装了 Docker Engine 和 Compose v2,docker compose version 能出版本号就行。完全没碰过 n8n 的先看安装那篇,Docker 是什么还没搞清的看这篇。

为什么不继续用 docker run

官方 Docker 安装页给的还是 docker run 一行流。那不是错,它面向的场景是"先跑起来看看"。跑通之后你会开始问另外三个问题:数据放哪、时区对不对、机器半夜重启了它回不回来。这三个问题在 docker run 里也能回答,答案散在命令行历史里。

我见过一次真事故:同事换机器,把旧的 docker run 命令抄过去,漏了 -v。容器起来是崭新的,界面让他重新注册,他以为 n8n 坏了,重装了两遍。数据一直在旧机器的卷里躺着,没人去看。

写进文件就没这个空间。少一行,docker compose config 一眼能看出来。

另外一句:别用 latest。n8n 基本每周发一个小版本,latest 哪天变了,你早上起来行为就不一样。镜像标签写死到具体版本,升级自己挑时间。

一份够用的 compose 文件

services:
  n8n:
    image: n8nio/n8n:2.43.7
    container_name: n8n
    restart: unless-stopped
    ports:
      - "127.0.0.1:5678:5678"
    environment:
      - TZ=Asia/Shanghai
      - GENERIC_TIMEZONE=Asia/Shanghai
      - N8N_ENCRYPTION_KEY=换成openssl生成的48位串
    volumes:
      - n8n-data:/home/node/.n8n

volumes:
  n8n-data:

文件名用 compose.yaml,或者 docker-compose.yml,Compose v2 两个都认。文件放哪就在哪执行命令,换个目录敲会报错,原文在下面"常见错误"里。

密钥那行先别看懵,一条命令生成:

openssl rand -hex 24

出来 48 个字符,粘进去。这一行是整份文件里最不能随手改的,第六节讲为什么。

逐行说几个真会踩的:

  • ports 前面那个 127.0.0.1: 不是装饰。只写 5678:5678 等于把面板摊在公网所有网卡上,后面接反代再放开。
  • /home/node/.n8n 是镜像里的固定路径,别改成 /root/.n8n。容器里跑的是 uid 1000 的 node 用户,改路径多半是权限问题。
  • volumes 用命名卷 n8n-data,不用 ./data:/home/node/.n8n 这种绑定挂载。绑定挂载更好找文件,但目录属主是 root 时容器起不来,得先 chown 1000:1000。
  • restart 写 unless-stopped,理由在第五节。

第一次启动,什么时候才算真好了

docker compose up -d
docker compose ps

第一次要拉镜像,2.43.7 解压后占 1.8G。镜像本来就在机器上的话,十几秒就 Up。

但 ps 显示 Up 不代表能用。n8n 首次启动要迁移数据库、生成密钥文件,这段时间端口已经监听,接口还返回空。我这次掐着表看:12 秒时 docker compose ps 已经是 Up,/rest/settings 什么都没有;20 秒左右才返回 JSON。日志里出现这一行才算真好了:

Editor is now accessible via:
http://localhost:5678

想看进度就 docker compose logs -f n8n。第一次启动还会打一行 No encryption key found - Auto-generating and saving to: /home/node/.n8n/config,说明密钥是它自己生成的——如果你已经在 compose 里写了密钥,这行就不该出现,出现了说明环境变量没生效。

Docker Compose 起的 n8n 画布,定时触发节点接着整理字段节点
容器里的实例,节点、连线、执行按钮都正常。这篇要验的三件事,都在这个环境里复现过

界面是英文的,跟部署方式无关,是账号里的语言设置;站点上那台是中文,因为它当初注册时选过。

TZ 和 GENERIC_TIMEZONE 不是一回事

两个变量名字长得像,管的东西完全不同。

TZ 管容器这个操作系统的时区,date、日志时间戳、系统里跑的脚本都看它。GENERIC_TIMEZONE 是 n8n 自己的实例时区,定时类节点按它排班,文档写的默认值是 America/New_York。

我第一次只写了 TZ,觉得时区这事就完了。结果是这样:

docker exec n8n date
Mon Nov  9 09:25:33 CST 2026

系统时钟没问题。但登录进去看工作流设置,时区那栏写着 Default - America/New_York。也就是说,画布上写"每天九点"的任务,会按纽约时间九点跑,跟你差 13 个小时。全程不报错,日志也不吭声。

补上 GENERIC_TIMEZONE=Asia/Shanghai,重建容器,那栏变成 Default - Asia/Shanghai。

n8n 工作流设置弹窗,时区一栏显示 Default - Asia/Shanghai
工作流菜单最下面一项是设置。第三行那个 Default 后面跟的就是实例时区,不是工作流自己设的

想更直接地确认时区生效,跑一次带时间的工作流。我在"整理字段"节点里放了个 $now.toFormat('yyyy-MM-dd HH:mm'),手动执行,输出是本地时间:

手动执行后整理字段节点的输出,env 为 compose,run_at 是本地时间
右侧 OUTPUT 那栏的 run_at 是 09:37,跟当时墙上的钟对得上。设错时区的话这里会是前一天晚上

两个变量都写,别省。只写 GENERIC_TIMEZONE 的话,调度对了,但容器里 date 还是 UTC,看日志又得自己换算。

卷里到底装了什么

进容器看一眼 /home/node/.n8n:

docker compose exec n8n ls -la /home/node/.n8n

里面五个东西有用:config(56 字节,只有一行密钥)、database.sqlite 加两个 WAL 文件(工作流、凭据、执行记录都在这)、nodes(装的社区节点)、storage(二进制数据,比如节点里存的图片)。

所以判断一个实例是不是真持久化,只看一件事:down 之后再 up,工作流还在不在。

docker compose down
docker compose up -d

down 删容器和网络,卷留着。我这么折腾了一遍,列表里两条工作流原封不动,连创建时间都没变。

compose down 再 up 之后的工作流列表,两条工作流都还在
down 完再 up,容器是新的,数据是旧的。这就是命名卷在干活

down -v 是另一回事,-v 连卷一起删。我为了拍下面这张图也跑了一遍:容器重新起来,界面回到首次设置账号那页,账号、工作流、执行记录一起消失,只剩一个空的 SQLite。

命名卷被删掉后重启容器,界面回到首次设置的账号页
同一个 compose 文件、同一个镜像,卷没了就是全新安装。这条命令不要在手滑的时候敲

备份也一样:光备 database.sqlite 没用,密钥在 config 里。两个一起,才叫备了。

重启策略:什么情况它会自己回来

restart: unless-stopped 的行为分三段:进程自己退出,会拉起;你手动停过,它就一直停着,连宿主机重启都不理;always 的区别是手动停过的,守护进程重启后会再拉起来。

我按三种情况试了一遍,结论跟直觉有点出入。

第一种,从外面 docker kill:

docker kill n8n
docker ps -a --filter name=n8n

容器 Exited (137),五秒后再看还是 exited,重启次数 0。Docker 把 stop 和 kill 当成"你主动停的",策略直接不生效,官方文档里写的是防止陷入重启死循环。

第二种,进容器把 n8n 主进程杀掉。先看进程:

docker compose exec n8n ps -o pid,args

第一行是 tini,PID 1;第二行 n8n 才是真正干活的,PID 每次不一样。对着它 kill -9,两秒后容器自己回来了,重启次数变成 1。这才叫验证重启策略。

第三种,docker compose stop。停完它就一直躺着,得 start 或者 up -d 叫醒。

查重启次数:

docker inspect n8n | grep -E '"RestartCount"|"Status"'

会打出两三行,其中一行是 "RestartCount": 1。

最后一句提醒:配置写错的时候,重启策略会把它反复拉起来。docker compose ps 里状态显示 Up 3 seconds,过一会再看还是 Up 3 seconds——那不是健康,是崩溃循环。下一节就是这个现场。

加密密钥,我在这一步把实例弄崩过

事情是这样的:实例已经跑了一天,里面有工作流也有凭据。我想着密钥该写进 compose,方便以后迁移,就往 environment 里加了一行 N8N_ENCRYPTION_KEY=,值是新生成的另一串。

up -d 之后容器起不来了,日志每几秒重复这一段:

Error: Mismatching encryption keys. The encryption key in the settings file /home/node/.n8n/config does not match the N8N_ENCRYPTION_KEY env var. Please make sure both keys match.
Error: Failed to load command "start"

原因不复杂:密钥已经在 config 文件里了,现在环境变量又给了一个不同的值,n8n 不知道该信哪个,直接不启动。

两个解法,看你的实例处在哪一步。

已经在跑的:把环境变量改成文件里那个值,别反过来。

docker compose exec n8n cat /home/node/.n8n/config

里面就一行 encryptionKey,抄进 compose,再 up -d,起来了,凭据也还能解开。

全新实例:从第一天就把密钥写死,先写再启动。我实测过顺序——新卷加环境变量密钥,首次启动写进 config 的就是环境变量那个值,两边一致,之后不会再撞这个错。

为什么非要管这个密钥:n8n 拿它加密数据库里的凭据。密钥对不上,凭据就是一堆解不开的字符串。删卷重来能绕过去,但工作流、执行记录、凭据一起没了。

密钥跟着卷走,卷没了密钥也没了。这也解释了为什么"换了台机器重新部署"最容易出事:镜像一样、compose 一样,密钥不一样,导过来的数据库打不开凭据。

五条自检

  1. 容器在跑:docker compose ps 显示 Up,不是 Restarting。
  2. 接口通了:curl -s http://127.0.0.1:5678/rest/settings | head -c 200,能看到 JSON。返回空说明还在启动。
  3. 时区对了:工作流菜单 → 设置,Timezone 那栏是 Default - Asia/Shanghai。
  4. 能跑:随便建个工作流,手动执行一次,输出里的时间是本地时间。
  5. 真持久化:docker compose down 再 up -d,工作流还在。

五条都过,这套东西才算立住。少一条,下次出问题你就得从头猜。

几个会遇到的报错

Mismatching encryption keys

上一节那个。改 env 之前先看 config 里是什么,别硬删文件——删了密钥,凭据全废,而且没有任何提示,直到你打开某个凭据节点才知道。

Error: Failed to load command "start"

这行本身没信息量,它是崩溃的收尾。真正的原因在它上面几行,可能是密钥不匹配,也可能是权限。

用绑定挂载 ./data:/home/node/.n8n 的时候常见的是权限:

Error: EACCES: permission denied, open '/home/node/.n8n/config'

宿主机上新建的目录属主是 root,容器里跑的是 uid 1000。修法是 chown -R 1000:1000 ./data。嫌麻烦就用命名卷,权限 Docker 自己管。

failed to bind host port 0.0.0.0:5678/tcp: address already in use

端口被占了。这台机器上可能还跑着别的东西,或者你之前那个 docker run 起的容器没删。查:

ss -ltnp 'sport = :5678'
docker ps -a

要么停掉占用的,要么改 compose 前半段那个宿主端口。

no configuration file provided: not found

不在 compose 文件所在目录执行。cd 过去再敲,或者用 docker compose -f /路径/compose.yaml up -d。

改了环境变量,restart 之后没变化

没有报错,就是没生效。我实测过:改完 GENERIC_TIMEZONE 跑 docker compose restart,进容器 printenv GENERIC_TIMEZONE 是空的,时区还是纽约。restart 只是把进程重启,不重建容器,环境变量是创建容器时定死的。

要生效只能用 docker compose up -d,它会发现配置变了,重建容器。看到输出里写着 Recreated 才算改到了。

容器里时间对,定时还是差 13 小时

只看 TZ 没看 GENERIC_TIMEZONE,第四节那个坑。

安全提醒

这三条只跟本篇的部署方式有关。

  • ports 保持 127.0.0.1:5678:5678。要公网访问就加反代和证书,别图省事改成 5678:5678。n8n 面板里存的是各种第三方凭据,被人登进去等于全部泄露。
  • compose 文件现在含加密密钥,权限收一下 chmod 600,也不要提交到公开仓库。要进 git 就把密钥挪进 .env 并忽略它。
  • docker compose down -v 会连数据一起删。做升级、迁移这类动作之前,先把卷备份出来,别在生产实例上手滑。

小结

compose 不长,真正要定死的就三样:卷挂 /home/node/.n8n、时区两个变量都写、restart 给 unless-stopped。这三样对了,剩下的都是细节。

密钥那行建议从第一次启动就写上,事后再加,大概率撞 Mismatching encryption keys,而且是在你已经存了凭据之后才发现。

改配置只认 up -d,restart 不读新环境变量。这条不记住,会浪费你半小时去怀疑时区数据库。

接着往下

容器跑起来只是开头,四个基础词(工作流、节点、连线、执行)搞不清,后面全是猜:Workflow、Node、Connection、Execution:四个词讲清 n8n 的世界。上一篇是 Docker 是什么:给 n8n 新手的最小必要知识,整套教程按阶段排在课程目录里。

参考资料

最后更新:2026 年 11 月 9 日,用的是 n8n 2.43.7。

THE END