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
169 changes: 169 additions & 0 deletions .github/workflows/deploy-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
name: Deploy Dev

on:
workflow_dispatch:
push:
branches:
- dev

permissions:
contents: read
packages: write

concurrency:
group: deploy-development
cancel-in-progress: true

env:
REGISTRY: ghcr.io
IMAGE_NAME: devkor-github/ontime-back
IMAGE_TAG: dev-${{ github.sha }}

jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push image
uses: docker/build-push-action@v6
with:
context: ./ontime-back
file: ./ontime-back/Dockerfile
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev-latest
cache-from: type=gha
cache-to: type=gha,mode=max

deploy-to-remote-pc:
needs: build-and-push
runs-on: ubuntu-latest
environment: development
env:
DEV_DEPLOY_DIR: ${{ secrets.DEV_DEPLOY_DIR || format('/home/{0}/OnTime-back-dev', secrets.DEV_REMOTE_USER) }}
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Prepare dev deploy directory
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.DEV_REMOTE_HOST }}
username: ${{ secrets.DEV_REMOTE_USER }}
key: ${{ secrets.DEV_REMOTE_SSH_KEY }}
script: |
set -eu
mkdir -p "${{ env.DEV_DEPLOY_DIR }}"

- name: Upload compose files to remote PC
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.DEV_REMOTE_HOST }}
username: ${{ secrets.DEV_REMOTE_USER }}
key: ${{ secrets.DEV_REMOTE_SSH_KEY }}
source: "ontime-back/docker-compose.yml,ontime-back/docker-compose.dev.yml"
target: ${{ env.DEV_DEPLOY_DIR }}
strip_components: 1

- name: Pull image and restart dev containers
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.DEV_REMOTE_HOST }}
username: ${{ secrets.DEV_REMOTE_USER }}
key: ${{ secrets.DEV_REMOTE_SSH_KEY }}
script: |
set -eu

DEPLOY_DIR="${{ env.DEV_DEPLOY_DIR }}"
CONTAINER_NAME="ontime-dev-container"

mkdir -p "$DEPLOY_DIR"
cd "$DEPLOY_DIR"

umask 077
cat > .env <<'EOF'
IMAGE_TAG=${{ env.IMAGE_TAG }}
BACKEND_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
BACKEND_CONTAINER_NAME=ontime-dev-container
BACKEND_HTTP_PORT=${{ secrets.DEV_BACKEND_HTTP_PORT || '8081' }}
BACKEND_MEMORY_LIMIT=${{ secrets.DEV_BACKEND_MEMORY_LIMIT || '768m' }}
BACKEND_CPU_LIMIT=${{ secrets.DEV_BACKEND_CPU_LIMIT || '1.0' }}
SERVER_PORT=8080
SPRING_PROFILES_ACTIVE=dev
JAVA_TOOL_OPTIONS=-XX:InitialRAMPercentage=50.0 -XX:MaxRAMPercentage=75.0 -Djava.security.egd=file:/dev/./urandom

MYSQL_DATABASE=${{ secrets.DEV_MYSQL_DATABASE || 'ontime_dev' }}
MYSQL_USER=${{ secrets.DEV_MYSQL_USER || 'ontime_dev' }}
MYSQL_PASSWORD=${{ secrets.DEV_MYSQL_PASSWORD || 'ontime_dev_password' }}
MYSQL_ROOT_PASSWORD=${{ secrets.DEV_MYSQL_ROOT_PASSWORD || 'ontime_dev_root_password' }}

SPRING_APPLICATION_NAME=${{ secrets.DEV_SPRING_APPLICATION_NAME || 'ontime-back-dev' }}
SPRING_DATASOURCE_URL=${{ secrets.DEV_SPRING_DATASOURCE_URL || 'jdbc:mysql://mysql:3306/ontime_dev?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8&allowPublicKeyRetrieval=true' }}
SPRING_DATASOURCE_USERNAME=${{ secrets.DEV_SPRING_DATASOURCE_USERNAME || secrets.DEV_MYSQL_USER || 'ontime_dev' }}
SPRING_DATASOURCE_PASSWORD=${{ secrets.DEV_SPRING_DATASOURCE_PASSWORD || secrets.DEV_MYSQL_PASSWORD || 'ontime_dev_password' }}
SPRING_DATASOURCE_DRIVER_CLASS_NAME=com.mysql.cj.jdbc.Driver
SPRING_JPA_HIBERNATE_DDL_AUTO=validate

SPRING_FLYWAY_ENABLED=true
SPRING_FLYWAY_BASELINE_ON_MIGRATE=false

