Can't deploy latest versions on fly.io

The latest versions of Silverbullet don’t like starting on fly.io, suddenly. I was running fine before (at least up until a couple of weeks ago), and now… nothing. I wonder if some combination of the Go backend and fly.io is causing problems, but I’m not sure what those problems are.

Initially, I was getting errors from fly around tini:

2025-09-30T16:11:52Z app[d899455a6e1198] dfw [info] INFO Preparing to run: `/sbin/tini -- /docker-entrypoint.sh` as root
2025-09-30T16:11:52Z app[d899455a6e1198] dfw [info] INFO [fly api proxy] listening at /.fly/api
2025-09-30T16:11:52Z app[d899455a6e1198] dfw [info][WARN  tini (655)] Tini is not running as PID 1 and isn't registered as a child subreaper.
2025-09-30T16:11:52Z app[d899455a6e1198] dfw [info]Zombie processes will not be re-parented to Tini, so zombie reaping won't work.
2025-09-30T16:11:52Z app[d899455a6e1198] dfw [info]To fix the problem, use the -s option or set the environment variable TINI_SUBREAPER to register Tini as a child subreaper, or run Tini as PID 1.

Since the machine shut down immediately after, I thought tini might be to blame. I’m less sure, now.

Since fly.io doesn’t like other things running as init, I tried making my own Dockerfile locally to run the silverbullet binary directly which skipped using tini. I brought over some of the ENV from the existing Dockerfile and based mine on the GitHub registry version. Still no luck, it looks like the binary immediately exits.

I tried again, this time using the docker-entrypoint.sh, same result.

I ran fly console, which starts a temporary machine in their cloud and gives you a console into it sort of “inside” your Docker container. When I run the silverbullet binary myself I get the same behavior:

7817602a5991e8:/# ./silverbullet
2025/10/01 14:45:18 User authentication enabled for user "drhayes" with lockout limit 10 and lockout time 60s
2025/10/01 14:45:18 Local shell command execution enabled for ALL commands.
2025/10/01 14:45:18 Index page index.md does not yet exist, creating...
2025/10/01 14:45:18 Config page CONFIG.md does not yet exist, creating...
2025/10/01 14:45:18 SilverBullet is now running: http://:::3000
7817602a5991e8:/#

I’m stumped! When I run my new Docker container locally it serves just fine, so this is probably something to do with fly.io.

I’m kind of familiar with Go, but I can’t see anything in the source of the server that stands out as a red flag. And, again, it runsjust fine on my local machine – both outside of Docker and within it.

Anyone else have this problem? Any solutions? Any ideas?

This is really weird, I can’t really explain it. I haven’t used fly.io in a while (since writing that guide that you also responded to). I’ll have to set it up again to see what this could be. Some cursory searching seems to suggest using something like tini should be fine, but since you tried just launching the binary right in the console and it starting fine and then just exiting… weird. What’s the exit code after running that?

echo $?

Thanks for the quick response! It’s 0, it thinks everything’s fine, I guess.

1859473b145418:/# ./silverbullet
2025/10/01 16:21:25 User authentication enabled for user "drhayes" with lockout limit 10 and lockout time 60s
2025/10/01 16:21:25 Local shell command execution enabled for ALL commands.
2025/10/01 16:21:25 Index page index.md does not yet exist, creating...
2025/10/01 16:21:25 Config page CONFIG.md does not yet exist, creating...
2025/10/01 16:21:25 SilverBullet is now running: http://:::3000
1859473b145418:/# echo $?
0
1859473b145418:/#

In case it helps, here’s the environment:

FLY_PUBLIC_IP=2a02:6ea0:d235:0:8000:39bd:2c0f:1
FLY_VM_MEMORY_MB=256
FLY_MACHINE_ID=1859473b145418
SHLVL=1
HOME=/root
PAGER=less
FLY_SSH=1
FLY_PRIVATE_IP=fdaa:0:45cf:a7b:526:39bd:2c0f:2
FLY_IMAGE_REF=registry.fly.io/silverbullet-fly:deployment-01K6G4S5DEVPJ0MJ49T3MXSDH3
TERM=xterm-ghostty
SB_USER=drhayes:XXXXXXXXX
LC_COLLATE=C
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
FLY_ALLOC_ID=1859473b145418
FLY_MACHINE_VERSION=01K6G97EKGJSG0C0MKCZ9PBAPF
LANG=C.UTF-8
SB_FOLDER=/space
SHELL=/bin/sh
FLY_PROCESS_GROUP=fly_app_console
FLY_REGION=dfw
FLY_APP_NAME=silverbullet-fly
PWD=/
SB_HOSTNAME=::
CHARSET=UTF-8
PRIMARY_REGION=dfw

The /space directory exists. The FLY_IMAGE_REF isn’t the GHCR image because it’s trying the one I made without tini. When I run it with the standard Dockerfile I get the same result, same 0 exit code.

In case it matters, here’s the fly.toml:

app = 'silverbullet-XXX'
primary_region = 'dfw'
swap_size_mb = 512

[build]
image = 'ghcr.io/silverbulletmd/silverbullet:v2'

[mounts]
source = 'bulletdrive'
destination = '/space'

[env]
SB_HOSTNAME = "::"

I’m at a loss! If I can’t host on Fly it’s not the biggest of deals, but I liked being able to run it there without giving access to the whole rest of the world – I usually access it with a proxy only I can run. The next thing I’d try is running it on Hetzner (or somthing) with tailscale.

I encountered the same issue with tini, yet it appears that it does not affect the normal initialization/operation of SB.

When I first migrated from version 2.0.0 to 2.1.6, the system failed to function properly.

My approach was to begin with the ‘blank’ official image of version 2.1.6 (or 2.1.7).

If it could launch and deploy successfully, I then incrementally restored my previous environment and added custom initialization scripts on top of this minimal, working official base.

I am able to share the initialization setup for my read-only SB instance (from Windows OS):

  1. fly.toml
app = 'enlarge-the-percentage'
primary_region = 'lax'
swap_size_mb = 256

[deploy]
  strategy = 'immediate'

[[mounts]]
  source = 'silverbullet_space'
  destination = '/space'

[[services]]
  protocol = 'tcp'
  internal_port = 3000
  auto_stop_machines = 'stop'
  auto_start_machines = true
  min_machines_running = 0

  [[services.ports]]
    port = 80
    handlers = ['http']

  [[services.ports]]
    port = 443
    handlers = ['tls', 'http']

[[vm]]
  memory = '256mb'
  cpu_kind = 'shared'
  cpus = 1

[[statics]]
  guest_path = '/space/STYLE'
  url_prefix = '/STYLE'
  1. Dockerfile
FROM ghcr.io/silverbulletmd/silverbullet:v2

RUN apk add --no-cache dos2unix

WORKDIR /

COPY init.sh /init.sh
RUN dos2unix /init.sh && chmod +x /init.sh

ENTRYPOINT ["/sbin/tini", "-g", "--"]
CMD ["/init.sh"]
  1. init.sh
#!/bin/bash
set -euo pipefail

cd /space

# -------------------------------
# 2️ 初始化或验证 Git 仓库
# -------------------------------
EXPECTED_USER_NAME="Xcz"
EXPECTED_USER_EMAIL="[email protected]"
EXPECTED_CREDENTIAL_HELPER="store"
EXPECTED_PULL_REBASE="false"
EXPECTED_PUSH_AUTOREMOTE="true"
EXPECTED_CORE_EDITOR="nano"
EXPECTED_ORIGIN_URL="https://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@github.com/ChenZhu-Xie/xczphysics_SilverBullet.git"

if [ ! -d ".git" ]; then
  echo "[INFO] 初始化 Git 仓库"
  git init -b main
fi

# Helper: 设置或修正配置
set_config_if_needed() {
  local key=$1
  local expected=$2
  local current
  current=$(git config --local --get "$key" || echo "")
  if [ "$current" != "$expected" ]; then
    echo "[WARN] 修正 Git 配置 $key: 当前='$current' -> 期望='$expected'"
    git config "$key" "$expected"
  else
    echo "[INFO] Git 配置 $key 正确"
  fi
}

# 检查并修复配置
set_config_if_needed "user.name" "$EXPECTED_USER_NAME"
set_config_if_needed "user.email" "$EXPECTED_USER_EMAIL"
set_config_if_needed "credential.helper" "$EXPECTED_CREDENTIAL_HELPER"
set_config_if_needed "pull.rebase" "$EXPECTED_PULL_REBASE"
set_config_if_needed "push.autoSetupRemote" "$EXPECTED_PUSH_AUTOREMOTE"
set_config_if_needed "core.editor" "$EXPECTED_CORE_EDITOR"

# 检查远程 origin
CURRENT_ORIGIN=$(git remote get-url origin 2>/dev/null || echo "")
if [ -z "$CURRENT_ORIGIN" ]; then
  echo "[WARN] 未检测到远程 origin,正在添加"
  git remote add origin "$EXPECTED_ORIGIN_URL"
elif [ "$CURRENT_ORIGIN" != "$EXPECTED_ORIGIN_URL" ]; then
  echo "[WARN] 远程 origin URL 不正确,正在修复: 当前='$CURRENT_ORIGIN'"
  git remote set-url origin "$EXPECTED_ORIGIN_URL"
else
  echo "[INFO] 远程 origin URL 正确"
fi

# -------------------------------
# 3️ 创建默认文件(如果不存在)
# -------------------------------
if [ ! -f ".gitignore" ]; then # 创建 .gitignore(仅当不存在时)
  echo "[INFO] 创建 .gitignore"
  echo ".silverbullet.db*" > .gitignore
  echo "*.bat" >> .gitignore
  echo "*.sublime-workspace" >> .gitignore
  echo "CONTAINER_BOOT.md" >> .gitignore
fi

# -------------------------------
# 5️ 强制同步远程 main 分支
# ------------------------------- # 不论 SB 是否只读,这里在 持久化卷挂载后、SB 启动前进行的 写入操作都会强制实施,且预计成功
run_with_retry() { # 封装一个带重试的函数
  local cmd="$*"
  local attempt=1
  local max_attempts=5   # 最大 5 次
  local delay=5          # 每次间隔 5 秒

  until eval "$cmd"; do
    if [ $attempt -ge $max_attempts ]; then
      echo "[ERROR] 命令失败:$cmd"
      return 1
    fi
    echo "[WARN] 命令失败:$cmd"
    echo "[INFO] $delay 秒后重试... (第 $((attempt+1))/$max_attempts 次)"
    sleep $delay
    attempt=$((attempt+1))
  done
}

# -------------------------------
# 5️ 强制同步远程 main 分支
# -------------------------------
echo "[INFO] 检查本地是否配置了远程 origin..."
if ! git remote get-url origin &>/dev/null; then
  echo "[ERROR] 没有配置远程 origin,请检查 init.sh 中的 git remote add"
  exit 1
fi
echo "[INFO] 当前远程 URL: $(git remote get-url origin)"

echo "[INFO] 检查远程仓库可访问性..."
if ! run_with_retry "git ls-remote --exit-code origin &>/dev/null"; then
  echo "[ERROR] 无法访问远程仓库(可能是网络或权限问题)"
  exit 1
fi

echo "[INFO] 检查远程是否为空..."
if [ -z "$(git ls-remote --heads origin)" ]; then
  echo "[INFO] 远程仓库为空,初始化推送"
  git add .
  git commit -m "initial commit" || true
  if ! run_with_retry "git push -u origin main:main"; then
    echo "[ERROR] 初始 push 失败(可能是只读权限)"
    exit 1
  fi
else
  echo "[INFO] 远程仓库非空,强制同步 main 分支"
  run_with_retry "git fetch origin main"
  run_with_retry "git reset --hard origin/main"
fi

echo "[INFO] Git 同步成功 ✅"

# 交给原有的入口脚本继续启动
exec /docker-entrypoint.sh "$@"

As an alternative, the previously mentioned init.sh, once its last line is removed, may be used as CONTAINER_BOOT.md and executed directly by the official docker-entrypoint.sh.

However, I personally find it better to keep the boot file outside of SB_space?

In addition, logically speaking, init.sh and docker-entrypoint.sh should be run in sequence? as init.sh modifies some Git settings AND performs Git operations within SB_space. — Therefore, init.sh ought to be executed prior to docker-entrypoint.sh, not in parallel or subsequently?

In addition to the official init script docker-entrypoint.sh, the official Dockerfile
is also worth studying. I hope it will be of help.

2 Likes

I was about to try Xiè Chén-Zhú’s solution, but now that 2.1.7 is updated with more server logging I saw these logs:

2025-10-06T19:09:23.353 app[d899455a6e1198] dfw [info] 2025/10/06 19:09:23 SilverBullet is now running: http://:::3000
2025-10-06T19:09:23.355 app[d899455a6e1198] dfw [info] 2025/10/06 19:09:23 HTTP server error: listen tcp: address :::3000: too many colons in address
2025-10-06T19:09:23.512 app[d899455a6e1198] dfw [info] 2025/10/06 19:09:23 INFO SSH listening listen_address=[fdaa:0:45cf:a7b:41e:9670:3225:2]:22

The too many colons error looks familiar, because in my fly.toml I’m specifying the SB_HOSTNAME environment variable as :: because I thought I needed to do that to bind to the IPv6 host in fly’s infrastructure. Based on their docs about this, I actually need to put fly-local-6pn here and it should work.

And it did! The machine stays running! I can proxy to my instance! So, problem solved.

The end of my fly.toml file looks like this now:

[env]
SB_HOSTNAME = "fly-local-6pn"
2 Likes