Skip to content

Use a distroless based runtime image #18208

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 39 commits into from

Conversation

AndrewFerr
Copy link
Member

@AndrewFerr AndrewFerr commented Mar 3, 2025

Based on #18039

Pull Request Checklist

  • Pull request is based on the develop branch
  • Pull request includes a changelog file. The entry should:
    • Be a short description of your change which makes sense to users. "Fixed a bug that prevented receiving messages from other servers." instead of "Moved X method from EventStore to EventWorkerStore.".
    • Use markdown where necessary, mostly for code blocks.
    • End with either a period (.) or an exclamation mark (!).
    • Start with a capital letter.
    • Feel free to credit yourself, by adding a sentence "Contributed by @github_username." or "Contributed by [Your Name]." to the end of the entry.
  • Code style is correct
    (run the linters)

@AndrewFerr AndrewFerr requested a review from a team as a code owner March 3, 2025 21:21
Comment on lines -23 to +25
command=/usr/local/bin/prefix-log /usr/local/bin/redis-server --unixsocket /tmp/redis.sock
command=/usr/local/bin/prefix-log /usr/bin/redis-server --unixsocket /tmp/redis.sock
{% else %}
command=/usr/local/bin/prefix-log /usr/local/bin/redis-server
command=/usr/local/bin/prefix-log /usr/bin/redis-server
Copy link
Member Author

Choose a reason for hiding this comment

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

This should have been its own commit to make this fix more clear, but this is needed because of the changes to how Dockerfile-workers installs Redis. This fixes the Complement run for workers+Postgres.

@AndrewFerr
Copy link
Member Author

The failed Complement run in SQLite is because of the application-defined rank SQL function not working:

db_conn.create_function("rank", 1, _rank)

def _rank(raw_match_info: bytes) -> float:
"""Handle match_info called w/default args 'pcx' - based on the example rank
function http://sqlite.org/fts3.html#appendix_a
"""
match_info = _parse_match_info(raw_match_info)
score = 0.0
p, c = match_info[:2]
for phrase_num in range(p):
phrase_info_idx = 2 + (phrase_num * c * 3)
for col_num in range(c):
col_idx = phrase_info_idx + (col_num * 3)
x1, x2 = match_info[col_idx : col_idx + 2]
if x1 > 0:
score += float(x1) / x2
return score

For whatever reason, when queries call that function (as is done by search_rooms in synapse/storage/databases/main/search.py), it appears as if the function was never called. I say this after having added some log lines to be printed in the _rank implementation, which appear when using a non-distroless Synapse image, but not when using a distroless one.

@sandhose
Copy link
Member

sandhose commented Mar 5, 2025

Sadly, this might be because of how sqlite is compiled in the builds we're grabbing…

That particular call uses matchinfo function and MATCH keyword from the FTS3 extension. That one isn't enabled by default:

Edit: nevermind, tried executing a simple FTS query with the Python we have in this image and it works

@sandhose
Copy link
Member

sandhose commented Mar 5, 2025

Alright I figured out the difference!

Using the current image:

$ docker run -it --rm --entrypoint='' matrixdotorg/synapse /usr/local/bin/python3
Python 3.12.9 (main, Feb 25 2025, 08:58:51) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sqlite3
>>> con = sqlite3.connect(":memory:")
>>> con.execute("CREATE VIRTUAL TABLE docs USING fts4(message);").fetchall()
[]
>>> con.execute("INSERT INTO docs VALUES ('Message number 4');").fetchall()
[]
>>> con.execute("SELECT * FROM docs WHERE docs MATCH 'Message AND 4';").fetchall()
[('Message number 4',)]

With the one from this branch:

$ docker run -it --rm --entrypoint='' syn /usr/local/bin/python3
Python 3.12.9 (main, Feb 12 2025, 14:50:54) [Clang 19.1.6 ] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sqlite3
>>> con = sqlite3.connect(":memory:")
>>> con.execute("CREATE VIRTUAL TABLE docs USING fts4(message);").fetchall()
[]
>>> con.execute("INSERT INTO docs VALUES ('Message number 4');").fetchall()
[]
>>> con.execute("SELECT * FROM docs WHERE docs MATCH 'Message AND 4';").fetchall()
[]

Which is ultimately what the test is doing: https://github.com/matrix-org/complement/blob/81c0d9ad96823e92cef2a9d146a23bf32d186126/tests/csapi/apidoc_search_test.go#L93

@sandhose
Copy link
Member

sandhose commented Mar 5, 2025

I've opened an issue on their side: astral-sh/python-build-standalone#550