JWT_SECRET_KEY=${{ secrets.DEV_JWT_SECRETKEY || 'dev_secret_key_for_ontime_back_remote_pc_development_1234567890' }}
JWT_ACCESS_EXPIRATION=${{ secrets.DEV_JWT_ACCESS_EXPIRATION || '3600000' }}
JWT_REFRESH_EXPIRATION=${{ secrets.DEV_JWT_REFRESH_EXPIRATION || '1209600000' }}
JWT_ACCESS_HEADER=${{ secrets.DEV_JWT_ACCESS_HEADER || 'Authorization' }}
JWT_REFRESH_HEADER=${{ secrets.DEV_JWT_REFRESH_HEADER || 'Authorization-refresh' }}

GOOGLE_WEB_CLIENT_ID=${{ secrets.DEV_GOOGLE_WEB_CLIENT_ID || 'dev-google-web-client-id' }}
GOOGLE_APP_CLIENT_ID=${{ secrets.DEV_GOOGLE_APP_CLIENT_ID || 'dev-google-app-client-id' }}

APPLE_CLIENT_ID=${{ secrets.DEV_APPLE_CLIENT_ID || 'dev-apple-client-id' }}
APPLE_TEAM_ID=${{ secrets.DEV_APPLE_TEAM_ID || 'dev-apple-team-id' }}
APPLE_LOGIN_KEY=${{ secrets.DEV_APPLE_LOGIN_KEY || 'dev-apple-key-id' }}
APPLE_PRIVATE_KEY_BASE64=${{ secrets.DEV_APPLE_PRIVATE_KEY_BASE64 }}
FEATURE_APPLE_LOGIN_ENABLED=${{ secrets.DEV_FEATURE_APPLE_LOGIN_ENABLED || 'false' }}

FIREBASE_CREDENTIALS_BASE64=${{ secrets.DEV_FIREBASE_CREDENTIALS_BASE64 }}
EOF

echo "${{ secrets.GHCR_READ_TOKEN }}" | sudo docker login ghcr.io -u "${{ secrets.GHCR_USERNAME }}" --password-stdin

if sudo docker compose version >/dev/null 2>&1; then
COMPOSE="sudo docker compose"
else
COMPOSE="sudo docker-compose"
fi

$COMPOSE -f docker-compose.yml -f docker-compose.dev.yml pull
$COMPOSE -f docker-compose.yml -f docker-compose.dev.yml up -d --remove-orphans

HEALTHY=false
for attempt in $(seq 1 30); do
STATUS="$(sudo docker inspect -f '{{.State.Health.Status}}' "$CONTAINER_NAME" 2>/dev/null || true)"
if [ "$STATUS" = "healthy" ]; then
echo "Container is healthy."
HEALTHY=true
break
fi
echo "Waiting for healthy container status; current status: ${STATUS:-unknown}"
sleep 5
done

if [ "$HEALTHY" != "true" ]; then
sudo docker logs --tail=200 "$CONTAINER_NAME" || true
exit 1
fi

curl -fsS "http://127.0.0.1:${{ secrets.DEV_BACKEND_HTTP_PORT || '8081' }}/actuator/health/readiness"
81 changes: 75 additions & 6 deletions docs/deployment.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Production Deployment
# Deployment

This service deploys as an immutable Docker image published to GitHub Container Registry (GHCR). Runtime configuration is injected through the EC2 `.env` file generated by GitHub Actions; private resource files are not copied into the image or bind-mounted from the host.
This service deploys as an immutable Docker image published to GitHub Container Registry (GHCR). Runtime configuration is injected through `.env` files generated by GitHub Actions; private resource files are not copied into the image or bind-mounted from the host.

## Required GitHub Secrets
Production deploys to EC2 from `main`. Development deploys to a remote Ubuntu/Linux PC from `dev`.

## Production GitHub Secrets

Deployment access:

Expand Down Expand Up @@ -85,9 +87,7 @@ base64 -i ontime-back/src/main/resources/key/AuthKey_743M7R5W3W.p8 | tr -d '\n'

Push to the `main` branch, or run `.github/workflows/deploy.yml` manually, to deploy production.

Pushes to `dev` run CI only. There is no dev-server deploy workflow in the one-EC2 plan.

The workflow:
The production workflow:

1. Builds `ontime-back/Dockerfile` from the `ontime-back/` context.
2. Pushes two GHCR tags:
Expand All @@ -100,6 +100,65 @@ The workflow:
7. Waits until the `ontime-container` Docker health status is `healthy`.
8. Installs Caddy if needed, configures `/etc/caddy/Caddyfile`, and verifies HTTPS for `ontime-back.duckdns.org`.

