Vyline DOCS

Troubleshooting

Separate the symptom into Vyline application behavior, persistence, Docker, or Android-host behavior before changing anything.

First checks

docker compose ps
docker compose logs --tail=200
docker inspect vyline
curl -fsS http://127.0.0.1:3000/healthz
df -hT

If healthz returns 200, Vyline's HTTP server itself is running. From there, isolate one boundary at a time: persistence only, container egress only, or LAN ingress only.

Restored history disappears after reload

Check first: the /app/data bind mount and write permissions. Verify persistence before assuming the problem is Protocol decryption.

docker inspect vyline
docker logs vyline
du -sh data storage

Resolution

  1. In docker inspect Mounts, confirm that /app/data and /app/storage point to the intended host directories and show RW=true.
  2. If a mount is read-only, points to a missing path, or still references the old path after a move, correct Compose/.env. Stop the container before moving existing data.
  3. Recreate with the current image so the bind-mount initialization runs again.
docker compose pull
docker compose up -d --force-recreate
docker compose logs --tail=100

After restore, reload the UI and then restart the container. If the history remains after both, persistence to SQLite is working.

Capacity shows 0 B

First confirm that data / storage are mounted at the intended paths and that the host can report capacity for that filesystem. Backend uses statfs on the storage filesystem, so unusual filesystems or execution environments can make capacity detection fail. If you suspect an old image, run docker compose pull, recreate the container, and retest with the same storage path.

Resolution

df -hT data storage
docker exec vyline sh -lc 'df -hT /app/data /app/storage'

If the host and container show different filesystems, correct the bind-mount path. If the host itself cannot report capacity reliably for a FUSE or other unusual filesystem, move the storage path to a normal ext4/xfs/btrfs filesystem and bind it again. Before moving data, update the image and retest the same mount.

Raspberry Pi / small arm64 host is extremely slow

Use free -h, vmstat 1, and docker stats to inspect memory pressure and swap activity. More zram/swap can avoid OOM while still making restore or synchronization extremely slow because of I/O wait. Where available, use tools such as iostat to inspect storage latency as well.

Resolution

Android: overlay2 EINVAL

Use docker info and mount information to identify the filesystem backing /var/lib/docker. If overlay2 creation actually returns EINVAL on an F2FS/casefold combination, consider moving Docker's data-root to an ext4 loop image as a workaround.

Resolution

Only when F2FS/casefold has been confirmed as the cause, create an ext4 loop for Docker's data-root from an Android root shell. Adjust the image size to available space.

ROOT=/data/data/com.termux/files/usr/var/lib/proot-distro/containers/ubuntu/rootfs
mkdir -p /data/local/docker-storage "$ROOT/var/lib/docker"
truncate -s 16G /data/local/docker-storage/docker-ext4.img
/system/bin/mke2fs -t ext4 -F /data/local/docker-storage/docker-ext4.img
LOOP=$(/system/bin/losetup -f)
/system/bin/losetup "$LOOP" /data/local/docker-storage/docker-ext4.img
mount -t ext4 -o rw,noatime "$LOOP" "$ROOT/var/lib/docker"

Restart dockerd inside the chroot, then verify the Backing Filesystem in docker info and run docker run --rm hello-world. If overlay2 already works on the original filesystem, this workaround is unnecessary.

Android: runc remount / invalid argument

Confirm that the chroot rootfs is mounted with the required self-bind and rslave setup.

Resolution

PREFIX=/data/data/com.termux/files/usr
ROOT="$PREFIX/var/lib/proot-distro/containers/ubuntu/rootfs"
BB="$PREFIX/bin/busybox"

su
$BB mount --bind "$ROOT" "$ROOT"
$BB mount --make-rslave "$ROOT"
$BB mount --rbind /dev "$ROOT/dev"
$BB mount --make-rslave "$ROOT/dev"
$BB mount -t proc proc "$ROOT/proc"
$BB mount --rbind /sys "$ROOT/sys"
$BB mount --make-rslave "$ROOT/sys"
$BB mount -t tmpfs -o mode=755,nosuid,nodev tmpfs "$ROOT/run"

Enter the chroot again only after these mounts exist, restart dockerd, and verify docker run --rm hello-world. If the error returns after every reboot, run this mount sequence before dockerd in the boot procedure.

Android: containers cannot reach the Internet

Check ip rule, the main routing table, FORWARD policy, MASQUERADE, and Android netd policy routing.

Resolution

Enable IPv4 forwarding first. Only after packet counters confirm that the real Android path uses legacy iptables, add dedicated chains without flushing netd-managed chains.

echo 1 > /proc/sys/net/ipv4/ip_forward
WAN_IF=$(ip route get 1.1.1.1 | awk '/dev/ {for (i=1;i<=NF;i++) if ($i=="dev") {print $(i+1); exit}}')
DOCKER_SUBNET=$(docker network inspect bridge --format '{{(index .IPAM.Config 0).Subnet}}')

iptables -N VYDOCKER_FWD 2>/dev/null || true
iptables -t nat -N VYDOCKER_POST 2>/dev/null || true
iptables -C FORWARD -j VYDOCKER_FWD 2>/dev/null || iptables -I FORWARD 1 -j VYDOCKER_FWD
iptables -t nat -C POSTROUTING -j VYDOCKER_POST 2>/dev/null || iptables -t nat -I POSTROUTING 1 -j VYDOCKER_POST
iptables -A VYDOCKER_FWD -i docker0 -o "$WAN_IF" -s "$DOCKER_SUBNET" -j ACCEPT
iptables -A VYDOCKER_FWD -i "$WAN_IF" -o docker0 -d "$DOCKER_SUBNET" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
iptables -t nat -A VYDOCKER_POST -s "$DOCKER_SUBNET" -o "$WAN_IF" -j MASQUERADE

If Android policy routing does not fall through to the main table, add a rule for the Docker subnet only, using an unused priority after inspecting the existing ip rule list. Do not remove netd's rules. Verify with docker run --rm alpine ping -c 1 1.1.1.1, then test DNS/HTTPS. See Android Networking for the full packet-path procedure.

Android: -p 3000:3000 resets from the LAN

Check in order: whether the host is listening, the NAT rules created by Docker, the iptables backend Android is actually using, and the chain through which packets travel. On tested devices, nft/legacy rule-path mismatches have caused this symptom, but do not add fixed DNAT rules before confirming the actual packet path on the target device.

Resolution

If curl http://127.0.0.1:3000/healthz works on the host and packet counters confirm the legacy path, build dedicated DNAT/FORWARD rules from the actual LAN interface, container IP, and bridge name.

CID=$(docker ps -qf 'name=vyline')
CONTAINER_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$CID")
NETWORK_ID=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.NetworkID}}{{end}}' "$CID")
BRIDGE_IF="br-$(printf '%s' "$NETWORK_ID" | cut -c1-12)"
LAN_IF=wlan0  # replace with the value from ip -br addr

iptables -t nat -N VYDOCKER_PRE 2>/dev/null || true
iptables -N VYDOCKER_FWD 2>/dev/null || true
iptables -t nat -C PREROUTING -j VYDOCKER_PRE 2>/dev/null || iptables -t nat -I PREROUTING 1 -j VYDOCKER_PRE
iptables -C FORWARD -j VYDOCKER_FWD 2>/dev/null || iptables -I FORWARD 1 -j VYDOCKER_FWD
iptables -t nat -A VYDOCKER_PRE -i "$LAN_IF" -p tcp --dport 3000 -j DNAT --to-destination "$CONTAINER_IP:3000"
iptables -A VYDOCKER_FWD -i "$LAN_IF" -o "$BRIDGE_IF" -p tcp -d "$CONTAINER_IP" --dport 3000 -j ACCEPT

Container IP and bridge names can change when Compose recreates the container. On devices that need this workaround, regenerate only these dedicated chains at boot or when Docker networking changes. Do not hard-code wlan0 for non-Wi-Fi paths.

Android: docker exec cannot see /app

PID=$(docker inspect -f '{{.State.Pid}}' vyline)
readlink /proc/$PID/ns/mnt
ls -la /proc/$PID/root/app
nsenter -t "$PID" -m -- ls -la /app
docker exec vyline ls -la /app

If the container PID root is correct, the problem can be isolated to the namespace used by docker exec.

Resolution

If /proc/$PID/root and nsenter are correct while only docker exec is wrong, do not delete the image or volumes. Rebuild the real-chroot mounts with self-bind + rslave, restart dockerd from that mount namespace, and recreate the container.

If the application HTTP endpoint is healthy but only the Compose healthcheck fails through the same exec path, disabling the healthcheck is an acceptable workaround after confirming the cause. Use nsenter for the container's real mount view while diagnosing.

Remote access returns 401 / 403

A non-loopback bind requires remote authentication even when VYLINE_LAN_ACCESS=false. Check the subdevice pairing state and installation ID. Set VYLINE_TRUST_REMOTE_OWNER=true for owner-only operations from a remote browser only when the route itself is authenticated by a layer such as Cloudflare Access or Tailscale ACLs.

Resolution

  1. For normal LAN use, keep VYLINE_TRUST_REMOTE_OWNER=false and pair the remote browser as a subdevice from the loopback owner UI.
  2. If browser data was cleared or the user moved to another browser/profile, the installation ID changes and pairing must be repeated.
  3. Only when Cloudflare Access, Tailscale ACLs, or an authenticated reverse proxy restricts users before they reach the Backend, and remote owner operations are required, set VYLINE_TRUST_REMOTE_OWNER=true and recreate the container.
docker compose up -d --force-recreate
docker compose logs --tail=100

Setting VYLINE_TRUST_REMOTE_OWNER=true on a raw LAN or Internet exposure is not a fix. Put the authentication boundary in place first.

Search by page, setting, or command