Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:-<none>}"

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}"
Expand All @@ -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
Expand Down
22 changes: 18 additions & 4 deletions docs/DOCKERFILE_EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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"]
Expand All @@ -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"]
Expand All @@ -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"]
Expand All @@ -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
Expand Down
Loading