## Development Remote PC Deployment

Push to the `dev` branch, or run `.github/workflows/deploy-dev.yml` manually, to deploy the development backend to the remote PC.

The development workflow:

1. Builds `ontime-back/Dockerfile` from the `ontime-back/` context.
2. Pushes two GHCR tags:
- `ghcr.io/devkor-github/ontime-back:dev-<commit-sha>`
- `ghcr.io/devkor-github/ontime-back:dev-latest`
3. Uploads `docker-compose.yml` and `docker-compose.dev.yml` to the remote PC.
4. Writes a development `.env` from GitHub secrets and safe dev defaults.
5. Runs `docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --remove-orphans`.
6. Starts MySQL as a private Docker Compose service with persistent volume `ontime-dev-mysql-data`.
7. Waits until the `ontime-dev-container` Docker health status is `healthy`.

Required development secrets:

- `DEV_REMOTE_HOST`
- `DEV_REMOTE_USER`
- `DEV_REMOTE_SSH_KEY`
- `GHCR_USERNAME`
- `GHCR_READ_TOKEN`

Optional development secrets:

- `DEV_DEPLOY_DIR` (defaults to `/home/<DEV_REMOTE_USER>/OnTime-back-dev`)
- `DEV_BACKEND_HTTP_PORT` (defaults to `8081`)
- `DEV_BACKEND_MEMORY_LIMIT` (defaults to `768m`)
- `DEV_BACKEND_CPU_LIMIT` (defaults to `1.0`)
- `DEV_MYSQL_DATABASE` (defaults to `ontime_dev`)
- `DEV_MYSQL_USER` (defaults to `ontime_dev`)
- `DEV_MYSQL_PASSWORD` (defaults to `ontime_dev_password`)
- `DEV_MYSQL_ROOT_PASSWORD` (defaults to `ontime_dev_root_password`)
- `DEV_SPRING_APPLICATION_NAME` (defaults to `ontime-back-dev`)
- `DEV_SPRING_DATASOURCE_URL` (defaults to the Compose MySQL service)
- `DEV_SPRING_DATASOURCE_USERNAME` (defaults to the dev MySQL user)
- `DEV_SPRING_DATASOURCE_PASSWORD` (defaults to the dev MySQL password)
- `DEV_JWT_SECRETKEY`
- `DEV_JWT_ACCESS_EXPIRATION`
- `DEV_JWT_REFRESH_EXPIRATION`
- `DEV_JWT_ACCESS_HEADER`
- `DEV_JWT_REFRESH_HEADER`
- `DEV_GOOGLE_WEB_CLIENT_ID`
- `DEV_GOOGLE_APP_CLIENT_ID`
- `DEV_APPLE_CLIENT_ID`
- `DEV_APPLE_TEAM_ID`
- `DEV_APPLE_LOGIN_KEY`
- `DEV_APPLE_PRIVATE_KEY_BASE64`
- `DEV_FEATURE_APPLE_LOGIN_ENABLED` (defaults to `false`)
- `DEV_FIREBASE_CREDENTIALS_BASE64`

Remote PC prerequisites:

- Ubuntu/Linux host with normal SSH access from GitHub Actions.
- Docker and the Docker Compose plugin installed.
- Inbound firewall access for the backend HTTP port, default `8081`.
- No public inbound MySQL port is required; MySQL stays inside the Docker network.

## HTTPS Prerequisites

Before running the production deploy, configure AWS and DNS:
Expand Down Expand Up @@ -129,6 +188,16 @@ curl -fsS https://ontime-back.duckdns.org/actuator/health/readiness
nc -zv ontime-prod.cpoeguokwaq5.ap-northeast-2.rds.amazonaws.com 3306
```

Manual checks on the remote development PC:

```bash
cd /home/<DEV_REMOTE_USER>/OnTime-back-dev
sudo docker compose -f docker-compose.yml -f docker-compose.dev.yml ps
sudo docker inspect -f '{{.State.Health.Status}}' ontime-dev-container
sudo docker logs --tail=200 ontime-dev-container
curl -fsS http://<remote-pc-host>:8081/actuator/health/readiness
```

## Rollback

Every deploy is tagged by commit SHA. To roll back, set `IMAGE_TAG` in `/home/ubuntu/OnTime-back/.env` to the previous known-good SHA, then restart from the existing Compose file:
Expand Down
Loading
Loading