Sebastian Reategui

Remote docker context on a QNAP QTS container host

QTS supports Docker with the installation of the app ‘Container Station’.

Container Station offers a GUI way to control containers, but interfacing with Docker is still possible via CLI.

In a most advantageous way, you can interface with Docker on QTS via remote context, just as you would expect from any Docker host.

However, only with some light modification.

Typical steps

You already have SSH connectivity to the remote QTS host.

(In this case I have an administrator user on the NAS with my name, and my pub key is defined in QTS Settings.)

  1. Install Container Station via the App Center. Confirm it is operating via the web UI.
  2. Ideally also confirm Docker is operating via an admin shell.
seb@nas1 ~ $ docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
seb@nas1 ~ $
  1. Add the remote server as a context in docker context.
docker context create nas1 --description="nas1" --docker="host=ssh://nas1"

Challenge

Usually at this point you can access the Docker daemon from your own local shell successfully.

seb@mac ~ $ docker context use nas1
nas1
Current context is now "nas1"

You can now normally run docker commands like docker ps.

However, out of the box on QTS, running anything will produce an error message instead:

error during connect: Get “http://docker.example.com/v1.51/containers/json": command [ssh -o ConnectTimeout=30 -T – nas1 docker system dial-stdio] has exited with exit status 127, make sure the URL is valid, and Docker 18.09 or later is installed on the remote host: stderr=bash: docker: command not found

This occurs because the SSH session is in an environment whose path doesn’t have the docker binary.

This can be seen when you repeat the SSH call with echo $PATH instead.

seb@mac ~ $ ssh -o ConnectTimeout=30 -T -- nas1 docker system dial-stdio -- echo '$PATH'
/usr/bin:/bin:/usr/sbin:/sbin

The docker binary is installed by QTS App Center to a user data location instead:

seb@nas1 ~ $ which -a docker
/share/CACHEDEV1_DATA/.qpkg/container-station/bin/docker

There is no reference to the docker binary within the 4 paths listed in the above PATH.

Solution

Best workaround to suggest is to create a symlink :)

ln -sf /share/CACHEDEV1_DATA/.qpkg/container-station/bin/docker /usr/bin/docker

Then the docker binary is available, and docker ps or any other command will work.

Assumedly as long as your SSH user is an administrator.

Justification

Methodology: Symlink to an existing location on PATH.

In pro:

  • Simple, relies on one system: POSIX filesystem links.
  • Persists after reboot, since it is part of the filesystem.

In contra:

  • May not be vendor update-proof, if system updates modify or change how /usr/bin is populated.*

Another method may be to edit the shell environment in some way to include the .qpkg/container-station/bin directory in its PATH.

However, this seemed to have greater complexity for me in terms of investigation time.

I would rather edit a filesystem link in a POSIX directory, than edit vendor-managed bash shell files.

Though there are others who perhaps would much prefer the other way around.

I note there is a clear difference in the PATH already between making an ssh call directly with a command, and opening an executable shell.

PATH when executing docker system dial-stdio -- echo '$PATH', as docker context would:

# 4 paths
/usr/bin:/bin:/usr/sbin:/sbin

PATH when executing echo $PATH in an interactive shell:

# many
seb@nas1 ~ $ echo $PATH
/opt/bin:/opt/sbin:/share/CACHEDEV1_DATA/.qpkg/container-station/bin:/bin:/sbin:/usr/bin:/usr/sbin:/usr/bin/X11:/usr/local/sbin:/usr/local/bin:/opt/QPython312/bin:/opt/Nano/bin:/opt/QGit/bin:/opt/bin/go/bin
seb@nas1 ~ $ echo $SHELL
/bin/bash
seb@nas1 ~ $

Clearly this shows some other processes are involved in establishing the bash environment, between vendor (QNAP QTS), bash versus sh, my own bash profile in my home directory if any, Entware, … there could be dragons … enough said for now.

Use case

Remote Docker context opens lots of doors for interacting with the QNAP host as you would any other container host.

Apply compose files stored in an IAC repo, that is not located directly on the remote host, instead your working client.

Exec shell into containers for troubleshooting, or quickly grep log output.

Use cp to copy data to/from containers on the local host, where the transport is SSH.

Easier management among many other servers, using a UI tool like Termix or XPipe.

Not have to mess too much with a QNAP shell, missing utils or binaries, or other environmental nuances.

The docker version details for this distribution of QTS (an x86_64 host, QNAP TS-873).

seb@nas1 ~ $ docker info
Client:
 Version:    27.1.2-qnap7
 Context:    default
 Debug Mode: false
 Plugins:
  buildx: Docker Buildx (Docker Inc.)
    Version:  v0.21.2-qnap1
    Path:     /usr/local/lib/docker/cli-plugins/docker-buildx
  compose: Docker Compose (Docker Inc.)
    Version:  v2.29.1-qnap2
    Path:     /usr/local/lib/docker/cli-plugins/docker-compose

Server:
 Containers: 1
  Running: 1
  Paused: 0
  Stopped: 0
 Images: 8
 Server Version: 27.1.2-qnap7
 Storage Driver: overlay2
  Backing Filesystem: extfs
  Supports d_type: true
  Using metacopy: true
  Native Overlay Diff: false
  userxattr: false
 Logging Driver: json-file
 Cgroup Driver: cgroupfs
 Cgroup Version: 1
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay qnet
  Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
 Swarm: inactive
 Runtimes: runc io.containerd.runc.v2 kata-runtime nvidia-runtime
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: 8fc6bcff51318944179630522a095cc9dbf9f353
 runc version: v1.1.13-0-g58aa920
 init version: de40ad0
 Security Options:
  apparmor
  seccomp
   Profile: builtin
 Kernel Version: 5.10.60-qnap
 Operating System: QTS 5.2.7 (20251024)
 OSType: linux
 Architecture: x86_64
 CPUs: 4
 Total Memory: 15.59GiB
 Name: nas1
 ID: [xxxxxxx]
 Docker Root Dir: /share/CACHEDEV1_DATA/Container/container-station-data/lib/docker
 Debug Mode: true
  File Descriptors: 30
  Goroutines: 50
  System Time: 2026-01-09T18:03:00.811063986+11:00
  EventsListeners: 1
 Experimental: false
 Insecure Registries:
  127.0.0.0/8
 Live Restore Enabled: false
 Product License: Community Engine
 Default Address Pools:
   Base: 172.29.0.0/16, Size: 22

Notes

(*) I also added this to an autorun script using OneCDOnly’s autorun tool for QTS create-autorun. Since I wanted some assurance it would stick through future updates long after I had forgotten about this.

Comments

Thanks for reading!

Commenting is provided through Giscus. Giscus is an open source comment system with no tracking or analytics, aside from OAuth via GitHub. To comment, you just need to sign in to GitHub and authorise Giscus once.