Skip to content

Conversation

@dirkwa
Copy link
Contributor

@dirkwa dirkwa commented Jan 2, 2026

This PR switches the Docker images from global npm install (npm i -g) to local install (npm install). This fixes the permission issues when installing plugins in rootless container environments like Podman.

What changed

  • Dockerfile_rel: Install signalk-server locally in /home/node/signalk/node_modules/ instead of globally in /usr/lib/node_modules/
  • startup.sh: Updated path to use local node_modules/.bin/signalk-server, fixed D-Bus warning
  • Dockerfile_base_24.04: Removed the setcap on the node binary (verified not needed)

The key fix is the symlink for @SignalK packages - the admin UI looks for them in a nested path, so we create:

ln -s /home/node/signalk/node_modules/@signalk node_modules/signalk-server/node_modules/@signalk

BLE works without setcap cap_net_raw+eip on the node binary when running with container capabilities - this is the cleaner approach for containerized deployments. Let me know if you'd like any changes!

Tested on RPI 4 and RPI5 with headless podman

  • Server startup
  • Admin UI
  • Plugin install (Appstore)
  • BLE scanning (bt-sensors-plugin-sk) - Works with NET_ADMIN + NET_RAW capabilities, no setcap needed
  • CAN/NMEA2000 (canboatjs) - Works with --net=host + NET_ADMIN
  • mDNS/Avahi discovery - Works via D-Bus socket mount

To verify

# 1. Install podman
sudo apt install podman podman-docker

# 2. Build the base image (use --no-cache to avoid stale layers)
podman build --no-cache -f docker/Dockerfile_base_24.04 \
  --build-arg NODE=22.x \
  -t cr.signalk.io/signalk/signalk-server-base:latest .

# 3. Build the Signal K server image
podman build --no-cache -f docker/Dockerfile_rel \
  --build-arg TAG=latest \
  -t signalk-server-test .

# 4. Run with rootless Podman
mkdir -p ~/.signalk
podman run -d --name signalk-test \
  -p 3000:3000 \
  -v ~/.signalk:/home/node/.signalk:Z \
  -v /run/dbus/system_bus_socket:/run/dbus/system_bus_socket \
  --net=host \
  --cap-add=NET_ADMIN \
  --cap-add=NET_RAW \
  --userns=keep-id:uid=1000,gid=1000 \
  signalk-server-test

dirkwa added 3 commits January 3, 2026 05:38
Change Docker images to use local npm install instead of global (npm -g).
This avoids permission issues when installing plugins and running in
rootless container environments like Podman.

Changes:
- Dockerfile: use npm install instead of npm i -g for tarballs
- Dockerfile_rel: use npm install instead of npm i -g for release
- Dockerfile_base_24.04: remove setcap on node binary (not needed)
- startup.sh: update path to local node_modules/.bin/
- README.md: update documentation for new directory structure

The server now installs in /home/node/signalk/node_modules/ and runs
entirely in user space without requiring special capabilities.
When running with rootless Podman/Docker and the host's D-Bus socket
mounted, the container should use the host's D-Bus and Avahi services
instead of trying to start its own.

Previously, startup.sh always tried to start D-Bus and Avahi, which
produced confusing error messages like "Address already in use" and
"Failed to contact D-Bus daemon" even though everything worked fine.

Now we detect if the host D-Bus socket is already connected and skip
starting container services in that case. This gives clean startup
logs for rootless container deployments while maintaining backwards
compatibility with Docker Compose setups that don't mount the socket.
@dirkwa dirkwa changed the title refactor(docker): switch from global to local npm install Hey, This PR switches the Docker images from global npm install (npm i -g) to local install (npm install). This fixes the permission issues when installing plugins in rootless container environments like Podman. refactor(docker): switch from global to local npm install Jan 2, 2026
Symlinks don't work reliably in all Docker/Compose configurations.
Copy the @SignalK packages to the nested node_modules location
where the admin-ui expects to find them.
@tkurki
Copy link
Member

tkurki commented Jan 2, 2026

Adding some insight into the local install: there is value in having non-docker non-container and docker container based installs similar. But there would also be value in not using or needing sudo, where we could be if did not use -g.

So we could experiment with containers, creating a new separate rootless -g container build. Once we are happy with that we could deprecate the old -g containers AND switch the non-container install over - or strongly discourage or even completely deprecate npm-based install.

@tkurki tkurki added the feature label Jan 2, 2026
@KEGustafsson
Copy link
Contributor

KEGustafsson commented Jan 2, 2026

In docker, docker compose, podman or podman compose can end user login as a root with -u root if needed. Target to be able run without root is good, but this should be done platform agnostic way.

Current rootless approach seems to run on podman, but not in docker or docker compose. Or then support platforms with own build recepies.

@dirkwa
Copy link
Contributor Author

dirkwa commented Jan 2, 2026

I am not a fan of maintaining two platforms. There must be a simple solution to happy coexist in both worlds. I will setup a vm with the docker toolchain and see what's needed to make both happy.

dirkwa added 3 commits January 3, 2026 03:06
Allow Dockerfile_base_24.04 to accept NODE_VERSION build arg (e.g.,
NODE_VERSION=22) in addition to existing NODE arg (e.g., NODE=22.x)
for better compatibility with different build scenarios.

Add shell logic to ensure NodeSource setup script receives proper
version format (with .x suffix) regardless of input format.

Maintains backward compatibility with CI/CD workflows that use
--build-arg NODE=22.x while enabling manual builds with
--build-arg NODE_VERSION=22.
Add automatic detection of container runtime environment and export
CONTAINER_RUNTIME environment variable to differentiate between Docker,
Podman, Kubernetes, containerd, CRI-O, and LXC at runtime.

Detection uses multiple methods in priority order:
- File markers (/.dockerenv for Docker, /run/.containerenv for Podman)
- Environment variables (KUBERNETES_SERVICE_HOST for Kubernetes)
- cgroup patterns (/kubepods, /lxc, /containerd, /docker, /libpod)
- Runtime sockets (/var/run/crio, /var/run/containerd/containerd.sock)

This enables plugins and addons to adapt behavior based on the actual
runtime environment. The same container image now works across all
runtimes while exposing runtime-specific information via
process.env.CONTAINER_RUNTIME.

Maintains backward compatibility with IS_IN_DOCKER environment variable
which remains set to "true" in all containerized environments for
existing server update detection logic.

Users can override detection by setting CONTAINER_RUNTIME manually:
docker run -e CONTAINER_RUNTIME=custom signalk/signalk-server

Modified files:
- docker/startup.sh: Add runtime detection logic for 6 container runtimes
- docker/README.md: Add comprehensive documentation with usage examples
  and detection methods for all supported runtimes
Copy link
Member

@tkurki tkurki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some inline comments.

I am just not sure how to proceed - like this, change all the containers at once, or start building new ones alongside the -g ones.

Does this break anything for the user - so that something that used to work no longer does?

- **`crio`** - CRI-O (common in Kubernetes environments)
- **`lxc`** - LXC/LXD containers

### Detection Methods
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove Detection Methods, Usage in Code and Manual Override alltogether. Detection is pretty obvious in the code, usage should be simple enough for the target audience and if you need to override container runtime you should know how to do it and not need 3x instructions.


## Container Runtime Detection

The server automatically detects which container runtime is being used and sets the `CONTAINER_RUNTIME` environment variable accordingly. This enables plugins and addons to adapt their behavior based on the actual runtime environment.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add to the master list of env variables.

Comment on lines +152 to +154
### Backward Compatibility

The `IS_IN_DOCKER` environment variable remains set to `true` in all containerized environments for backward compatibility with existing code and plugins.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing changes in this regard, so don't want to add "remains set" here - remove?

Comment on lines -85 to -91
## Directory structure

- server files: `/home/node/signalk`
- settings files and plugins: `/home/node/.signalk`

You most probably want to mount `/home/node/.signalk` from the host or as a volume to persist your settings.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove?

FROM ubuntu:24.04
ARG NODE
ARG NODE_VERSION
# Ensure NODE has .x suffix for NodeSource (supports both "22" and "22.x" input)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This I don't understand? And if the argument changes don't the workflow files need to be changed also?

@dirkwa
Copy link
Contributor Author

dirkwa commented Jan 9, 2026

Does this break anything for the user - so that something that used to work no longer does?

In fairness I am quite new to the way how docker is used today and what images are used by whom and in which use case.
I was focusing on the Universal Installer based upon podman and I am close to finalize it.

Supported platforms

  • Linux arm64 - implemented
  • Linux amd64 - implemented
  • Windows wsl - implemented
  • macOS - will be next

For rapid user adoption I need

  • End User simple install like curl -sSL https://get.signalk.io | bash
  • The images must be as small as possible - I am here already down to < 600MB Ubuntu is 1.3 GB (!) - yes, incl. python!
  • SignalK in userspace only - for following devpods mandatory
  • A way to let the admin UI know what to do via my patch hook

Major features work

I would love that we only just need one docker image that can be used - as it was (unknown) and for the universal installer.

With #2239 we could use:

REPOSITORY                      TAG                  IMAGE ID      CREATED         SIZE
localhost/signalk-server        alpine-local         7a466c9f824e  2 minutes ago   524 MB

vs

ghcr.io/signalk/signalk-server  latest               5c705aaa2f7b  2 days ago      1.34 GB

@dirkwa
Copy link
Contributor Author

dirkwa commented Jan 9, 2026

@tkurki @KEGustafsson

Just to be clear - this PR is work in progress, not ready for merge. I want to discuss the direction first.

What I'm proposing:
One Alpine image for everything - Docker, Podman, Universal Installer. 524MB vs 1.34GB is hard to ignore. Same image, same registry, less to maintain.

CONTAINER_RUNTIME detection - So the server knows where it's running. I'll do a separate PR to signalk-server to use this instead of IS_IN_DOCKER. Then we can phase out IS_IN_DOCKER properly.

Node 24 - Ready for it, good WASM support.

One thing I need input on: npm global packages in userspace only, or separate images?

For the Universal Installer I need everything running in userspace (no root). If that's a problem for traditional Docker users, we could do two images - but I'd rather not.

Worst case: keep Ubuntu for Docker as-is, Alpine for Universal Installer. But that doubles the maintenance and defeats the purpose.

What's your take?

@KEGustafsson
Copy link
Contributor

KEGustafsson commented Jan 9, 2026

I think we are now discussing about image and disk size in many forums at the same time.
Once again, I made comparison with alpine:latest and ubuntu:24.04 base images and same packages included

Size difference is ~500MB on disk, less in compressed images. Extracted image on disk can't be that small as you indicated.

IMAGE                                        ID             DISK USAGE   CONTENT SIZE   EXTRA
ghcr.io/kegustafsson/signalk-server:ubuntu   b26df4854a6a       1.69GB          391MB    U
ghcr.io/kegustafsson/signalk-server:alpine   71288948eac9        1.2GB          267MB    U

I haven't test this alpine image yet, so can't say is there compatibility issues with plugins etc...

@dirkwa
Copy link
Contributor Author

dirkwa commented Jan 9, 2026

@KEGustafsson
Let us continue here until we know if podman and docker will use the same image or separate ones: https://discord.com/channels/1170433917761892493/1459099551024812146

Thanks,
Dirk

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants