-
Notifications
You must be signed in to change notification settings - Fork 0
ci: add EC2 deployment workflow #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e5c8baf
f9f3f1b
ebe0c72
63cb87e
61bf743
7abdd95
2d3c0a4
8cb0add
83080f7
3c1f1fd
0f483f2
07e0b82
61d2b12
5029e04
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| .git | ||
| .github | ||
| .pytest_cache | ||
| .venv | ||
| __pycache__ | ||
| *.py[cod] | ||
| *.egg-info | ||
| .env | ||
| linko-dev.db | ||
| dist | ||
| build |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| DATABASE_URL= | ||
| POSTGRES_DB= | ||
| POSTGRES_USER= | ||
| POSTGRES_PASSWORD= | ||
| API_PORT= | ||
|
|
||
| JWT_SECRET_KEY= | ||
| JWT_ALGORITHM= | ||
| JWT_ACCESS_TOKEN_EXPIRE_MINUTES= | ||
|
|
||
| GOOGLE_CLIENT_ID= | ||
| GOOGLE_CLIENT_SECRET= | ||
| YOUTUBE_API_KEY= | ||
|
|
||
| AI_PROVIDER= | ||
| GEMINI_API_KEY= | ||
| GEMINI_MODEL= | ||
| SUPADATA_API_KEY= | ||
|
|
||
| CORS_ORIGINS= |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| name: Deploy to EC2 | ||
|
|
||
| on: | ||
| pull_request: | ||
| push: | ||
| branches: | ||
| - main | ||
| workflow_dispatch: | ||
|
|
||
| concurrency: | ||
| group: deploy-ec2-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| test: | ||
| name: Test | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Check out repository | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up Python | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: "3.13" | ||
| cache: pip | ||
|
|
||
| - name: Install dependencies | ||
| run: python -m pip install -e ".[dev]" | ||
|
|
||
| - name: Run tests | ||
| run: python -m pytest -v | ||
|
|
||
| deploy: | ||
| name: Deploy | ||
| runs-on: ubuntu-latest | ||
| needs: test | ||
| if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' | ||
| environment: production | ||
|
|
||
| steps: | ||
| - name: Check out repository | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Configure SSH | ||
| env: | ||
| EC2_HOST: ${{ secrets.EC2_HOST }} | ||
| EC2_PORT: ${{ secrets.EC2_PORT }} | ||
| EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }} | ||
| EC2_USER: ${{ secrets.EC2_USER }} | ||
| EC2_HOST_KEY: ${{ secrets.EC2_HOST_KEY }} | ||
| run: | | ||
| test -n "$EC2_HOST" | ||
| test -n "$EC2_SSH_KEY" | ||
| test -n "$EC2_HOST_KEY" | ||
| mkdir -p ~/.ssh | ||
| printf '%s\n' "$EC2_SSH_KEY" > ~/.ssh/linko-ec2 | ||
| chmod 600 ~/.ssh/linko-ec2 | ||
| printf '%s\n' "$EC2_HOST_KEY" >> ~/.ssh/known_hosts | ||
| { | ||
| echo "Host linko-ec2" | ||
| echo " HostName $EC2_HOST" | ||
| echo " Port ${EC2_PORT:-22}" | ||
| echo " User ${EC2_USER:-ubuntu}" | ||
| echo " IdentityFile ~/.ssh/linko-ec2" | ||
| echo " StrictHostKeyChecking yes" | ||
| } >> ~/.ssh/config | ||
|
|
||
| - name: Package application | ||
| run: | | ||
| tar \ | ||
| --exclude='.git' \ | ||
| --exclude='.github' \ | ||
| --exclude='.pytest_cache' \ | ||
| --exclude='.venv' \ | ||
| --exclude='__pycache__' \ | ||
| --exclude='*.py[cod]' \ | ||
| --exclude='*.egg-info' \ | ||
| --exclude='.env' \ | ||
| -czf /tmp/linko-server.tar.gz . | ||
|
|
||
| - name: Upload application | ||
| env: | ||
| DEPLOY_PATH: ${{ secrets.EC2_DEPLOY_PATH }} | ||
| PROD_ENV: ${{ secrets.PROD_ENV }} | ||
| run: | | ||
| ssh linko-ec2 "mkdir -p '${DEPLOY_PATH:-/opt/linko-server}'" | ||
| scp /tmp/linko-server.tar.gz "linko-ec2:${DEPLOY_PATH:-/opt/linko-server}/release.tar.gz" | ||
| if [ -n "$PROD_ENV" ]; then | ||
| printf '%s\n' "$PROD_ENV" > /tmp/linko.env | ||
| scp /tmp/linko.env "linko-ec2:${DEPLOY_PATH:-/opt/linko-server}/.env" | ||
| fi | ||
|
|
||
| - name: Restart services | ||
| env: | ||
| DEPLOY_PATH: ${{ secrets.EC2_DEPLOY_PATH }} | ||
| run: | | ||
| ssh linko-ec2 "DEPLOY_PATH='${DEPLOY_PATH:-/opt/linko-server}' bash -s" <<'REMOTE' | ||
| set -euo pipefail | ||
| cd "$DEPLOY_PATH" | ||
| tar -xzf release.tar.gz | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The restart script untars Useful? React with 👍 / 👎. |
||
| rm release.tar.gz | ||
|
|
||
| test -f .env | ||
| docker compose -f docker-compose.prod.yml up -d --build | ||
| docker compose -f docker-compose.prod.yml exec -T api python -m alembic upgrade head | ||
| docker compose -f docker-compose.prod.yml ps | ||
| docker image prune -f | ||
| REMOTE | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| FROM python:3.13-slim | ||
|
|
||
| ENV PYTHONDONTWRITEBYTECODE=1 \ | ||
| PYTHONUNBUFFERED=1 | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| RUN apt-get update \ | ||
| && apt-get install -y --no-install-recommends gcc libpq-dev \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| COPY pyproject.toml ./ | ||
| COPY alembic.ini ./ | ||
| COPY alembic ./alembic | ||
| COPY app ./app | ||
|
|
||
| RUN python -m pip install --upgrade pip \ | ||
| && python -m pip install . | ||
|
|
||
| EXPOSE 8000 | ||
|
|
||
| CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -66,3 +66,22 @@ Stop local services: | |
| ```sh | ||
| docker compose down | ||
| ``` | ||
|
|
||
| ## EC2 Deployment | ||
|
|
||
| The production deployment uses GitHub Actions to copy the app to an EC2 instance | ||
| and restart Docker Compose there. Configure these GitHub environment secrets for | ||
| the `production` environment: | ||
|
|
||
| - `EC2_HOST`: EC2 public host or IP address. | ||
| - `EC2_USER`: SSH user, for example `ubuntu`. | ||
| - `EC2_SSH_KEY`: private key with SSH access to the instance. | ||
| - `EC2_PORT`: optional SSH port, defaults to `22`. | ||
| - `EC2_DEPLOY_PATH`: optional deploy directory, defaults to `/opt/linko-server`. | ||
| - `PROD_ENV`: full contents of the production `.env` file. Use `.env.example` as | ||
| the template, and replace secrets before deploying. | ||
|
Comment on lines
+76
to
+82
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The workflow now hard-requires Useful? React with 👍 / 👎. |
||
|
|
||
| On the EC2 instance, install Docker and the Docker Compose plugin first. Then run | ||
| the `Deploy to EC2` workflow manually, or merge to `main` to deploy | ||
| automatically. The workflow builds the FastAPI image on EC2, starts the API and | ||
| PostgreSQL containers, and runs Alembic migrations. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The workflow-level concurrency key is scoped to
github.ref, but the deploy job can run frompull_request,push, andworkflow_dispatch; those events often use different refs while targeting the same EC2 host/path. That allows concurrent production deploys to race onrelease.tar.gzupload/extract and service restarts, which can produce inconsistent rollout state.Useful? React with 👍 / 👎.