Skip to content

non-root container with s6-overlay#798

Merged
bbernhard merged 2 commits into
bbernhard:rootless_s6from
poggenpower:non-root
Mar 7, 2026
Merged

non-root container with s6-overlay#798
bbernhard merged 2 commits into
bbernhard:rootless_s6from
poggenpower:non-root

Conversation

@poggenpower
Copy link
Copy Markdown

This using s6-overlay to manage processes need to run in the container.
s6-services/ contains all files for services control. entrypoint.sh is not executed anymore
jsonrpc2-helper is migrated into the startscript s6-services/signal-json-rpc/run. I don't see an advantage to use a separate
generator. I think it is all in one place now and not less maintainable.

This using s6-overlay to manage processes need to run in the container.
jsonrpc2-helper is migrated into the startscript.
@poggenpower
Copy link
Copy Markdown
Author

@bbernhard this is a first draft of my non-root approach. It replaces #789 If you can take a quick look if it going into the right direction or if you see anything showstopper or things that doesn't fit for you.
@kaktus42 any tests and feedback is welcome too.

It is a draft. Not heavily tested jet. Need still some cleanup e.g. removal of jsonrpc2-helper.go

if you don't want to build on your own for first tests, you can use: ghcr.io/poggenpower/signal:noroot-s6. It is AMD64/x86 only.

@bbernhard
Copy link
Copy Markdown
Owner

bbernhard commented Feb 27, 2026

Many thanks for your PR!

I think it might be a good idea to merge any changes to a separate branch - I've just created the rootless_s6 branch for that purpose. (I think I could change the target branch myself too, but I didn't want to mess with your PR). Since this is going to be a pretty big change, I guess it might make sense to test the changes on a separate branch (maybe even release official testing images to get more feedback) and merge it into the master branch once we are sure it's not going to break anything.

Regarding the jsonrpc2-helper: Personally I am not a big fan of bash - imho it's quite easy to introduce subtle bugs and I find it harder to maintain (e.g I could imagine that the jsonrpc2.yml might grow in the future and writing that via bash feels a bit rough - I definitely would prefer a compiled language with proper library support here). But that's just a minor thing and can be done later - so no need to change that right now.

I'll try to find some time in the next days to check out the code and play a little bit with it myself to get a better feeling how s6 works :)

@poggenpower poggenpower changed the base branch from master to rootless_s6 March 1, 2026 15:46
@poggenpower
Copy link
Copy Markdown
Author

I have changed the PR to rootless_s6. I agree on having this in parallel. I can change it to no touch existing files. e.g. by moving the changes to Dockerfile-rootless. (You can build with docker build -f 'Dockerfile-rootless' .). Not sure how active the community is watching some random branches.

Regarding the wrapper. Shell works pretty well for mangling ENVs and such. I would see a real advantage if there is a real go-wrapper which serves the socket and feed the java sub process to get rid of the ugly nc + fifo.
Unfortunately I am not a real go programmer and could not do make than as my favorite AI. But as you said, let's move it out of the scope of this change.

Regarding s6-overlay. Unfortunately I was not able to go with tini as it is not made for running more than one service, otherwise I had preferred it. I didn't found the s6 docs easy to read as they focusing more on complex legacy setups instead of starting your own service.

@bbernhard
Copy link
Copy Markdown
Owner

Thanks!

I finally found some time to play around with your changes and on my system it's working pretty well so far - great work! (only checked on my x86-64 system for now, but it looks really promising!)

A few small things I've noticed:

  • the GIN_MODE env variable isn't set anymore
  • Debian is used as base image instead of Ubuntu: I am myself also a Debian guy, but it seems that for some reason the Ubuntu guys create smaller base images (the Ubuntu image is about half the size of the Debian base image).

But those things are really some small details - so there's no need to fix them right now.

If you are fine with your PR, I'd like to get your changes merged to the branch. After that, I'd pull in the latest changes from the master branch and fix all the merge conflicts. For now, I'd like to keep the changes on a dedicated branch - that makes it easier to iterate on it. Once there is a working armv7 & arm64 build, I'd like to reach out to the community to see if there are some volunteers out there for testing. :)

@poggenpower
Copy link
Copy Markdown
Author

poggenpower commented Mar 4, 2026

just added GIN_MODE

  • Debian is used as base image instead of Ubuntu: I am myself also a Debian guy, but it seems that for some reason the Ubuntu guys create smaller base images (the Ubuntu image is about half the size of the Debian base image

I have used FROM debian:trixie-slim the slim image is around 40MB, ubuntu ~ 70 MB and debian:trixie ~120 MB.
But I think the dockerfile should work with ubuntu too. So we can switch if you like.

Yes, I think it is good to merge for some testing.

still needs cleanup:

  • entrypoint.sh (not needed anymore)
  • jsonrpc2-helper.go (not needed anymore, you you may want to keep it.)
  • unsure if s6-services in the root dir is the right place.
  • shell scripts could be tidied up, but if it went back into go again, then it is not necessary

@bbernhard
Copy link
Copy Markdown
Owner

bbernhard commented Mar 7, 2026

just added GIN_MODE

great, thanks!

I have used FROM debian:trixie-slim the slim image is around 40MB, ubuntu ~ 70 MB and debian:trixie ~120 MB.
But I think the dockerfile should work with ubuntu too. So we can switch if you like.

Ah, sorry, missed that you used the slim version. Looks like debian slim and ubuntu are pretty much the same:

https://hub.docker.com/layers/library/debian/trixie-slim/images/sha256-3b688c3b069da7a7c5dba3b7a5ec8e3744e8d03a5ec40e9eba8ca95dfe732fe6

https://hub.docker.com/layers/library/ubuntu/noble/images/sha256-98ff7968124952e719a8a69bb3cccdd217f5fe758108ac4f21ad22e1df44d237

So I am totally fine with debian :)

