fix(deployment): run Caddy non-root under all users, add process supervision, fix healthcheck
- docker-entrypoint.sh: always start both Caddy and Python instead of exiting early for non-root containers; drop Caddy's privileges via gosu (never runs as root); supervise both processes with `wait -n` and tear down the container if either one dies - Caddyfile: add `admin off`; enforce MAX_UPLOAD_SIZE via `request_body max_size` on /api/* instead of relying on an unused env var - docker-compose.yml: use explicit MAX_UPLOAD_SIZE=30MB unit - Dockerfile: merge COPY+chown into a single `COPY --chown` layer for src/; fix HEALTHCHECK Python one-liner (literal \n was causing a SyntaxError on every check, so the container never reported healthy)
This commit is contained in:
1 parent
9a70ae1578
commit
27f2314c96
5 files changed
+89
-42
No files matched your search
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
# Disable automatic HTTPS for local development
|
# No TLS is terminated here, so disable HTTPS redirects/certs and the admin API
|
||||||
auto_https off
|
auto_https off
|
||||||
|
admin off
|
||||||
}
|
}
|
||||||
|
|
||||||
:8080 {
|
:8080 {
|
||||||
@@ -13,6 +14,10 @@
|
|||||||
|
|
||||||
# HTTP API endpoints (preserve /api/* path)
|
# HTTP API endpoints (preserve /api/* path)
|
||||||
handle /api/* {
|
handle /api/* {
|
||||||
|
# Reject oversized uploads before they reach the backend
|
||||||
|
request_body {
|
||||||
|
max_size {$MAX_UPLOAD_SIZE:100MB}
|
||||||
|
}
|
||||||
reverse_proxy 127.0.0.1:8082
|
reverse_proxy 127.0.0.1:8082
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,12 +112,12 @@ RUN groupadd -r webssh && useradd -r -g webssh webssh
|
|||||||
|
|
||||||
WORKDIR /app/src
|
WORKDIR /app/src
|
||||||
|
|
||||||
# Copy application and static files
|
# Copy application and static files (owned by webssh in one layer, no separate chown pass)
|
||||||
COPY src/ /app/src/
|
COPY --chown=webssh:webssh src/ /app/src/
|
||||||
COPY deployment/docker-entrypoint.sh /usr/local/bin/
|
COPY deployment/docker-entrypoint.sh /usr/local/bin/
|
||||||
|
|
||||||
# Create .ssh directory for host keys
|
# Create .ssh directory for host keys
|
||||||
RUN mkdir -p /app/src/.ssh && chown -R webssh:webssh /app/src
|
RUN mkdir -p /app/src/.ssh && chown webssh:webssh /app/src/.ssh
|
||||||
|
|
||||||
# Copy entrypoint
|
# Copy entrypoint
|
||||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||||
@@ -137,7 +137,7 @@ EXPOSE 8080 8081 8082 8083
|
|||||||
|
|
||||||
# Health check via Caddy (serving root) using Python (no curl required)
|
# Health check via Caddy (serving root) using Python (no curl required)
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
CMD python -c "import sys,urllib.request as u;\ntry:\n r=u.urlopen('http://127.0.0.1:8080/', timeout=5);\n code=getattr(r,'status',None) or r.getcode();\n sys.exit(0 if code==200 else 1)\nexcept Exception:\n sys.exit(1)"
|
CMD python -c "import sys, urllib.request; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8080/', timeout=5).getcode() == 200 else 1)"
|
||||||
|
|
||||||
# Use shell form to see entrypoint errors
|
# Use shell form to see entrypoint errors
|
||||||
ENTRYPOINT /usr/local/bin/docker-entrypoint.sh
|
ENTRYPOINT /usr/local/bin/docker-entrypoint.sh
|
||||||
@@ -14,8 +14,8 @@ services:
|
|||||||
- LOG_LEVEL=INFO
|
- LOG_LEVEL=INFO
|
||||||
# Configure users: format is username:password,username2:password2
|
# Configure users: format is username:password,username2:password2
|
||||||
- WEBSSH_USERS=admin:admin
|
- WEBSSH_USERS=admin:admin
|
||||||
# Max upload size (supports format like "100M" for 100MB or "50M" for 50MB)
|
# Max upload size, enforced by both Caddy and the backend (e.g. "30MB", "512KB")
|
||||||
- MAX_UPLOAD_SIZE=30M
|
- MAX_UPLOAD_SIZE=30MB
|
||||||
# Session Recording Configuration
|
# Session Recording Configuration
|
||||||
- RECORDING_ENABLED=true
|
- RECORDING_ENABLED=true
|
||||||
- RECORDING_RETENTION_DAYS=30
|
- RECORDING_RETENTION_DAYS=30
|
||||||
|
|||||||
@@ -1,47 +1,88 @@
|
|||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
set -x
|
set -x
|
||||||
|
|
||||||
# Check if running as root
|
CADDY_PID=""
|
||||||
if [ "$(id -u)" != "0" ]; then
|
APP_PID=""
|
||||||
echo "Not running as root, starting application directly..."
|
|
||||||
exec python /app/src/run.py
|
# Tear down both processes together so a crash of one doesn't leave the
|
||||||
|
# container running half-broken (and eventually silently unhealthy).
|
||||||
|
cleanup() {
|
||||||
|
trap - TERM INT
|
||||||
|
[ -n "$CADDY_PID" ] && kill -TERM "$CADDY_PID" 2>/dev/null || true
|
||||||
|
[ -n "$APP_PID" ] && kill -TERM "$APP_PID" 2>/dev/null || true
|
||||||
|
wait 2>/dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup TERM INT
|
||||||
|
|
||||||
|
RUN_AS_ROOT=0
|
||||||
|
if [ "$(id -u)" = "0" ]; then
|
||||||
|
RUN_AS_ROOT=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create data directory with correct permissions for the WebSSH user
|
if [ "$RUN_AS_ROOT" = "1" ]; then
|
||||||
if [ ! -d "/data" ]; then
|
# Create data directory with correct permissions for the WebSSH user
|
||||||
echo "Creating /data directory..."
|
if [ ! -d "/data" ]; then
|
||||||
mkdir -p /data
|
echo "Creating /data directory..."
|
||||||
|
mkdir -p /data
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Current /data permissions:"
|
||||||
|
ls -la /data || true
|
||||||
|
|
||||||
|
# Ensure /data is writable and owned by the WebSSH user
|
||||||
|
echo "Setting permissions on /data..."
|
||||||
|
chmod 755 /data
|
||||||
|
chown webssh:webssh /data
|
||||||
|
|
||||||
|
if [ "$(stat -c %U /data)" != "webssh" ]; then
|
||||||
|
echo "WARNING: Could not change ownership of /data to webssh"
|
||||||
|
echo "Current owner: $(stat -c %U /data)"
|
||||||
|
echo "This may happen with bind mounts. Ensure host directory is writable."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Final /data permissions:"
|
||||||
|
ls -la /data || true
|
||||||
|
else
|
||||||
|
echo "Not running as root (uid=$(id -u)); skipping /data permission setup."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Current /data permissions:"
|
# Caddy (run as a non-root user below) needs a writable home for its autosave
|
||||||
ls -la /data || true
|
# state and, if TLS is ever enabled, ACME certificates/keys.
|
||||||
ls -la / | grep data || true
|
export XDG_DATA_HOME="${XDG_DATA_HOME:-/tmp/caddy-data}"
|
||||||
|
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-/tmp/caddy-config}"
|
||||||
# Ensure /data is writable and owned by the WebSSH user (UID 999)
|
mkdir -p "$XDG_DATA_HOME" "$XDG_CONFIG_HOME"
|
||||||
echo "Setting permissions on /data..."
|
if [ "$RUN_AS_ROOT" = "1" ]; then
|
||||||
chmod 755 /data
|
chown -R webssh:webssh "$XDG_DATA_HOME" "$XDG_CONFIG_HOME"
|
||||||
chown webssh:webssh /data
|
|
||||||
|
|
||||||
# Check if chown worked
|
|
||||||
if [ "$(stat -c %U /data)" != "webssh" ]; then
|
|
||||||
echo "WARNING: Could not change ownership of /data to webssh"
|
|
||||||
echo "Current owner: $(stat -c %U /data)"
|
|
||||||
echo "This may happen with bind mounts. Ensure host directory is writable."
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Final /data permissions:"
|
# Start Caddy (serves static files and reverse-proxies to local services) as
|
||||||
ls -la /data || true
|
# the unprivileged webssh user; it handles untrusted network input and must
|
||||||
|
# not run as root.
|
||||||
# Configure nginx max upload size from environment
|
|
||||||
MAX_UPLOAD_SIZE="${MAX_UPLOAD_SIZE:-100M}"
|
|
||||||
|
|
||||||
# Start Caddy in background (serves static files and reverse-proxies to local services)
|
|
||||||
if command -v caddy >/dev/null 2>&1; then
|
if command -v caddy >/dev/null 2>&1; then
|
||||||
echo "Starting Caddy..."
|
echo "Starting Caddy..."
|
||||||
caddy run --config /etc/caddy/Caddyfile &
|
if [ "$RUN_AS_ROOT" = "1" ]; then
|
||||||
|
gosu webssh caddy run --config /etc/caddy/Caddyfile &
|
||||||
|
else
|
||||||
|
caddy run --config /etc/caddy/Caddyfile &
|
||||||
|
fi
|
||||||
|
CADDY_PID=$!
|
||||||
else
|
else
|
||||||
echo "Caddy binary not found; continuing without reverse proxy"
|
echo "Caddy binary not found; continuing without reverse proxy"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Start the application (run as the WebSSH user)
|
# Start the application, also dropping privileges when possible
|
||||||
exec gosu webssh python /app/src/run.py
|
if [ "$RUN_AS_ROOT" = "1" ]; then
|
||||||
|
gosu webssh python /app/src/run.py &
|
||||||
|
else
|
||||||
|
python /app/src/run.py &
|
||||||
|
fi
|
||||||
|
APP_PID=$!
|
||||||
|
|
||||||
|
# If either supervised process exits, shut the container down instead of
|
||||||
|
# silently continuing in a half-working (but "healthy") state.
|
||||||
|
wait -n
|
||||||
|
EXIT_CODE=$?
|
||||||
|
echo "A supervised process exited with code $EXIT_CODE; shutting down"
|
||||||
|
cleanup
|
||||||
|
exit "$EXIT_CODE"
|
||||||
@@ -111,8 +111,9 @@ body {
|
|||||||
|
|
||||||
.connection-heading {
|
.connection-heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
flex-direction: column; /* Добавено: подрежда ги вертикално */
|
||||||
gap: 8px;
|
align-items: flex-start; /* Променено: подравнява ги вляво */
|
||||||
|
gap: 2px; /* Намалено, за да са по-близо едно до друго вертикално */
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user