The Python distribution from uv comes with the SQLite extension compiled
without the `ENABLE_FTS3_PARENTHESIS` flag, which Synapse requires for
its full-text search queries.

So, revert to using the Python from the "python-slim" image, which comes
with SQLite compiled with that flag.
Downstream container images that use the Synapse image as a base will
break due to the change to distroless.
Exclude pip from the runtime image, and don't copy unused binaries &
unused/empty directories.

This also requires to go back to using uv in a build stage to install
supervisor in the workers image.
as it is useless without a shell anyways
Also add a comment explaining why it's used, and install it
Add back zlib1g, and add libs that uv's python builds statically
Don't append an arch to package names that already have an arch
COPYing a symlink resolves the link, so explicitly make links instead
Instead of manually creating links to the python binary in the final
runtime image, copy them from a builder image.
Do this instead of appending package names with ":{arch}", which fails
when downloading architecture-independent packages for a non-host arch.
@AndrewFerr
Copy link
Member Author

The last round of commits should wrap this up, barring further review.

@sandhose
Copy link
Member

@AndrewFerr ftr, I've pushed a patch to python-build-standalone to enable the flag we needed, and it just got merged, so if you want to switch back to the uv-based python install, you could :)

Now that uv's prebuilt Python uses the `ENABLE_FTS3_PARENTHESIS` flag
for its SQLite extension, it is once again usable for Synapse
(see astral-sh/python-build-standalone#550).

Still skip copying unused binaries & directories into the runtime image.
Copy link
Contributor

@reivilibre reivilibre left a comment

Choose a reason for hiding this comment

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

I had some comments but not sure all of them still apply because the history is a little windy and I think some of them got handled since ...

Comment on lines 27 to 41
| grep '^\w' > /tmp/pkg-list && \
mkdir -p /tmp/debs && \
cat /tmp/pkg-list && \
cd /tmp/debs && \
xargs apt-get download </tmp/pkg-list

# Extract the debs for each architecture
RUN \
mkdir -p /install/var/lib/dpkg/status.d/ && \
for deb in /tmp/debs/*.deb; do \
package_name=$(dpkg-deb -I ${deb} | awk '/^ Package: .*$/ {print $2}'); \
echo "Extracting: ${package_name}"; \
dpkg --ctrl-tarfile $deb | tar -Ox ./control > /install/var/lib/dpkg/status.d/${package_name}; \
dpkg --extract $deb /install; \
done;
Copy link
Contributor

Choose a reason for hiding this comment

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

this is not super elegant. Not sure if we can do better though.

Copy link
Member Author

Choose a reason for hiding this comment

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

FWIW the same strategy is done in stage 2 of docker/Dockerfile.

Comment on lines +15 to +35
prefixer = ['mawk', '-W', 'interactive', f'{{print "{os.environ['SUPERVISOR_PROCESS_NAME']} | "$0; fflush() }}']

r_out, w_out = os.pipe()
if os.fork() == 0:
os.close(w_out)
os.dup2(r_out, sys.stdin.fileno())
os.execvp(prefixer[0], prefixer)
os.close(r_out)

r_err, w_err = os.pipe()
if os.fork() == 0:
os.close(w_out)
os.close(w_err)
os.dup2(r_err, sys.stdin.fileno())
os.dup2(sys.stderr.fileno(), sys.stdout.fileno())
os.execvp(prefixer[0], prefixer)
os.close(r_err)

os.dup2(w_out, sys.stdout.fileno())
os.dup2(w_err, sys.stderr.fileno())
os.execvp(sys.argv[1], sys.argv[1:])
Copy link
Contributor

Choose a reason for hiding this comment

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

I have to say I'd prefer to use the shell for this, as this has really suffered in clarity.

I am also not confident in vouching for its correctness here.

Copy link
Member Author

Choose a reason for hiding this comment

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

To help with clarity, I'll add some comments to this.

It's a lot more code than a sh script, but it really just boils down to redirecting output streams.

These changes help with local builds, but aren't needed for distroless
@AndrewFerr
Copy link
Member Author

This PR has grown to be quite large, and after having played with it some more, I realize now that the impacts of switching to distroless could be more than originally anticipated (like breaking downstream image builds & environments that expect more tools to be installed). So, closing this until it's potentially revisited later.

There are some changes in here that aren't strictly related to distroless, though, which I intend to split into other PRs.

@AndrewFerr AndrewFerr closed this Mar 19, 2025
@AndrewFerr AndrewFerr deleted the docker/distroless branch March 19, 2025 20:02
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