Yes, I think it is good to merge for some testing.

Great, then I'll merge it right away. Thanks!

@bbernhard bbernhard marked this pull request as ready for review March 7, 2026 15:16
@bbernhard bbernhard merged commit 53cd2dc into bbernhard:rootless_s6 Mar 7, 2026
@Qhilm
Copy link
Copy Markdown

Qhilm commented May 3, 2026

Hello, I'd like to test this as well.

Do I need to modify anything anything in the docker compose file, apart from the image now being "bbernhard/signal-cli-rest-api:rootless-latest"?

[edit]

Which permissions should I assign to the folder mapped to /home/.local/share/signal-cli?

@poggenpower
Copy link
Copy Markdown
Author

poggenpower commented May 3, 2026

Do I need to modify anything anything in the docker compose file, apart from the image now being "bbernhard/signal-cli-rest-api:rootless-latest"?

The docker compose file should work in the same manner.

Which permissions should I assign to the folder mapped to /home/.local/share/signal-cli?

At the moment the UID/GID is hard coded to 1000 - as it was before. So for security reasons all files should be owned by this user.

As java/signal-cli rely on the ability to lookup a UID by username, changing the UID is tricky. Podman is far better suited to run as non-root. something like:

podman run \
  --passwd \
  --passwd-entry '$USERNAME:x:$UID:$GID:Dynamic User:/home:/bin/bash' \
  --user 1234:1234 \
  your-image

Please report any issue, you stumble upon. I am happy to look into it.

@Qhilm
Copy link
Copy Markdown

Qhilm commented May 3, 2026

In case anyone else tries and runs into my issue, I had to change the docker compose.

I simply swapped the image tag in my docker compose but got this error at first:

signal-api | /package/admin/s6-overlay-3.2.2.0/libexec/preinit: 41: cannot create /run/test of writability: Permission denied
signal-api | s6-overlay-suexec: fatal: child failed with exit code 2
signal-api exited with code 2 (restarting)

first docker compose version
networks:
    docker-vlan1005:
      external: true

services:
  signal-api:
    container_name: signal-api
    image: bbernhard/signal-cli-rest-api:rootless-latest
    environment:
      MODE: native # can only use normal/native with AUTO_RECEIVE_SCHEDULE
      AUTO_RECEIVE_SCHEDULE: 0 22 * * *
      LOG_LEVEL: debug
    volumes:
      # The folder contains the password and cryptographic keys when a new number is registered
      - ${DOCKER_PATH}/services/notifications/signal-api/config:/home/.local/share/signal-cli
    networks:
      docker-vlan1005:
        ipv4_address: ${SIGNAL_API_IPV4}
        ipv6_address: ${SIGNAL_API_IPV6}
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/v1/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s

Then, the friendly AI suggested I had to remove "no-new-privileges:true" and add

tmpfs:
  - /run:exec,size=64m

then it worked.

If I took a wrong turn somewhere, let me know.

Many thanks for this @poggenpower.

@poggenpower
Copy link
Copy Markdown
Author

@Qhilm thank you for sharing your feedback.
I think the removal of:

    security_opt:
      - no-new-privileges:true

you should keep it for security reasons.

tmpfs:
 - /run:exec,size=64m

should be enough to allow s6 to start. Please test and share error message
I need to dig deeper on this. Maybe a chmod on /run while creating the image is fixing this at least if your run as UID 1000.

@poggenpower
Copy link
Copy Markdown
Author

Tuned it a little, this is working for me:

services:
  signal-api:
    container_name: signal-api
    user: "1000:1000"
    image: bbernhard/signal-cli-rest-api:rootless-latest
    environment:
      MODE: native # can only use normal/native with AUTO_RECEIVE_SCHEDULE
      AUTO_RECEIVE_SCHEDULE: 0 22 * * *
      LOG_LEVEL: debug
    volumes:
      # The folder contains the password and cryptographic keys when a new number is registered
      - ./config:/home/.local/share/signal-cli
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    tmpfs:
      - /run:exec,size=64m,uid=1000,gid=1000,mode=0755
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/v1/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s

forced all to UID 1000
added line user: "1000:1000"
and set UID for the tmpfs /run:exec,size=64m,uid=1000,gid=1000,mode=0755

@poggenpower
Copy link
Copy Markdown
Author

Did some further test. It looks like, this it is possible to run with any UID with the compose file above just replace all occurrences.

@Qhilm
Copy link
Copy Markdown

Qhilm commented May 7, 2026

Hello, if I simply add back no-new-privileges:true, I get this error:

signal-api  | /package/admin/s6-overlay/libexec/preinit: info: read-only root
signal-api  | /package/admin/s6-overlay-3.2.2.0/libexec/preinit: 41: cannot create /run/test of writability: Permission denied
signal-api  | s6-overlay-suexec: fatal: child failed with exit code 2
signal-api exited with code 2 (restarting)

but if I add ,uid=1000,gid=1000,mode=0755 behind the - /run:exec,size=64m line as well as user: "1000:1000", then it works. (my docker user is also uid 1000)

Thanks a lot!

poggenpower added a commit to poggenpower/signal-cli-rest-api that referenced this pull request May 7, 2026
from test and conversation in closed PR bbernhard#798
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants