Azimutt Docker Compose 部署指南

用 Docker Compose 部署 Azimutt 2.0 三件套(应用 main 构建 + PostgreSQL 16 元数据库 + @azimutt/gateway 0.1.26)全流程:compose 全文、demo 库初始化、启动与登录/gateway schema/query 验证、等价 docker run、升级回滚与 403/400/404 等常见故障处理(2026-11-03 实测)。

AzimuttAzimuttDocker ComposeDockerPostgreSQL自托管2026/11/03

Azimutt Docker Compose 部署指南

目标:从零用 Docker Compose 部署 Azimutt 2.0(应用 + PostgreSQL 16 元数据库 + Node.js gateway 三件套),跑通登录、项目 API 与 gateway schema/query 全链路,并给出等价 docker run、升级回滚与常见故障处理。

一、环境信息

  • 宿主:Ubuntu 26.04.1 LTS x86_64,IP 192.168.1.200
  • Docker:29.9.2 (build a82e528);Docker Compose:v5.6.1
  • 部署组件:Azimutt main(release 构建号 2.0.1787693602,OTP 25 / Elixir 1.14.3 / ERTS 13.1.4)、gateway @azimutt/gateway@0.1.26、元数据库 postgres:16
  • 对外端口:应用 192.168.1.200:8136(容器内 4000)、gateway 192.168.1.200:8137(容器内 3000)、PostgreSQL 192.168.1.200:5432
  • 专用网络:azimutt-main-net(bridge,固定子网 10.89.61.0/24,db/server/gateway 分别固定 10.89.61.10/20/30)
  • 部署目录:/opt/azimutt-stack(compose.yaml、.env、init/ 与 data/ 都在这里)

二、镜像选择说明

用途镜像选择理由(2026-11-03 实测)
Azimutt 应用ghcr.io/azimuttapp/azimutt:main官方唯一滚动发布渠道。GitHub 无 Releases,ghcr 仓库只有 main 与一个特性分支 tag,没有版本化 tag 可选;main 即最新构建,真实版本以 release 构建号标识(/app/bin/azimutt version → azimutt 2.0.1787693602)
gatewaynode:20-alpine + npm 全局安装 @azimutt/gateway@0.1.26gateway 无官方镜像,官方口径就是用 Node.js 运行 npm 包;启动命令里固定包版本 0.1.26 以保证可复现
元数据库postgres:16Azimutt 后端为 Elixir/Phoenix,元数据必须存 PostgreSQL;16 为当前稳定大版本

⚠️ main 是滚动 tag,今天与明天的 main 内容可能不同。生产部署务必用 docker inspect 记录当前 digest(见「十、常用运维命令」),升级/回滚都以 digest 为准。

三、前置准备

$ docker --version
Docker version 29.9.2, build a82e528
$ docker compose version
Docker Compose version v5.6.1

端口规划:

端口用途暴露给谁
8136Azimutt Web(容器内 4000)浏览器
8137gateway(容器内 3000)浏览器(前端直连)与 CLI
5432PostgreSQL(容器内 5432)gateway 访问业务库/元数据库;测试环境临时对宿主开放

生成应用加密密钥(应至少 64 字节随机值):

$ openssl rand -hex 32
cd0ba5b8f7f85b8b0a140a25af22106b5aebf37cc39206a67559877eeb649afb

四、部署步骤

4.1 目录与 .env

/opt/azimutt-stack/
├── compose.yaml
├── .env
├── init/
│   ├── 01-demo-db.sh            # 建 demo 库
│   └── demo/02-demo-schema.sql  # 示例业务 schema(6 表 31 列 7 外键)
└── data/
    ├── db/      # PostgreSQL 数据目录(持久化)
    └── uploads/ # Azimutt local 文件存储(持久化)

.env 只需一个变量(compose 用 ${SECRET_KEY_BASE} 插值):

$ cat .env
SECRET_KEY_BASE=08383932639413583b2343a17290e77c8dca6026d3abc6a032fbb09e0be930c3183a1b7142bbfdf5

