From 3913448f2f7069ec52f22da9b1726734e761be33 Mon Sep 17 00:00:00 2001 From: c41ms0n <193478517+c41ms0n@users.noreply.github.com> Date: Fri, 6 Mar 2026 19:28:34 +0200 Subject: [PATCH 1/8] build: switch to Go-based multi-stage build and improve - Use golang:1.26-trixie builder instead of debian:sid - Build proton-bridge from source via version argument/envelopment - Add support for PTY tools (dtach, abduco, reptyr) for interactive sessions - Introduce manage and attach commands for bridge CLI sessions - Improve daemon startup with port readiness checks - Add HEALTHCHECK and configurable CMD/ENTRYPOINT - Harden entrypoint with strict bash flags and better error handling - Install additional runtime deps (libfido2, procps) and optional PTY tools --- build/Dockerfile | 43 ++++++--- build/entrypoint.sh | 206 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 211 insertions(+), 38 deletions(-) diff --git a/build/Dockerfile b/build/Dockerfile index e90ff25..a97b93e 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,33 +1,52 @@ -# The build image could be golang, but it currently does not support riscv64. Only debian:sid does, at the time of writing. -FROM debian:sid-slim AS build +### The Deb install is just a repack of the official ProtonMail Bridge deb package with less dependencies. +### I recommend you don't use this. It's here for legacy reasons. + +FROM golang:1.26-trixie AS build ARG version +ENV version=${version} -# Install dependencies -RUN apt-get update && apt-get install -y golang build-essential libsecret-1-dev + +RUN apt-get update && apt-get install -y build-essential libsecret-1-dev libfido2-dev libcbor-dev # Build ADD https://github.com/ProtonMail/proton-bridge.git#${version} /build/ WORKDIR /build/ RUN make build-nogui vault-editor -FROM debian:sid-slim +# ----------------------------------------------------------------------------- + +FROM debian:trixie-slim LABEL maintainer="Simon Felding " +# Select PTY tool for manage/attach commands: dtach (default), abduco, reptyr +ARG PTY_TOOL=dtach +ENV PTY_TOOL=${PTY_TOOL} + EXPOSE 25/tcp EXPOSE 143/tcp -# Install dependencies and protonmail bridge -RUN apt-get update \ - && apt-get install -y --no-install-recommends socat pass libsecret-1-0 ca-certificates \ - && rm -rf /var/lib/apt/lists/* +WORKDIR /protonmail -# Copy bash scripts COPY gpgparams entrypoint.sh /protonmail/ - # Copy protonmail COPY --from=build /build/bridge /protonmail/ COPY --from=build /build/proton-bridge /protonmail/ COPY --from=build /build/vault-editor /protonmail/ -ENTRYPOINT ["bash", "/protonmail/entrypoint.sh"] +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + socat pass libsecret-1-0 libfido2-1 ca-certificates procps \ + && case "${PTY_TOOL}" in \ + dtach) apt-get install -y --no-install-recommends dtach ;; \ + abduco) apt-get install -y --no-install-recommends abduco ;; \ + reptyr) apt-get install -y --no-install-recommends reptyr ;; \ + esac \ + && chmod +x /protonmail/entrypoint.sh \ + && rm -rf /var/lib/apt/lists/* + +HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=120s \ + CMD /bin/bash -c "true < /dev/tcp/localhost/25" + +ENTRYPOINT ["/protonmail/entrypoint.sh"] +CMD ["run"] diff --git a/build/entrypoint.sh b/build/entrypoint.sh index 1931087..bfb422a 100644 --- a/build/entrypoint.sh +++ b/build/entrypoint.sh @@ -1,35 +1,189 @@ #!/bin/bash -set -ex +set -euo pipefail -# Initialize -if [[ $1 == init ]]; then +PTY_TOOL="${PTY_TOOL:-dtach}" +BRIDGE_SOCK=/protonmail/bridge.sock +BRIDGE_PID_FILE=/protonmail/bridge.pid - # Initialize pass - gpg --generate-key --batch /protonmail/gpgparams - pass init pass-key - - # Kill the other instance as only one can be running at a time. - # This allows users to run entrypoint init inside a running conainter - # which is useful in a k8s environment. - # || true to make sure this would not fail in case there is no running instance. - pkill protonmail-bridge || true +# Clean stale gpg-agent sockets left from a previous run +rm -f /root/.gnupg/S.gpg-agent* 2>/dev/null || true - # Login - /protonmail/proton-bridge --cli $@ +# --- PTY helpers (only used by: init, manage, attach) --- -else +pty_start() { + case "${PTY_TOOL}" in + dtach) dtach -n "${BRIDGE_SOCK}" "$@" ;; + abduco) abduco -n bridge "$@" ;; + # reptyr re-attaches existing PIDs; use nohup+setsid to launch headlessly instead + reptyr) setsid "$@" /dev/null & echo $! > "${BRIDGE_PID_FILE}" ;; + esac +} - # socat will make the conn appear to come from 127.0.0.1 - # ProtonMail Bridge currently expects that. - # It also allows us to bind to the real ports :) - socat TCP-LISTEN:25,fork TCP:127.0.0.1:1025 & - socat TCP-LISTEN:143,fork TCP:127.0.0.1:1143 & +pty_attach() { + case "${PTY_TOOL}" in + dtach) exec dtach -a "${BRIDGE_SOCK}" -e '^\' ;; + abduco) exec abduco -a bridge ;; + reptyr) exec reptyr "$(cat "${BRIDGE_PID_FILE}")" ;; + esac +} - # Start protonmail - # Fake a terminal, so it does not quit because of EOF... - rm -f faketty - mkfifo faketty - cat faketty | /protonmail/proton-bridge --cli $@ +detach_hint() { + case "${PTY_TOOL}" in + dtach|abduco) echo "Ctrl+\\" ;; + reptyr) echo "Ctrl+C" ;; + esac +} -fi +# Wait up to $1 seconds for the bridge socket (or PID file) to appear +wait_for_session() { + local timeout="${1:-10}" + local elapsed=0 + while [[ "${elapsed}" -lt "${timeout}" ]]; do + case "${PTY_TOOL}" in + dtach|abduco) [[ -S "${BRIDGE_SOCK}" ]] && return 0 ;; + reptyr) [[ -f "${BRIDGE_PID_FILE}" ]] && return 0 ;; + esac + sleep 1 + (( elapsed++ )) || true + done + echo "ERROR: bridge session did not start within ${timeout}s." >&2 + return 1 +} + +# --- Commands --- + +CMD="${1:-run}" + +case "${CMD}" in + + init) + # One-time setup: generate GPG key, init password store, interactive login. + # Run as: docker run -it init + gpg --generate-key --batch /protonmail/gpgparams + pass init pass-key + exec /protonmail/proton-bridge --cli + ;; + + manage) + # Open an interactive --cli session for account management (add/remove accounts etc). + # Run as: docker run -it --rm -v manage + # NOTE: Stop the running daemon container first to avoid port/lock conflicts. + CONTAINER_ID=$(hostname) + echo " Starting management session... [PTY_TOOL=${PTY_TOOL}]" + pty_start /protonmail/proton-bridge --cli + + # Wait for the session socket/pid to appear before printing attach instructions + wait_for_session 10 + + echo " Management session ready." + echo " Attach: docker exec -it ${CONTAINER_ID} /protonmail/entrypoint.sh attach" + echo " Detach: $(detach_hint)" + + # Block so the container stays alive for `docker exec attach`. + # If stdin is a tty (docker run -it), jump straight into the session. + if [[ -t 0 ]]; then + pty_attach + else + # No tty: wait until the bridge session disappears then exit cleanly. + while true; do + case "${PTY_TOOL}" in + dtach|abduco) [[ -S "${BRIDGE_SOCK}" ]] || break ;; + reptyr) kill -0 "$(cat "${BRIDGE_PID_FILE}" 2>/dev/null)" 2>/dev/null || break ;; + esac + sleep 2 + done + fi + ;; + + attach) + # Reattach to a running manage session. + case "${PTY_TOOL}" in + dtach|abduco) + if [[ ! -S "${BRIDGE_SOCK}" ]]; then + echo "ERROR: No active session found (${BRIDGE_SOCK} does not exist)." >&2 + echo " Start one first: docker exec -it \$(hostname) /protonmail/entrypoint.sh manage" >&2 + exit 1 + fi + ;; + reptyr) + if [[ ! -f "${BRIDGE_PID_FILE}" ]]; then + echo "ERROR: No active session found (${BRIDGE_PID_FILE} does not exist)." >&2 + echo " Start one first: docker exec -it \$(hostname) /protonmail/entrypoint.sh manage" >&2 + exit 1 + fi + ;; + esac + pty_attach + ;; + + run) + # Daemon mode: --noninteractive runs headless, output goes directly to docker logs. + CONTAINER_ID=$(hostname) + echo "========================================" + echo " ProtonMail Bridge daemon starting..." + echo " Container: ${CONTAINER_ID}" + echo "" + echo " Available commands:" + echo " First-time setup:" + echo " docker run -it init" + echo "" + echo " Manage accounts (stop daemon first):" + echo " docker run -it --rm -v manage" + echo "" + echo " Attach to a running manage session:" + echo " docker exec -it ${CONTAINER_ID} /protonmail/entrypoint.sh attach" + echo "" + echo " View logs:" + echo " docker logs -f ${CONTAINER_ID}" + echo "========================================" + + # Start bridge in background so we can wait for it to bind its ports + # before socat begins accepting connections. + /protonmail/proton-bridge --noninteractive & + BRIDGE_PID=$! + + # Wait for bridge to open its local SMTP and IMAP ports (up to 60s) + echo " Waiting for bridge ports 1025/1143..." + for port in 1025 1143; do + elapsed=0 + until socat -u OPEN:/dev/null TCP:127.0.0.1:${port} 2>/dev/null; do + sleep 1 + (( elapsed++ )) || true + if [[ "${elapsed}" -ge 60 ]]; then + echo "ERROR: bridge port ${port} did not open within 60s." >&2 + kill "${BRIDGE_PID}" 2>/dev/null || true + exit 1 + fi + done + echo " Port ${port} ready." + done + + # socat forwards standard ports to bridge's localhost-only listener ports. + # retry=30,interval=2 handles transient bridge restarts without dropping connections. + socat TCP-LISTEN:25,fork,reuseaddr TCP:127.0.0.1:1025,nodelay,retry=30,interval=2 & + SOCAT_SMTP_PID=$! + socat TCP-LISTEN:143,fork,reuseaddr TCP:127.0.0.1:1143,nodelay,retry=30,interval=2 & + SOCAT_IMAP_PID=$! + + # Verify both socat processes started + sleep 1 + for pid in "${SOCAT_SMTP_PID}" "${SOCAT_IMAP_PID}"; do + if ! kill -0 "${pid}" 2>/dev/null; then + echo "ERROR: socat port-forward (pid ${pid}) failed to start." >&2 + kill "${BRIDGE_PID}" 2>/dev/null || true + exit 1 + fi + done + + # Wait on bridge; if it exits, bring down socat too. + wait "${BRIDGE_PID}" + kill "${SOCAT_SMTP_PID}" "${SOCAT_IMAP_PID}" 2>/dev/null || true + ;; + + *) + echo "Usage: entrypoint.sh [init|manage|attach|run]" >&2 + exit 1 + ;; + +esac From 05a0388dd6d64e4e083bb087a83017aa2e7ed0d6 Mon Sep 17 00:00:00 2001 From: c41ms0n <193478517+c41ms0n@users.noreply.github.com> Date: Thu, 16 Apr 2026 03:07:20 +0300 Subject: [PATCH 2/8] build: add reptyr support and validation to PTY_TOOL Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- build/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/build/Dockerfile b/build/Dockerfile index a97b93e..60917e4 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -41,6 +41,7 @@ RUN apt-get update \ dtach) apt-get install -y --no-install-recommends dtach ;; \ abduco) apt-get install -y --no-install-recommends abduco ;; \ reptyr) apt-get install -y --no-install-recommends reptyr ;; \ + *) echo "Unsupported PTY_TOOL: ${PTY_TOOL}. Supported values are: dtach, abduco, reptyr." >&2; exit 1 ;; \ esac \ && chmod +x /protonmail/entrypoint.sh \ && rm -rf /var/lib/apt/lists/* From 7516b3b23422ac98826e38809cde9551d64651bf Mon Sep 17 00:00:00 2001 From: c41ms0n <193478517+c41ms0n@users.noreply.github.com> Date: Thu, 16 Apr 2026 03:08:50 +0300 Subject: [PATCH 3/8] chore: update TCP healthcheck for multiple mail services Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- build/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Dockerfile b/build/Dockerfile index 60917e4..7e7ef90 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -47,7 +47,7 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=120s \ - CMD /bin/bash -c "true < /dev/tcp/localhost/25" + CMD /bin/bash -c "true < /dev/tcp/localhost/25 && true < /dev/tcp/localhost/143 && true < /dev/tcp/localhost/1025 && true < /dev/tcp/localhost/1143" ENTRYPOINT ["/protonmail/entrypoint.sh"] CMD ["run"] From cb69c79ec1569656b5a1ab2b17bd51807f2f2065 Mon Sep 17 00:00:00 2001 From: c41ms0n <193478517+c41ms0n@users.noreply.github.com> Date: Thu, 16 Apr 2026 03:26:37 +0300 Subject: [PATCH 4/8] fix(entrypoint): correct abduco session detection and harden PTY_TOOL handling - Validate PTY_TOOL at startup and verify the selected binary exists - Add abduco_session_alive() using `abduco -l` instead of checking BRIDGE_SOCK (abduco does not create a socket file, so the old check always failed) - Fix wait_for_session, manage blocking loop, and attach to use tool-specific liveness checks rather than grouping dtach and abduco together - Add default (*) error branches to all PTY_TOOL case statements - Check kill -0 BRIDGE_PID inside the port readiness loop to fail fast if the bridge exits before its ports are ready - Add SIGTERM/SIGINT trap with a cleanup() function in run mode so Docker stop properly reaps bridge and socat children instead of waiting for the kill timeout - Replace \$(hostname) with placeholder in user-facing error messages --- build/entrypoint.sh | 88 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 15 deletions(-) diff --git a/build/entrypoint.sh b/build/entrypoint.sh index bfb422a..43a3cf5 100644 --- a/build/entrypoint.sh +++ b/build/entrypoint.sh @@ -6,6 +6,20 @@ PTY_TOOL="${PTY_TOOL:-dtach}" BRIDGE_SOCK=/protonmail/bridge.sock BRIDGE_PID_FILE=/protonmail/bridge.pid +# Validate PTY_TOOL early and ensure the selected binary is present. +case "${PTY_TOOL}" in + dtach|abduco|reptyr) + if ! command -v "${PTY_TOOL}" &>/dev/null; then + echo "ERROR: PTY_TOOL=${PTY_TOOL} but '${PTY_TOOL}' was not found in PATH." >&2 + exit 1 + fi + ;; + *) + echo "ERROR: PTY_TOOL=${PTY_TOOL} is not supported. Valid values: dtach, abduco, reptyr." >&2 + exit 1 + ;; +esac + # Clean stale gpg-agent sockets left from a previous run rm -f /root/.gnupg/S.gpg-agent* 2>/dev/null || true @@ -17,6 +31,7 @@ pty_start() { abduco) abduco -n bridge "$@" ;; # reptyr re-attaches existing PIDs; use nohup+setsid to launch headlessly instead reptyr) setsid "$@" /dev/null & echo $! > "${BRIDGE_PID_FILE}" ;; + *) echo "ERROR: pty_start: unsupported PTY_TOOL=${PTY_TOOL}" >&2; exit 1 ;; esac } @@ -25,6 +40,7 @@ pty_attach() { dtach) exec dtach -a "${BRIDGE_SOCK}" -e '^\' ;; abduco) exec abduco -a bridge ;; reptyr) exec reptyr "$(cat "${BRIDGE_PID_FILE}")" ;; + *) echo "ERROR: pty_attach: unsupported PTY_TOOL=${PTY_TOOL}" >&2; exit 1 ;; esac } @@ -32,17 +48,25 @@ detach_hint() { case "${PTY_TOOL}" in dtach|abduco) echo "Ctrl+\\" ;; reptyr) echo "Ctrl+C" ;; + *) echo "(unknown)" ;; esac } -# Wait up to $1 seconds for the bridge socket (or PID file) to appear +# True if the abduco session named 'bridge' is listed as running. +abduco_session_alive() { + abduco -l 2>/dev/null | grep -qw 'bridge' +} + +# Wait up to $1 seconds for the bridge session (socket or PID file) to appear. wait_for_session() { local timeout="${1:-10}" local elapsed=0 while [[ "${elapsed}" -lt "${timeout}" ]]; do case "${PTY_TOOL}" in - dtach|abduco) [[ -S "${BRIDGE_SOCK}" ]] && return 0 ;; - reptyr) [[ -f "${BRIDGE_PID_FILE}" ]] && return 0 ;; + dtach) [[ -S "${BRIDGE_SOCK}" ]] && return 0 ;; + abduco) abduco_session_alive && return 0 ;; + reptyr) [[ -f "${BRIDGE_PID_FILE}" ]] && return 0 ;; + *) echo "ERROR: wait_for_session: unsupported PTY_TOOL=${PTY_TOOL}" >&2; exit 1 ;; esac sleep 1 (( elapsed++ )) || true @@ -88,8 +112,10 @@ case "${CMD}" in # No tty: wait until the bridge session disappears then exit cleanly. while true; do case "${PTY_TOOL}" in - dtach|abduco) [[ -S "${BRIDGE_SOCK}" ]] || break ;; - reptyr) kill -0 "$(cat "${BRIDGE_PID_FILE}" 2>/dev/null)" 2>/dev/null || break ;; + dtach) [[ -S "${BRIDGE_SOCK}" ]] || break ;; + abduco) abduco_session_alive || break ;; + reptyr) kill -0 "$(cat "${BRIDGE_PID_FILE}" 2>/dev/null)" 2>/dev/null || break ;; + *) echo "ERROR: manage loop: unsupported PTY_TOOL=${PTY_TOOL}" >&2; exit 1 ;; esac sleep 2 done @@ -99,20 +125,31 @@ case "${CMD}" in attach) # Reattach to a running manage session. case "${PTY_TOOL}" in - dtach|abduco) + dtach) if [[ ! -S "${BRIDGE_SOCK}" ]]; then - echo "ERROR: No active session found (${BRIDGE_SOCK} does not exist)." >&2 - echo " Start one first: docker exec -it \$(hostname) /protonmail/entrypoint.sh manage" >&2 + echo "ERROR: No active dtach session found (${BRIDGE_SOCK} does not exist)." >&2 + echo " Start one first: docker exec -it /protonmail/entrypoint.sh manage" >&2 + exit 1 + fi + ;; + abduco) + if ! abduco_session_alive; then + echo "ERROR: No active abduco session 'bridge' found." >&2 + echo " Start one first: docker exec -it /protonmail/entrypoint.sh manage" >&2 exit 1 fi ;; reptyr) if [[ ! -f "${BRIDGE_PID_FILE}" ]]; then echo "ERROR: No active session found (${BRIDGE_PID_FILE} does not exist)." >&2 - echo " Start one first: docker exec -it \$(hostname) /protonmail/entrypoint.sh manage" >&2 + echo " Start one first: docker exec -it /protonmail/entrypoint.sh manage" >&2 exit 1 fi ;; + *) + echo "ERROR: attach: unsupported PTY_TOOL=${PTY_TOOL}" >&2 + exit 1 + ;; esac pty_attach ;; @@ -120,6 +157,25 @@ case "${CMD}" in run) # Daemon mode: --noninteractive runs headless, output goes directly to docker logs. CONTAINER_ID=$(hostname) + + # Cleanup handler: forward SIGTERM/SIGINT and reap child processes cleanly. + # As PID 1, without this Docker's SIGTERM would leave children running until + # the kill timeout expires. + BRIDGE_PID= + SOCAT_SMTP_PID= + SOCAT_IMAP_PID= + _cleanup_done=0 + cleanup() { + [[ "${_cleanup_done}" -eq 1 ]] && return + _cleanup_done=1 + echo " Shutting down bridge and port-forwards..." >&2 + [[ -n "${BRIDGE_PID:-}" ]] && kill "${BRIDGE_PID}" 2>/dev/null || true + [[ -n "${SOCAT_SMTP_PID:-}" ]] && kill "${SOCAT_SMTP_PID}" 2>/dev/null || true + [[ -n "${SOCAT_IMAP_PID:-}" ]] && kill "${SOCAT_IMAP_PID}" 2>/dev/null || true + wait 2>/dev/null || true + } + trap cleanup EXIT SIGTERM SIGINT + echo "========================================" echo " ProtonMail Bridge daemon starting..." echo " Container: ${CONTAINER_ID}" @@ -132,7 +188,7 @@ case "${CMD}" in echo " docker run -it --rm -v manage" echo "" echo " Attach to a running manage session:" - echo " docker exec -it ${CONTAINER_ID} /protonmail/entrypoint.sh attach" + echo " docker exec -it /protonmail/entrypoint.sh attach" echo "" echo " View logs:" echo " docker logs -f ${CONTAINER_ID}" @@ -143,16 +199,20 @@ case "${CMD}" in /protonmail/proton-bridge --noninteractive & BRIDGE_PID=$! - # Wait for bridge to open its local SMTP and IMAP ports (up to 60s) + # Wait for bridge to open its local SMTP and IMAP ports (up to 60s). + # Abort immediately if the bridge process exits before the port is ready. echo " Waiting for bridge ports 1025/1143..." for port in 1025 1143; do elapsed=0 until socat -u OPEN:/dev/null TCP:127.0.0.1:${port} 2>/dev/null; do + if ! kill -0 "${BRIDGE_PID}" 2>/dev/null; then + echo "ERROR: bridge process (pid ${BRIDGE_PID}) exited before port ${port} became ready." >&2 + exit 1 + fi sleep 1 (( elapsed++ )) || true if [[ "${elapsed}" -ge 60 ]]; then echo "ERROR: bridge port ${port} did not open within 60s." >&2 - kill "${BRIDGE_PID}" 2>/dev/null || true exit 1 fi done @@ -171,14 +231,12 @@ case "${CMD}" in for pid in "${SOCAT_SMTP_PID}" "${SOCAT_IMAP_PID}"; do if ! kill -0 "${pid}" 2>/dev/null; then echo "ERROR: socat port-forward (pid ${pid}) failed to start." >&2 - kill "${BRIDGE_PID}" 2>/dev/null || true exit 1 fi done - # Wait on bridge; if it exits, bring down socat too. + # Wait on bridge; EXIT trap will bring down socat when bridge exits. wait "${BRIDGE_PID}" - kill "${SOCAT_SMTP_PID}" "${SOCAT_IMAP_PID}" 2>/dev/null || true ;; *) From 8c6e7b301a245dca565167d88fc45b91e399ce21 Mon Sep 17 00:00:00 2001 From: c41ms0n <193478517+c41ms0n@users.noreply.github.com> Date: Thu, 16 Apr 2026 03:35:03 +0300 Subject: [PATCH 5/8] chore(deb): sync entrypoint and Dockerfile from build/ - Replace legacy entrypoint with the current build/ version (PTY_TOOL support, init/manage/attach/run commands, SIGTERM trap, port-readiness liveness check, abduco session detection, placeholder in error messages); only difference is binary name protonmail-bridge instead of /protonmail/proton-bridge - Add ARG/ENV PTY_TOOL and conditional apt install of dtach/abduco/reptyr - Add HEALTHCHECK on 127.0.0.1 for all four ports (25, 143, 1025, 1143) - Switch from CMD bash ... to ENTRYPOINT + CMD ["run"] --- deb/Dockerfile | 17 ++- deb/entrypoint.sh | 276 +++++++++++++++++++++++++++++++++++++++------- 2 files changed, 253 insertions(+), 40 deletions(-) diff --git a/deb/Dockerfile b/deb/Dockerfile index a0d8e2c..26647f1 100644 --- a/deb/Dockerfile +++ b/deb/Dockerfile @@ -12,6 +12,10 @@ RUN bash /install.sh FROM debian:sid-slim LABEL maintainer="Simon Felding " +# Select PTY tool for manage/attach commands: dtach (default), abduco, reptyr +ARG PTY_TOOL=dtach +ENV PTY_TOOL=${PTY_TOOL} + EXPOSE 25/tcp EXPOSE 143/tcp @@ -23,6 +27,17 @@ COPY --from=build /protonmail.deb /tmp/protonmail.deb RUN apt-get update \ && apt-get install -y --no-install-recommends /tmp/protonmail.deb socat pass libsecret-1-0 ca-certificates procps \ + && case "${PTY_TOOL}" in \ + dtach) apt-get install -y --no-install-recommends dtach ;; \ + abduco) apt-get install -y --no-install-recommends abduco ;; \ + reptyr) apt-get install -y --no-install-recommends reptyr ;; \ + *) echo "Unsupported PTY_TOOL: ${PTY_TOOL}. Supported values are: dtach, abduco, reptyr." >&2; exit 1 ;; \ + esac \ + && chmod +x /protonmail/entrypoint.sh \ && rm -rf /var/lib/apt/lists/* -CMD ["bash", "/protonmail/entrypoint.sh"] +HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=120s \ + CMD /bin/bash -c "true < /dev/tcp/127.0.0.1/25 && true < /dev/tcp/127.0.0.1/143 && true < /dev/tcp/127.0.0.1/1025 && true < /dev/tcp/127.0.0.1/1143" + +ENTRYPOINT ["/protonmail/entrypoint.sh"] +CMD ["run"] diff --git a/deb/entrypoint.sh b/deb/entrypoint.sh index 13637e5..3fa5500 100644 --- a/deb/entrypoint.sh +++ b/deb/entrypoint.sh @@ -1,49 +1,247 @@ #!/bin/bash -set -ex +set -euo pipefail -# Initialize -if [[ $1 == init ]]; then +PTY_TOOL="${PTY_TOOL:-dtach}" +BRIDGE_SOCK=/protonmail/bridge.sock +BRIDGE_PID_FILE=/protonmail/bridge.pid - # # Parse parameters - # TFP="" # Default empty two factor passcode - # shift # skip `init` - # while [[ $# -gt 0 ]]; do - # key="$1" - # case $key in - # -u|--username) - # USERNAME="$2" - # ;; - # -p|--password) - # PASSWORD="$2" - # ;; - # -t|--twofactor) - # TWOFACTOR="$2" - # ;; - # esac - # shift - # shift - # done +# Validate PTY_TOOL early and ensure the selected binary is present. +case "${PTY_TOOL}" in + dtach|abduco|reptyr) + if ! command -v "${PTY_TOOL}" &>/dev/null; then + echo "ERROR: PTY_TOOL=${PTY_TOOL} but '${PTY_TOOL}' was not found in PATH." >&2 + exit 1 + fi + ;; + *) + echo "ERROR: PTY_TOOL=${PTY_TOOL} is not supported. Valid values: dtach, abduco, reptyr." >&2 + exit 1 + ;; +esac - # Initialize pass - gpg --generate-key --batch /protonmail/gpgparams - pass init pass-key +# Clean stale gpg-agent sockets left from a previous run +rm -f /root/.gnupg/S.gpg-agent* 2>/dev/null || true - # Login - protonmail-bridge --cli +# --- PTY helpers (only used by: init, manage, attach) --- -else +pty_start() { + case "${PTY_TOOL}" in + dtach) dtach -n "${BRIDGE_SOCK}" "$@" ;; + abduco) abduco -n bridge "$@" ;; + # reptyr re-attaches existing PIDs; use nohup+setsid to launch headlessly instead + reptyr) setsid "$@" /dev/null & echo $! > "${BRIDGE_PID_FILE}" ;; + *) echo "ERROR: pty_start: unsupported PTY_TOOL=${PTY_TOOL}" >&2; exit 1 ;; + esac +} - # socat will make the conn appear to come from 127.0.0.1 - # ProtonMail Bridge currently expects that. - # It also allows us to bind to the real ports :) - socat TCP-LISTEN:25,fork TCP:127.0.0.1:1025 & - socat TCP-LISTEN:143,fork TCP:127.0.0.1:1143 & +pty_attach() { + case "${PTY_TOOL}" in + dtach) exec dtach -a "${BRIDGE_SOCK}" -e '^\' ;; + abduco) exec abduco -a bridge ;; + reptyr) exec reptyr "$(cat "${BRIDGE_PID_FILE}")" ;; + *) echo "ERROR: pty_attach: unsupported PTY_TOOL=${PTY_TOOL}" >&2; exit 1 ;; + esac +} - # Start protonmail - # Fake a terminal, so it does not quit because of EOF... - rm -f faketty - mkfifo faketty - cat faketty | protonmail-bridge --cli +detach_hint() { + case "${PTY_TOOL}" in + dtach|abduco) echo "Ctrl+\\" ;; + reptyr) echo "Ctrl+C" ;; + *) echo "(unknown)" ;; + esac +} -fi +# True if the abduco session named 'bridge' is listed as running. +abduco_session_alive() { + abduco -l 2>/dev/null | grep -qw 'bridge' +} + +# Wait up to $1 seconds for the bridge session (socket or PID file) to appear. +wait_for_session() { + local timeout="${1:-10}" + local elapsed=0 + while [[ "${elapsed}" -lt "${timeout}" ]]; do + case "${PTY_TOOL}" in + dtach) [[ -S "${BRIDGE_SOCK}" ]] && return 0 ;; + abduco) abduco_session_alive && return 0 ;; + reptyr) [[ -f "${BRIDGE_PID_FILE}" ]] && return 0 ;; + *) echo "ERROR: wait_for_session: unsupported PTY_TOOL=${PTY_TOOL}" >&2; exit 1 ;; + esac + sleep 1 + (( elapsed++ )) || true + done + echo "ERROR: bridge session did not start within ${timeout}s." >&2 + return 1 +} + +# --- Commands --- + +CMD="${1:-run}" + +case "${CMD}" in + + init) + # One-time setup: generate GPG key, init password store, interactive login. + # Run as: docker run -it init + gpg --generate-key --batch /protonmail/gpgparams + pass init pass-key + exec protonmail-bridge --cli + ;; + + manage) + # Open an interactive --cli session for account management (add/remove accounts etc). + # Run as: docker run -it --rm -v manage + # NOTE: Stop the running daemon container first to avoid port/lock conflicts. + CONTAINER_ID=$(hostname) + echo " Starting management session... [PTY_TOOL=${PTY_TOOL}]" + pty_start protonmail-bridge --cli + + # Wait for the session socket/pid to appear before printing attach instructions + wait_for_session 10 + + echo " Management session ready." + echo " Attach: docker exec -it ${CONTAINER_ID} /protonmail/entrypoint.sh attach" + echo " Detach: $(detach_hint)" + + # Block so the container stays alive for `docker exec attach`. + # If stdin is a tty (docker run -it), jump straight into the session. + if [[ -t 0 ]]; then + pty_attach + else + # No tty: wait until the bridge session disappears then exit cleanly. + while true; do + case "${PTY_TOOL}" in + dtach) [[ -S "${BRIDGE_SOCK}" ]] || break ;; + abduco) abduco_session_alive || break ;; + reptyr) kill -0 "$(cat "${BRIDGE_PID_FILE}" 2>/dev/null)" 2>/dev/null || break ;; + *) echo "ERROR: manage loop: unsupported PTY_TOOL=${PTY_TOOL}" >&2; exit 1 ;; + esac + sleep 2 + done + fi + ;; + + attach) + # Reattach to a running manage session. + case "${PTY_TOOL}" in + dtach) + if [[ ! -S "${BRIDGE_SOCK}" ]]; then + echo "ERROR: No active dtach session found (${BRIDGE_SOCK} does not exist)." >&2 + echo " Start one first: docker exec -it /protonmail/entrypoint.sh manage" >&2 + exit 1 + fi + ;; + abduco) + if ! abduco_session_alive; then + echo "ERROR: No active abduco session 'bridge' found." >&2 + echo " Start one first: docker exec -it /protonmail/entrypoint.sh manage" >&2 + exit 1 + fi + ;; + reptyr) + if [[ ! -f "${BRIDGE_PID_FILE}" ]]; then + echo "ERROR: No active session found (${BRIDGE_PID_FILE} does not exist)." >&2 + echo " Start one first: docker exec -it /protonmail/entrypoint.sh manage" >&2 + exit 1 + fi + ;; + *) + echo "ERROR: attach: unsupported PTY_TOOL=${PTY_TOOL}" >&2 + exit 1 + ;; + esac + pty_attach + ;; + + run) + # Daemon mode: --noninteractive runs headless, output goes directly to docker logs. + CONTAINER_ID=$(hostname) + + # Cleanup handler: forward SIGTERM/SIGINT and reap child processes cleanly. + # As PID 1, without this Docker's SIGTERM would leave children running until + # the kill timeout expires. + BRIDGE_PID= + SOCAT_SMTP_PID= + SOCAT_IMAP_PID= + _cleanup_done=0 + cleanup() { + [[ "${_cleanup_done}" -eq 1 ]] && return + _cleanup_done=1 + echo " Shutting down bridge and port-forwards..." >&2 + [[ -n "${BRIDGE_PID:-}" ]] && kill "${BRIDGE_PID}" 2>/dev/null || true + [[ -n "${SOCAT_SMTP_PID:-}" ]] && kill "${SOCAT_SMTP_PID}" 2>/dev/null || true + [[ -n "${SOCAT_IMAP_PID:-}" ]] && kill "${SOCAT_IMAP_PID}" 2>/dev/null || true + wait 2>/dev/null || true + } + trap cleanup EXIT SIGTERM SIGINT + + echo "========================================" + echo " ProtonMail Bridge daemon starting..." + echo " Container: ${CONTAINER_ID}" + echo "" + echo " Available commands:" + echo " First-time setup:" + echo " docker run -it init" + echo "" + echo " Manage accounts (stop daemon first):" + echo " docker run -it --rm -v manage" + echo "" + echo " Attach to a running manage session:" + echo " docker exec -it /protonmail/entrypoint.sh attach" + echo "" + echo " View logs:" + echo " docker logs -f ${CONTAINER_ID}" + echo "========================================" + + # Start bridge in background so we can wait for it to bind its ports + # before socat begins accepting connections. + protonmail-bridge --noninteractive & + BRIDGE_PID=$! + + # Wait for bridge to open its local SMTP and IMAP ports (up to 60s). + # Abort immediately if the bridge process exits before the port is ready. + echo " Waiting for bridge ports 1025/1143..." + for port in 1025 1143; do + elapsed=0 + until socat -u OPEN:/dev/null TCP:127.0.0.1:${port} 2>/dev/null; do + if ! kill -0 "${BRIDGE_PID}" 2>/dev/null; then + echo "ERROR: bridge process (pid ${BRIDGE_PID}) exited before port ${port} became ready." >&2 + exit 1 + fi + sleep 1 + (( elapsed++ )) || true + if [[ "${elapsed}" -ge 60 ]]; then + echo "ERROR: bridge port ${port} did not open within 60s." >&2 + exit 1 + fi + done + echo " Port ${port} ready." + done + + # socat forwards standard ports to bridge's localhost-only listener ports. + # retry=30,interval=2 handles transient bridge restarts without dropping connections. + socat TCP-LISTEN:25,fork,reuseaddr TCP:127.0.0.1:1025,nodelay,retry=30,interval=2 & + SOCAT_SMTP_PID=$! + socat TCP-LISTEN:143,fork,reuseaddr TCP:127.0.0.1:1143,nodelay,retry=30,interval=2 & + SOCAT_IMAP_PID=$! + + # Verify both socat processes started + sleep 1 + for pid in "${SOCAT_SMTP_PID}" "${SOCAT_IMAP_PID}"; do + if ! kill -0 "${pid}" 2>/dev/null; then + echo "ERROR: socat port-forward (pid ${pid}) failed to start." >&2 + exit 1 + fi + done + + # Wait on bridge; EXIT trap will bring down socat when bridge exits. + wait "${BRIDGE_PID}" + ;; + + *) + echo "Usage: entrypoint.sh [init|manage|attach|run]" >&2 + exit 1 + ;; + +esac From ba65344ec171a8c6585a27267f954f863bdf5d87 Mon Sep 17 00:00:00 2001 From: c41ms0n <193478517+c41ms0n@users.noreply.github.com> Date: Thu, 16 Apr 2026 03:37:04 +0300 Subject: [PATCH 6/8] fix(deb): add libfido2-1 runtime dependency install.sh strips the official deb Depends line, removing libfido2-1. The bridge binary still links against it, so it must be installed explicitly. --- deb/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deb/Dockerfile b/deb/Dockerfile index 26647f1..8aa81f6 100644 --- a/deb/Dockerfile +++ b/deb/Dockerfile @@ -26,7 +26,7 @@ COPY gpgparams entrypoint.sh PACKAGE /protonmail/ COPY --from=build /protonmail.deb /tmp/protonmail.deb RUN apt-get update \ - && apt-get install -y --no-install-recommends /tmp/protonmail.deb socat pass libsecret-1-0 ca-certificates procps \ + && apt-get install -y --no-install-recommends /tmp/protonmail.deb socat pass libsecret-1-0 libfido2-1 ca-certificates procps \ && case "${PTY_TOOL}" in \ dtach) apt-get install -y --no-install-recommends dtach ;; \ abduco) apt-get install -y --no-install-recommends abduco ;; \ @@ -37,7 +37,7 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=120s \ - CMD /bin/bash -c "true < /dev/tcp/127.0.0.1/25 && true < /dev/tcp/127.0.0.1/143 && true < /dev/tcp/127.0.0.1/1025 && true < /dev/tcp/127.0.0.1/1143" + CMD /bin/bash -c "true < /dev/tcp/localhost/25 && true < /dev/tcp/localhost/143 && true < /dev/tcp/localhost/1025 && true < /dev/tcp/localhost/1143" ENTRYPOINT ["/protonmail/entrypoint.sh"] CMD ["run"] From dad7066244d82f5d8b58298772fc244307961838 Mon Sep 17 00:00:00 2001 From: c41ms0n <193478517+c41ms0n@users.noreply.github.com> Date: Thu, 23 Apr 2026 00:36:06 +0300 Subject: [PATCH 7/8] docker: BuildKit cache + CGO LTO optimization Add persistent cache mounts for the Go module cache, Go build cache, and apt so only changed packages are re-downloaded or recompiled on subsequent builds. CGO LTO is injected via `make LIBFIDO2_LDFLAGS=...` rather than ENV because the Makefile sets CGO_LDFLAGS inline in go-build-finalize, clobbering any inherited environment variable. Binary stripping is done with strip --strip-all post-build since the Makefile owns the -ldflags chain and cannot be extended without losing the -X version constants. --- build/Dockerfile | 111 ++++++++++++++++++++++++++++++++++++++++++----- deb/Dockerfile | 8 +++- 2 files changed, 106 insertions(+), 13 deletions(-) diff --git a/build/Dockerfile b/build/Dockerfile index 7e7ef90..4d79bf4 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,21 +1,104 @@ +# syntax=docker/dockerfile:1 ### The Deb install is just a repack of the official ProtonMail Bridge deb package with less dependencies. ### I recommend you don't use this. It's here for legacy reasons. +# ============================================================================= +# BUILD STAGE +# ============================================================================= FROM golang:1.26-trixie AS build ARG version ENV version=${version} +# --------------------------------------------------------------------------- +# LTO — why ENV CGO_LDFLAGS didn't work: +# +# The Makefile's go-build-finalize on Linux expands to: +# CGO_LDFLAGS="${LIBFIDO2_LDFLAGS}" go build ... +# That inline shell assignment *overwrites* any CGO_LDFLAGS inherited from +# the Docker environment entirely. +# +# Fix: pass LIBFIDO2_LDFLAGS on the make command line (see RUN step below). +# make CLI variables override Makefile ?= and := definitions, so our flags +# reach the CGO_LDFLAGS inline assignment verbatim. +# +# To disable LTO at build time: +# docker buildx build --build-arg CGO_LTO_FLAGS="" ... +# --------------------------------------------------------------------------- +ARG CGO_LTO_FLAGS="-flto=auto -O2" -RUN apt-get update && apt-get install -y build-essential libsecret-1-dev libfido2-dev libcbor-dev +# -trimpath: the Makefile never sets this flag, so GOFLAGS is safe here. +# Strips host machine paths from the binary → reproducible builds, no path leaks. +ENV GOFLAGS="-trimpath" -# Build +# --------------------------------------------------------------------------- +# System dependencies +# --------------------------------------------------------------------------- +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + libsecret-1-dev \ + libfido2-dev \ + libcbor-dev + +# --------------------------------------------------------------------------- +# Fetch source via BuildKit's git ADD (shallow clone at the requested ref). +# Because the entire tree arrives in one shot we cannot pre-copy go.mod, +# but the module cache mount below still avoids re-downloading dependencies. +# --------------------------------------------------------------------------- ADD https://github.com/ProtonMail/proton-bridge.git#${version} /build/ WORKDIR /build/ -RUN make build-nogui vault-editor -# ----------------------------------------------------------------------------- +# --------------------------------------------------------------------------- +# Pre-seed the Go module cache. +# This layer is invalidated only when go.mod / go.sum change (dep upgrades). +# On pure source changes the cache is warm and this step is near-instant. +# --------------------------------------------------------------------------- +RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \ + go mod download -x +# --------------------------------------------------------------------------- +# Compile. +# +# Cache mounts: +# /go/pkg/mod — downloaded module zips (read, shared) +# /root/.cache/go-build — incremental .a object cache (read-write, locked) +# +# make variable overrides: +# LIBFIDO2_LDFLAGS — appends LTO flags to the CGO_LDFLAGS inline assignment +# in go-build-finalize without touching any other flags. +# Default Makefile value: -lfido2 -lcbor -lssl -lcrypto +# +# Why not -s -w here: +# The Makefile builds its own -ldflags chain: +# BUILD_FLAGS += -ldflags '${GO_LDFLAGS}' +# GO_LDFLAGS carries the -X version/revision constants computed at build +# time. Overriding GO_LDFLAGS on the CLI loses those constants. +# GOFLAGS=-ldflags="-s -w" is silently ignored when the Makefile already +# passes -ldflags on the go build command line (last -ldflags wins in go). +# → We strip the binaries in the next step instead. +# --------------------------------------------------------------------------- +RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \ + --mount=type=cache,target=/root/.cache/go-build,sharing=locked \ + make build-nogui vault-editor \ + BUILD_ENV=prod \ + LIBFIDO2_LDFLAGS="-lfido2 -lcbor -lssl -lcrypto ${CGO_LTO_FLAGS}" + +# --------------------------------------------------------------------------- +# Strip debug symbols — equivalent to go -ldflags="-s -w". +# Typically saves 25-35 % on binary size. +# Remove this RUN if you need delve / runtime stack traces. +# --------------------------------------------------------------------------- +RUN strip --strip-all \ + /build/proton-bridge \ + /build/bridge \ + /build/vault-editor + +# ============================================================================= +# RUNTIME STAGE — minimal Debian image, no build toolchain +# ============================================================================= FROM debian:trixie-slim LABEL maintainer="Simon Felding " @@ -29,25 +112,31 @@ EXPOSE 143/tcp WORKDIR /protonmail COPY gpgparams entrypoint.sh /protonmail/ -# Copy protonmail -COPY --from=build /build/bridge /protonmail/ -COPY --from=build /build/proton-bridge /protonmail/ -COPY --from=build /build/vault-editor /protonmail/ -RUN apt-get update \ +COPY --from=build /build/bridge /protonmail/ +COPY --from=build /build/proton-bridge /protonmail/ +COPY --from=build /build/vault-editor /protonmail/ + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update \ && apt-get install -y --no-install-recommends \ socat pass libsecret-1-0 libfido2-1 ca-certificates procps \ && case "${PTY_TOOL}" in \ dtach) apt-get install -y --no-install-recommends dtach ;; \ abduco) apt-get install -y --no-install-recommends abduco ;; \ reptyr) apt-get install -y --no-install-recommends reptyr ;; \ - *) echo "Unsupported PTY_TOOL: ${PTY_TOOL}. Supported values are: dtach, abduco, reptyr." >&2; exit 1 ;; \ + *) echo "Unsupported PTY_TOOL: ${PTY_TOOL}. Supported: dtach, abduco, reptyr." >&2 ; exit 1 ;; \ esac \ && chmod +x /protonmail/entrypoint.sh \ && rm -rf /var/lib/apt/lists/* HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=120s \ - CMD /bin/bash -c "true < /dev/tcp/localhost/25 && true < /dev/tcp/localhost/143 && true < /dev/tcp/localhost/1025 && true < /dev/tcp/localhost/1143" + CMD /bin/bash -c \ + "true < /dev/tcp/localhost/25 \ + && true < /dev/tcp/localhost/143 \ + && true < /dev/tcp/localhost/1025 \ + && true < /dev/tcp/localhost/1143" ENTRYPOINT ["/protonmail/entrypoint.sh"] CMD ["run"] diff --git a/deb/Dockerfile b/deb/Dockerfile index 8aa81f6..0df10de 100644 --- a/deb/Dockerfile +++ b/deb/Dockerfile @@ -31,13 +31,17 @@ RUN apt-get update \ dtach) apt-get install -y --no-install-recommends dtach ;; \ abduco) apt-get install -y --no-install-recommends abduco ;; \ reptyr) apt-get install -y --no-install-recommends reptyr ;; \ - *) echo "Unsupported PTY_TOOL: ${PTY_TOOL}. Supported values are: dtach, abduco, reptyr." >&2; exit 1 ;; \ + *) echo "Unsupported PTY_TOOL: ${PTY_TOOL}. Supported: dtach, abduco, reptyr." >&2 ; exit 1 ;; \ esac \ && chmod +x /protonmail/entrypoint.sh \ && rm -rf /var/lib/apt/lists/* HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=120s \ - CMD /bin/bash -c "true < /dev/tcp/localhost/25 && true < /dev/tcp/localhost/143 && true < /dev/tcp/localhost/1025 && true < /dev/tcp/localhost/1143" + CMD /bin/bash -c \ + "true < /dev/tcp/localhost/25 \ + && true < /dev/tcp/localhost/143 \ + && true < /dev/tcp/localhost/1025 \ + && true < /dev/tcp/localhost/1143" ENTRYPOINT ["/protonmail/entrypoint.sh"] CMD ["run"] From 036e49faf092e238ce13c9506ca13fae50b302d1 Mon Sep 17 00:00:00 2001 From: c41ms0n <193478517+c41ms0n@users.noreply.github.com> Date: Thu, 23 Apr 2026 00:53:32 +0300 Subject: [PATCH 8/8] healthcheck: parallel bash probe for smtp/imap on all 4 ports --- build/Dockerfile | 12 ++++-------- build/healthcheck.sh | 38 ++++++++++++++++++++++++++++++++++++++ deb/Dockerfile | 12 ++++-------- deb/healthcheck.sh | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 16 deletions(-) create mode 100644 build/healthcheck.sh create mode 100644 deb/healthcheck.sh diff --git a/build/Dockerfile b/build/Dockerfile index 4d79bf4..4721ddd 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -111,7 +111,7 @@ EXPOSE 143/tcp WORKDIR /protonmail -COPY gpgparams entrypoint.sh /protonmail/ +COPY gpgparams entrypoint.sh healthcheck.sh /protonmail/ COPY --from=build /build/bridge /protonmail/ COPY --from=build /build/proton-bridge /protonmail/ @@ -128,15 +128,11 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ reptyr) apt-get install -y --no-install-recommends reptyr ;; \ *) echo "Unsupported PTY_TOOL: ${PTY_TOOL}. Supported: dtach, abduco, reptyr." >&2 ; exit 1 ;; \ esac \ - && chmod +x /protonmail/entrypoint.sh \ + && chmod +x /protonmail/entrypoint.sh /protonmail/healthcheck.sh \ && rm -rf /var/lib/apt/lists/* -HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=120s \ - CMD /bin/bash -c \ - "true < /dev/tcp/localhost/25 \ - && true < /dev/tcp/localhost/143 \ - && true < /dev/tcp/localhost/1025 \ - && true < /dev/tcp/localhost/1143" +HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=120s \ + CMD /protonmail/healthcheck.sh ENTRYPOINT ["/protonmail/entrypoint.sh"] CMD ["run"] diff --git a/build/healthcheck.sh b/build/healthcheck.sh new file mode 100644 index 0000000..bcf10f8 --- /dev/null +++ b/build/healthcheck.sh @@ -0,0 +1,38 @@ +#!/bin/bash +set -euo pipefail + +# Proton Bridge healthcheck — probes all 4 ports in parallel. +# Exit 0 = healthy, 1 = unhealthy (Docker HEALTHCHECK contract). + +TIMEOUT=5 # seconds per probe + +check_smtp() { + local port=$1 + echo 'QUIT' | socat -T${TIMEOUT} - TCP4:localhost:${port} 2>/dev/null \ + | grep -q '^220' +} + +check_imap() { + local port=$1 + printf 'A1 LOGOUT\r\n' | socat -T${TIMEOUT} - TCP4:localhost:${port} 2>/dev/null \ + | grep -q '^\* OK' +} + +# Fire all probes in parallel, capture PIDs +check_smtp 25 & PID_SMTP_25=$! +check_imap 143 & PID_IMAP_143=$! +check_smtp 1025 & PID_SMTP_1025=$! +check_imap 1143 & PID_IMAP_1143=$! + +# Collect results — || prevents set -e from exiting early on probe failure +FAIL=0 +wait $PID_SMTP_25 || { echo "FAIL smtp:25"; FAIL=1; } +wait $PID_IMAP_143 || { echo "FAIL imap:143"; FAIL=1; } +wait $PID_SMTP_1025 || { echo "FAIL smtp:1025"; FAIL=1; } +wait $PID_IMAP_1143 || { echo "FAIL imap:1143"; FAIL=1; } + +if [[ $FAIL -eq 0 ]]; then + echo "OK smtp:25 imap:143 smtp:1025 imap:1143" +fi + +exit $FAIL diff --git a/deb/Dockerfile b/deb/Dockerfile index 0df10de..ab0854c 100644 --- a/deb/Dockerfile +++ b/deb/Dockerfile @@ -22,7 +22,7 @@ EXPOSE 143/tcp WORKDIR /protonmail # Copy bash scripts -COPY gpgparams entrypoint.sh PACKAGE /protonmail/ +COPY gpgparams entrypoint.sh healthcheck.sh PACKAGE /protonmail/ COPY --from=build /protonmail.deb /tmp/protonmail.deb RUN apt-get update \ @@ -33,15 +33,11 @@ RUN apt-get update \ reptyr) apt-get install -y --no-install-recommends reptyr ;; \ *) echo "Unsupported PTY_TOOL: ${PTY_TOOL}. Supported: dtach, abduco, reptyr." >&2 ; exit 1 ;; \ esac \ - && chmod +x /protonmail/entrypoint.sh \ + && chmod +x /protonmail/entrypoint.sh /protonmail/healthcheck.sh \ && rm -rf /var/lib/apt/lists/* -HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=120s \ - CMD /bin/bash -c \ - "true < /dev/tcp/localhost/25 \ - && true < /dev/tcp/localhost/143 \ - && true < /dev/tcp/localhost/1025 \ - && true < /dev/tcp/localhost/1143" +HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=120s \ + CMD /protonmail/healthcheck.sh ENTRYPOINT ["/protonmail/entrypoint.sh"] CMD ["run"] diff --git a/deb/healthcheck.sh b/deb/healthcheck.sh new file mode 100644 index 0000000..bcf10f8 --- /dev/null +++ b/deb/healthcheck.sh @@ -0,0 +1,38 @@ +#!/bin/bash +set -euo pipefail + +# Proton Bridge healthcheck — probes all 4 ports in parallel. +# Exit 0 = healthy, 1 = unhealthy (Docker HEALTHCHECK contract). + +TIMEOUT=5 # seconds per probe + +check_smtp() { + local port=$1 + echo 'QUIT' | socat -T${TIMEOUT} - TCP4:localhost:${port} 2>/dev/null \ + | grep -q '^220' +} + +check_imap() { + local port=$1 + printf 'A1 LOGOUT\r\n' | socat -T${TIMEOUT} - TCP4:localhost:${port} 2>/dev/null \ + | grep -q '^\* OK' +} + +# Fire all probes in parallel, capture PIDs +check_smtp 25 & PID_SMTP_25=$! +check_imap 143 & PID_IMAP_143=$! +check_smtp 1025 & PID_SMTP_1025=$! +check_imap 1143 & PID_IMAP_1143=$! + +# Collect results — || prevents set -e from exiting early on probe failure +FAIL=0 +wait $PID_SMTP_25 || { echo "FAIL smtp:25"; FAIL=1; } +wait $PID_IMAP_143 || { echo "FAIL imap:143"; FAIL=1; } +wait $PID_SMTP_1025 || { echo "FAIL smtp:1025"; FAIL=1; } +wait $PID_IMAP_1143 || { echo "FAIL imap:1143"; FAIL=1; } + +if [[ $FAIL -eq 0 ]]; then + echo "OK smtp:25 imap:143 smtp:1025 imap:1143" +fi + +exit $FAIL