From d0975f279eb28d5f4947359e1a0ec6e60537ac88 Mon Sep 17 00:00:00 2001 From: heznpc Date: Fri, 24 Apr 2026 01:13:55 +0900 Subject: [PATCH] feat: add deploy rollback and non-root USER in Dockerfile examples - cd.yml: capture previous running image before deploy; on --wait failure, rewrite compose with previous tag and restart - DOCKERFILE_EXAMPLES.md: add non-root user (USER app) to Python, Go, Rust, Java examples to match the Node example's USER node pattern --- .github/workflows/cd.yml | 40 ++++++++++++++++++++++++++++++++----- docs/DOCKERFILE_EXAMPLES.md | 22 ++++++++++++++++---- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 4036e82..3e9c13f 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -87,13 +87,28 @@ jobs: username: ${{ secrets.VPS_USER }} key: ${{ secrets.VPS_SSH_KEY }} script: | + set -e IMAGE="ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }}" PORT="${{ secrets.APP_PORT || '3000' }}" mkdir -p ~/app - cat > ~/app/docker-compose.yml << EOF + cd ~/app + + # Capture currently running image for potential rollback + PREV_IMAGE="" + if docker compose ps -q app >/dev/null 2>&1; then + CID=$(docker compose ps -q app || true) + if [ -n "$CID" ]; then + PREV_IMAGE=$(docker inspect --format '{{.Config.Image}}' "$CID" 2>/dev/null || true) + fi + fi + echo "Previous image: ${PREV_IMAGE:-}" + + write_compose() { + local img="$1" + cat > ~/app/docker-compose.yml << EOF services: app: - image: $IMAGE + image: $img env_file: $HOME/.env.app ports: - "${PORT}:${PORT}" @@ -105,10 +120,25 @@ jobs: retries: 3 start_period: 10s EOF - cd ~/app + } + + write_compose "$IMAGE" docker compose pull - docker compose up -d --wait - docker image prune -f + + if docker compose up -d --wait; then + echo "Deploy succeeded." + docker image prune -f + else + echo "::error::Deploy health check failed." + if [ -n "$PREV_IMAGE" ] && [ "$PREV_IMAGE" != "$IMAGE" ]; then + echo "Rolling back to $PREV_IMAGE" + write_compose "$PREV_IMAGE" + docker compose up -d --wait || echo "::error::Rollback also failed — manual intervention required." + else + echo "No previous image available to roll back to." + fi + exit 1 + fi - name: Clean up old GHCR images uses: actions/delete-package-versions@v5 diff --git a/docs/DOCKERFILE_EXAMPLES.md b/docs/DOCKERFILE_EXAMPLES.md index 67adabb..e1f07e9 100644 --- a/docs/DOCKERFILE_EXAMPLES.md +++ b/docs/DOCKERFILE_EXAMPLES.md @@ -33,9 +33,12 @@ COPY app/ . FROM python:3.12-slim +RUN adduser --disabled-password --gecos "" app WORKDIR /app COPY --from=build /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages -COPY --from=build /app . +COPY --from=build --chown=app:app /app . + +USER app EXPOSE 8000 CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] @@ -54,8 +57,11 @@ RUN CGO_ENABLED=0 go build -o server . FROM alpine:3.20 +RUN adduser -D -H app WORKDIR /app -COPY --from=build /app/server . +COPY --from=build --chown=app:app /app/server . + +USER app EXPOSE 8080 CMD ["./server"] @@ -74,8 +80,11 @@ RUN cargo build --release FROM alpine:3.20 +RUN adduser -D -H app WORKDIR /app -COPY --from=build /app/target/release/server . +COPY --from=build --chown=app:app /app/target/release/server . + +USER app EXPOSE 8080 CMD ["./server"] @@ -92,8 +101,11 @@ RUN ./gradlew bootJar --no-daemon FROM eclipse-temurin:21-jre-alpine +RUN addgroup --system app && adduser --system --ingroup app app WORKDIR /app -COPY --from=build /app/build/libs/*.jar app.jar +COPY --from=build --chown=app:app /app/build/libs/*.jar app.jar + +USER app EXPOSE 8080 CMD ["java", "-jar", "app.jar"] @@ -110,6 +122,8 @@ EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] ``` +> Note: The official `nginx:alpine` image runs as root by default to bind port 80. For non-root Nginx, use `nginxinc/nginx-unprivileged:alpine` (listens on 8080). + ## Tips - **Multi-stage builds** reduce image size by separating build dependencies from runtime