4.2 compose.yaml(全文)

name: azimutt-main-stack

services:
  azimutt-db:
    image: postgres:16
    container_name: azimutt-main-db
    hostname: azimutt-main-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: azimutt
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: q1w2e3r4
    ports:
      - "192.168.1.200:5432:5432"
    volumes:
      - ./data/db:/var/lib/postgresql/data
      - ./init:/docker-entrypoint-initdb.d:ro
    networks:
      azimutt-net:
        ipv4_address: 10.89.61.10
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d azimutt"]
      interval: 5s
      timeout: 5s
      retries: 12

  azimutt:
    image: ghcr.io/azimuttapp/azimutt:main
    container_name: azimutt-main-server
    hostname: azimutt-main-server
    restart: unless-stopped
    depends_on:
      azimutt-db:
        condition: service_healthy
    ports:
      - "192.168.1.200:8136:4000"
    environment:
      PHX_SERVER: "true"
      PHX_PROTOCOL: ipv4
      PHX_HOST: localhost
      PORT: "4000"
      SECRET_KEY_BASE: ${SECRET_KEY_BASE}
      DATABASE_URL: postgresql://postgres:q1w2e3r4@azimutt-main-db:5432/azimutt
      DATABASE_POOL_SIZE: "10"
      GATEWAY_URL: http://192.168.1.200:8137
      FILE_STORAGE_ADAPTER: local
      AUTH_PASSWORD: "true"
      SKIP_ONBOARDING_FUNNEL: "true"
      SKIP_EMAIL_CONFIRMATION: "true"
      PUBLIC_SITE: "false"
    volumes:
      - ./data/uploads:/app/bin/uploads
    networks:
      azimutt-net:
        ipv4_address: 10.89.61.20

  gateway:
    image: node:20-alpine
    container_name: azimutt-main-gateway
    hostname: azimutt-main-gateway
    restart: unless-stopped
    command: sh -c "npm install -g @azimutt/gateway@0.1.26 --no-audit --no-fund && node /usr/local/lib/node_modules/@azimutt/gateway/out/start.js"
    ports:
      - "192.168.1.200:8137:3000"
    environment:
      NODE_ENV: production
      LOG_LEVEL: info
      API_HOST: 0.0.0.0
      API_PORT: "3000"
      CORS_ALLOW_ORIGIN: http://192.168.1.200:8136
    networks:
      azimutt-net:
        ipv4_address: 10.89.61.30

networks:
  azimutt-net:
    name: azimutt-main-net
    driver: bridge
    ipam:
      config:
        - subnet: 10.89.61.0/24

三处拓扑要点:

  1. DATABASE_URL 指向元数据库 azimutt-main-db:5432/azimutt(应用自身账号/项目/注释存这里);
  2. GATEWAY_URL 必须是浏览器可达的地址(这里是宿主 192.168.1.200:8137),它会被注入前端页面 window.gateway_url,浏览器随后直连 gateway——写内部域名则浏览器解析不到;
  3. gateway 的 CORS_ALLOW_ORIGIN 要与浏览器访问应用的地址完全一致(http://192.168.1.200:8136),否则跨域被拒。

4.3 demo 业务库初始化脚本(可选)

init/ 挂载进 postgres 的 /docker-entrypoint-initdb.d,仅首次初始化(数据目录为空)时执行,用来造一个供 Azimutt 演示的 demo 库。

init/01-demo-db.sh:

#!/bin/bash
set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
    CREATE DATABASE demo;
    GRANT ALL PRIVILEGES ON DATABASE demo TO $POSTGRES_USER;
EOSQL
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname demo -f /docker-entrypoint-initdb.d/demo/02-demo-schema.sql

init/demo/02-demo-schema.sql(6 表:categories/users/products/orders/order_items/reviews,7 个外键):

CREATE TABLE categories (
    id bigserial PRIMARY KEY,
    name varchar(100) NOT NULL UNIQUE,
    slug varchar(120) NOT NULL UNIQUE,
    created_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE users (
    id bigserial PRIMARY KEY,
    email varchar(255) NOT NULL UNIQUE,
    full_name varchar(200) NOT NULL,
    category_id bigint REFERENCES categories (id),
    created_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE products (
    id bigserial PRIMARY KEY,
    sku varchar(40) NOT NULL UNIQUE,
    title varchar(200) NOT NULL,
    price numeric(10, 2) NOT NULL CHECK (price >= 0),
    category_id bigint NOT NULL REFERENCES categories (id),
    created_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE orders (
    id bigserial PRIMARY KEY,
    user_id bigint NOT NULL REFERENCES users (id),
    status varchar(20) NOT NULL DEFAULT 'pending',
    total numeric(12, 2) NOT NULL DEFAULT 0,
    created_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE order_items (
    id bigserial PRIMARY KEY,
    order_id bigint NOT NULL REFERENCES orders (id),
    product_id bigint NOT NULL REFERENCES products (id),
    quantity integer NOT NULL CHECK (quantity > 0),
    unit_price numeric(10, 2) NOT NULL
);

CREATE TABLE reviews (
    id bigserial PRIMARY KEY,
    product_id bigint NOT NULL REFERENCES products (id),
    user_id bigint NOT NULL REFERENCES users (id),
    rating smallint NOT NULL CHECK (rating BETWEEN 1 AND 5),
    body text,
    created_at timestamptz NOT NULL DEFAULT now()
);

INSERT INTO categories (name, slug) VALUES
    ('Books', 'books'), ('Electronics', 'electronics'), ('Home', 'home');
INSERT INTO users (email, full_name, category_id) VALUES
    ('alice@example.com', 'Alice', 1), ('bob@example.com', 'Bob', 2);
INSERT INTO products (sku, title, price, category_id) VALUES
    ('B-001', 'The Elixir Book', 39.90, 1),
    ('E-001', 'USB-C Hub', 25.00, 2);
INSERT INTO orders (user_id, status, total) VALUES (1, 'paid', 64.90);
INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES
    (1, 1, 1, 39.90), (1, 2, 1, 25.00);
INSERT INTO reviews (product_id, user_id, rating, body) VALUES
    (1, 2, 5, 'Great book');

4.4 启动

$ cd /opt/azimutt-stack
$ docker compose up -d
 Network azimutt-main-net Creating 
 Network azimutt-main-net Creating 
 Network azimutt-main-net Created 
 Network azimutt-main-net Created 
 Container azimutt-main-gateway Creating 
 Container azimutt-main-db Creating 
 Container azimutt-main-db Created 
 Container azimutt-main-server Creating 
 Container azimutt-main-gateway Created 
 Container azimutt-main-server Created 
 Container azimutt-main-db Starting 
 Container azimutt-main-gateway Starting 
 Container azimutt-main-db Started 
 Container azimutt-main-db Waiting 
 Container azimutt-main-gateway Started 
 Container azimutt-main-db Healthy 
 Container azimutt-main-server Starting 
 Container azimutt-main-server Started 

(Network 行重复出现是 Compose v5.6.1 的实际输出,未删减。)depends_on: condition: service_healthy 保证 server 在 db 健康后才启动。应用启动日志(docker logs azimutt-main-server):

Setup Local file storage
Setup Local email provider
Setup Password auth
02:32:49.317 [info] Migrations already up
Setup Local file storage
Setup Local email provider
Setup Password auth
02:32:50.078 [info] Running AzimuttWeb.Endpoint with cowboy 2.12.0 at 0.0.0.0:4000 (http)
02:32:50.080 [info] Access AzimuttWeb.Endpoint at https://localhost

Setup 行是 runtime.exs 按环境变量装配文件存储/邮件/认证的运行痕迹(镜像入口先跑迁移再启动,故出现两次)。gateway 首次启动需 npm 安装约 1 分钟,就绪日志:

{"level":30,"time":1793672876496,"pid":1,"hostname":"azimutt-main-gateway","msg":"Server listening at http://0.0.0.0:3000"}

4.5 部署验证

$ docker compose ps
NAME                   IMAGE                             COMMAND                  SERVICE      CREATED          STATUS                    PORTS
azimutt-main-db        postgres:16                       "docker-entrypoint.s…"   azimutt-db   51 seconds ago   Up 50 seconds (healthy)   192.168.1.200:5432->5432/tcp
azimutt-main-gateway   node:20-alpine                    "docker-entrypoint.s…"   gateway      51 seconds ago   Up 50 seconds             192.168.1.200:8137->3000/tcp
azimutt-main-server    ghcr.io/azimuttapp/azimutt:main   "sh -c '/app/bin/mig…"   azimutt      51 seconds ago   Up 45 seconds             192.168.1.200:8136->4000/tcp

应用入口状态码(PUBLIC_SITE=false 使首页重定向登录页):

$ curl -s -o /dev/null -w 'root=%{http_code} redirect=%{redirect_url}\n' http://192.168.1.200:8136/
root=302 redirect=http://192.168.1.200:8136/login
$ curl -s -o /dev/null -w 'register=%{http_code}\n' http://192.168.1.200:8136/register
register=200
$ curl -s -o /dev/null -w 'health=%{http_code}\n' http://192.168.1.200:8136/health
health=200

登录(先取 CSRF token 与会话 cookie,再 POST;会话 cookie 带 Secure; SameSite=None,http 下 curl 的 cookie jar 不落盘,需手动拼 Cookie 头):

$ curl -s -D get.hdr -o login.html http://192.168.1.200:8136/login
$ TOK=$(grep -oE 'name="_csrf_token"[^>]*value="[^"]+"' login.html | sed -E 's/.*value="([^"]+)".*/\1/' | head -1)
$ CK=$(grep -i 'set-cookie: _azimutt_key' get.hdr | sed -E 's/.*_azimutt_key=([^;]+).*/_azimutt_key=\1/')
$ curl -s -H "Cookie: $CK" -D login2.hdr -o /dev/null -X POST http://192.168.1.200:8136/login \
  --data-urlencode _utf8=✓ --data-urlencode "_csrf_token=$TOK" \
  --data-urlencode 'user[email]=admin@azimutt.local' \
  --data-urlencode 'user[password]=q1w2e3r4q1w2'
$ grep -iE '^(HTTP|location)' login2.hdr
HTTP/1.1 302 Found
location: /home

登录后用新会话 cookie 调项目 API:

$ SCK=$(grep -i 'set-cookie: _azimutt_key' login2.hdr | tail -1 | sed -E 's/.*_azimutt_key=([^;]+).*/_azimutt_key=\1/')
$ curl -s -H "Cookie: $SCK" http://192.168.1.200:8136/api/v1/organizations
[{"description":null,"id":"32170407-49ef-4e66-ab0f-44214d50eaf3","name":"admin","slug":"admin"}]
$ curl -s -H "Cookie: $SCK" http://192.168.1.200:8136/api/v1/organizations/32170407-49ef-4e66-ab0f-44214d50eaf3/projects
[{"name":"demo-shop","id":"4d40deb4-3df6-48d9-be54-76636daf6c65","slug":"demo-shop","organization_id":"32170407-49ef-4e66-ab0f-44214d50eaf3","storage_kind":"local","nb_sources":1,"nb_tables":6,"nb_columns":31,"nb_relations":7,"nb_types":0,"nb_comments":0,"nb_layouts":0,"nb_notes":0,"nb_memos":0,"created_at":"2026-11-03T01:55:54Z","updated_at":"2026-11-03T01:55:54Z","imported_at":"2026-11-03T01:55:54Z","visibility":"private","default_source":null,"archived":false}]

gateway 三验(健康、schema、query——注意参数名是 url):

$ curl -s http://192.168.1.200:8137/health
{"status":200,"version":"x.y.z"}
$ curl -s -X POST http://192.168.1.200:8137/gateway/schema -H "Content-Type: application/json" \
  -d '{"url":"postgresql://postgres:q1w2e3r4@192.168.1.200:5432/demo"}' | head -c 200
{"tables":[{"schema":"public","table":"categories","columns":[{"name":"id","type":"bigint","default":"nextval('categories_id_seq'::regclass)"},{"name":"name","type":"character varying(100)"},{"name":"
$ curl -s -X POST http://192.168.1.200:8137/gateway/schema -H "Content-Type: application/json" \
  -d '{"url":"postgresql://postgres:q1w2e3r4@192.168.1.200:5432/demo"}' | python3 -c 'import json,sys; d=json.load(sys.stdin); print("tables:",len(d["tables"]),"relations:",len(d["relations"]),"types:",len(d["types"]))'
tables: 6 relations: 7 types: 0
$ curl -s -X POST http://192.168.1.200:8137/gateway/query -H "Content-Type: application/json" \
  -d '{"url":"postgresql://postgres:q1w2e3r4@192.168.1.200:5432/demo","query":"select id, sku, title, price from products order by id limit 5"}'
{"query":"select id, sku, title, price from products order by id limit 5","columns":[{"name":"id","ref":{"table":"public.products","column":"id"}},{"name":"sku","ref":{"table":"public.products","column":"sku"}},{"name":"title","ref":{"table":"public.products","column":"title"}},{"name":"price","ref":{"table":"public.products","column":"price"}}],"rows":[{"id":1,"sku":"B-001","title":"The Elixir Book","price":"39.90"},{"id":2,"sku":"E-001","title":"USB-C Hub","price":"25.00"}]}

五、数据持久化说明

挂载宿主路径作用
./data/db:/var/lib/postgresql/data/opt/azimutt-stack/data/db元数据库(账号/组织/项目)+ demo 业务库
./data/uploads:/app/bin/uploads/opt/azimutt-stack/data/uploadsFILE_STORAGE_ADAPTER=local 时 remote 项目的 schema JSON 落盘目录
./init:/docker-entrypoint-initdb.d:ro/opt/azimutt-stack/init仅首次初始化执行
$ ls -l data/
total 8
drwx------ 19 dnsmasq root 4096 Nov  3 10:26 db
drwxr-xr-x  2 root    root 4096 Nov  3 06:56 uploads
$ docker exec azimutt-main-db psql -U postgres -d demo -t -c "select count(*) from information_schema.columns where table_schema='public'"
    31
$ docker exec azimutt-main-db psql -U postgres -d azimutt -t -c "select count(*) from users"
     1

注意:storage_kind=local 的项目 schema 草稿存在浏览器端,服务端 uploads 目录为空属正常;只有 remote 项目才落文件。备份与恢复不在本指南范围,以元数据库整库备份为主即可。

六、环境变量表

应用(azimutt 服务):

变量本例取值作用
PHX_SERVERtrue以服务器模式启动 Phoenix(必需,否则不监听)
PHX_PROTOCOLipv4监听 IPv4(默认 ipv6,bridge 网络下必须改)
PHX_HOSTlocalhost构建绝对 URL 的 host(生产填域名)
PORT4000应用监听端口
SECRET_KEY_BASE.env 注入会话/cookie 加密密钥,≥64 字节随机值
DATABASE_URLpostgresql://postgres:q1w2e3r4@azimutt-main-db:5432/azimutt元数据库连接串
DATABASE_POOL_SIZE10连接池大小(默认即 10)
GATEWAY_URLhttp://192.168.1.200:8137注入前端 window.gateway_url,须浏览器可达
FILE_STORAGE_ADAPTERlocal文件存储驱动(local/s3)
AUTH_PASSWORDtrue启用邮箱+密码认证
SKIP_ONBOARDING_FUNNELtrue跳过新用户引导流程
SKIP_EMAIL_CONFIRMATIONtrue跳过邮箱验证(无邮件服务时必开)
PUBLIC_SITEfalse关闭官网页,/ 直接 302 到 /login

gateway(gateway 服务,取自官方 gateway.env):

变量本例取值作用
NODE_ENVproduction运行环境
LOG_LEVELinfopino 日志级别
API_HOST0.0.0.0监听地址
API_PORT3000监听端口
CORS_ALLOW_ORIGINhttp://192.168.1.200:8136允许的应用来源,必须与浏览器访问地址一致

七、配置文件挂载说明

Azimutt 无独立配置文件可挂:全部配置经环境变量注入,config/runtime.exs 在每次启动(以及 eval/rpc 等命令执行)时读取。实测佐证:执行任何 release 命令都会先打印装配日志——

Setup Local file storage
Setup Local email provider
Setup Password auth

因此改配置的唯一动作是:改 .env/compose environment → docker compose up -d 重建生效。唯一需要挂载的「配置」是 init/ 脚本(首次初始化 demo 库)与 data/(持久化)。

八、等价 docker run

不用 compose 也可逐个拉起(已实测;azimutt-main-net 网络与 azimutt-main-db 数据库沿用 compose 建好的)。应用实例(8138 为验证端口,实测通过后即删):

$ docker run -d --name azimutt-run --network azimutt-main-net \
  -e PHX_SERVER=true -e PHX_PROTOCOL=ipv4 -e PHX_HOST=localhost -e PORT=4000 \
  -e SECRET_KEY_BASE=08383932639413583b2343a17290e77c8dca6026d3abc6a032fbb09e0be930c3183a1b7142bbfdf5 \
  -e DATABASE_URL=postgresql://postgres:q1w2e3r4@azimutt-main-db:5432/azimutt \
  -e DATABASE_POOL_SIZE=10 -e GATEWAY_URL=http://192.168.1.200:8137 \
  -e FILE_STORAGE_ADAPTER=local -e AUTH_PASSWORD=true \
  -e SKIP_ONBOARDING_FUNNEL=true -e SKIP_EMAIL_CONFIRMATION=true -e PUBLIC_SITE=false \
  -v /opt/azimutt-stack/data/uploads:/app/bin/uploads \
  -p 192.168.1.200:8138:4000 ghcr.io/azimuttapp/azimutt:main
94e999a1e39b6d9fc3163689609f5300d6405d5eef94cba68c328dab078c59be
$ curl -s -o /dev/null -w 'run-health=%{http_code}\n' http://192.168.1.200:8138/health
run-health=200
$ docker exec azimutt-run /app/bin/azimutt pid
65
$ docker exec azimutt-run /app/bin/azimutt version
azimutt 2.0.1787693602
$ docker exec azimutt-run /app/bin/azimutt stop
$ curl -s -m 5 -o /dev/null -w 'after-stop=%{http_code}\n' http://192.168.1.200:8138/health
after-stop=000
$ docker rm -f azimutt-run

stop 无输出即成功(Erlang release 标准行为),随后端口即失联。gateway:

$ docker run -d --name azimutt-gw-run \
  -e NODE_ENV=production -e LOG_LEVEL=info -e API_HOST=0.0.0.0 -e API_PORT=3000 \
  -e CORS_ALLOW_ORIGIN=http://192.168.1.200:8136 \
  -p 192.168.1.200:8139:3000 node:20-alpine \
  sh -c "npm install -g @azimutt/gateway@0.1.26 --no-audit --no-fund && node /usr/local/lib/node_modules/@azimutt/gateway/out/start.js"
e1e2be576b7e4455b579553edd4b1470058560f1b667032d431bb21541cb3893
$ curl -s http://192.168.1.200:8139/health
{"status":200,"version":"x.y.z"}
$ docker logs azimutt-gw-run 2>&1 | grep 'Server listening'
{"level":30,"time":1793672245067,"pid":1,"hostname":"e1e2be576b7e","msg":"Server listening at http://0.0.0.0:3000"}
$ docker rm -f azimutt-gw-run

九、常用运维命令

日常巡检以 docker compose ps(见 4.5)与 docker compose logs azimutt 为主。核对当前运行的 release 构建号:

$ docker exec azimutt-main-server /app/bin/azimutt version
azimutt 2.0.1787693602

升级(main 滚动 tag):

$ docker compose pull
 Image node:20-alpine Pulling 
 Image postgres:16 Pulling 
 Image ghcr.io/azimuttapp/azimutt:main Pulling 
 Image node:20-alpine Pulled 
 Image postgres:16 Pulled 
 Image ghcr.io/azimuttapp/azimutt:main Pulled 
$ docker compose up -d

升级后务必 docker exec azimutt-main-server /app/bin/azimutt version 核对构建号是否变化。

回滚:官方无版本化 tag,回滚只能按 digest 钉住旧构建。每次部署后记录当前 digest:

$ docker inspect --format "{{json .RepoDigests}}" ghcr.io/azimuttapp/azimutt:main
["ghcr.io/azimuttapp/azimutt@sha256:ecb0ca8b51ed397cf5e8500de748c3d1490cdf1f9773777be8f769f4b9f1ae99"]

需要回滚时把 compose 里的 image: 改成该 @sha256:... 摘要再 docker compose up -d。

十、常见问题

10.1 登录返回 403「Oops, not this time. Try harder later」

POST /login 未携带有效会话 cookie 或 CSRF token 时,Phoenix 防伪造拦截渲染 403(实测空 token 提交:HTTP/1.1 403 Forbidden)。会话 cookie _azimutt_key 带 Secure; HttpOnly; SameSite=None,curl 在 http 下不会自动收发——cookie jar 直接不落盘(实测 jar.txt 只有注释头)。解决:从响应头手动提取 cookie 值并用 -H "Cookie: _azimutt_key=..." 提交(见 4.5 登录流程)。

10.2 浏览器 http 访问能打开页面但登录后仍像未登录

同为 Secure; SameSite=None cookie 惹的祸:现代浏览器拒收 http 连接下发的 Secure cookie。生产应上 TLS 反向代理并设 PHX_PROTOCOL=https、PHX_HOST=<域名>(日志 Access AzimuttWeb.Endpoint at https://localhost 即 host 拼绝对 URL 的体现)。

10.3 密码设 8 位登录报「Email or Password invalid.」

Azimutt 强制密码 12~72 位。实测 8 位口令 q1w2e3r4 登录返回 200 且页面含:

<p class="alert alert-danger">Email or Password invalid.</p>

示例账号因此用 admin@azimutt.local / q1w2e3r4q1w2(12 位)。

10.4 gateway 返回 400 Bad Request

请求体参数名必须是 url,写成 connection 即 400(实测 badparam=400,响应体 {"error":"Bad Request"})。

10.5 gateway 返回 404

0.1.26 的路由是 /gateway/schema、/gateway/query、/gateway/column-stats、/gateway/table-stats、/gateway/parse-url、/health、/ping、/status;旧文档的 /net/inspect 已不存在(实测 oldroute=404,日志 Route GET:/net/inspect not found)。

10.6 gateway 刚启动 curl 不通

gateway 启动命令先 npm install -g(约 1 分钟),就绪前请求挂起返回 000。以日志出现 Server listening at http://0.0.0.0:3000 为准。

10.7 前端连不上 gateway(CORS 或地址不可达)

GATEWAY_URL 注入的 window.gateway_url 由浏览器直连($SCK 为 4.5 中提取的会话 cookie):

$ curl -s -H "Cookie: $SCK" http://192.168.1.200:8136/projects/4d40deb4-3df6-48d9-be54-76636daf6c65 | grep -o "window.gateway_url[^;]*"
window.gateway_url = 'http://192.168.1.200:8137'

填内部域名(如 http://azimutt-main-gateway:3000)浏览器无法解析;同时 gateway 的 CORS_ALLOW_ORIGIN 必须等于应用访问 origin。

十一、参考链接

Azimutt Docker Compose 部署指南 - Quzuo