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
- In
docker inspectMounts, confirm that/app/dataand/app/storagepoint to the intended host directories and showRW=true. - 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.
- 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
- If
vmstatshows sustainedsi/so, the host is swap-thrashing. Prefer zram and avoid relying on slow SD-card swap for normal operation. - If
wastays high, movedata/andstorage/to lower-latency storage such as a USB SSD. - During a large restore, stop unrelated heavy containers. Optional services such as Portainer can be stopped while diagnosing.
- More swap prevents OOM; it does not make the workload faster. If pressure remains high, reduce concurrent work or move Vyline to a host with more RAM.
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
- For normal LAN use, keep
VYLINE_TRUST_REMOTE_OWNER=falseand pair the remote browser as a subdevice from the loopback owner UI. - If browser data was cleared or the user moved to another browser/profile, the installation ID changes and pairing must be repeated.
- 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=trueand 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.