Skip to content

Commit

Permalink
Migrate to Kamal for deployment
Browse files Browse the repository at this point in the history
  • Loading branch information
Layoric committed Dec 3, 2024
1 parent 206d6ae commit a79d445
Show file tree
Hide file tree
Showing 13 changed files with 536 additions and 176 deletions.
97 changes: 97 additions & 0 deletions .github/workflows/build-container.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: Build Container
permissions:
packages: write
contents: write
on:
workflow_run:
workflows: ["Build"]
types:
- completed
branches:
- main
- master
workflow_dispatch:

env:
DOCKER_BUILDKIT: 1
KAMAL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
KAMAL_REGISTRY_USERNAME: ${{ github.actor }}

jobs:
build-container:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Set up environment variables
run: |
echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
echo "repository_name=$(echo ${{ github.repository }} | cut -d '/' -f 2)" >> $GITHUB_ENV
echo "repository_name_lower=$(echo ${{ github.repository }} | cut -d '/' -f 2 | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
echo "org_name=$(echo ${{ github.repository }} | cut -d '/' -f 1)" >> $GITHUB_ENV
if [ -n "${{ secrets.APPSETTINGS_PATCH }}" ]; then
echo "HAS_APPSETTINGS_PATCH=true" >> $GITHUB_ENV
else
echo "HAS_APPSETTINGS_PATCH=false" >> $GITHUB_ENV
fi
if [ -n "${{ secrets.KAMAL_DEPLOY_IP }}" ]; then
echo "HAS_DEPLOY_ACTION=true" >> $GITHUB_ENV
else
echo "HAS_DEPLOY_ACTION=false" >> $GITHUB_ENV
fi
# This step is for the deployment of the templates only, safe to delete
- name: Modify csproj for template deploy
if: env.HAS_DEPLOY_ACTION == 'true'
run: |
sed -i 's#<ContainerLabel Include="service" Value="my-app" />#<ContainerLabel Include="service" Value="${{ env.repository_name_lower }}" />#g' MyApp/MyApp.csproj
- name: Check for Client directory and package.json
id: check_client
run: |
if [ -d "MyApp.Client" ] && [ -f "MyApp.Client/package.json" ]; then
echo "client_exists=true" >> $GITHUB_OUTPUT
else
echo "client_exists=false" >> $GITHUB_OUTPUT
fi
- name: Setup Node.js
if: steps.check_client.outputs.client_exists == 'true'
uses: actions/setup-node@v3
with:
node-version: 22

- name: Install npm dependencies
if: steps.check_client.outputs.client_exists == 'true'
working-directory: ./MyApp.Client
run: npm install

- name: Install x tool
run: dotnet tool install -g x

- name: Apply Production AppSettings
if: env.HAS_APPSETTINGS_PATCH == 'true'
working-directory: ./MyApp
run: |
cat <<EOF >> appsettings.json.patch
${{ secrets.APPSETTINGS_PATCH }}
EOF
x patch appsettings.json.patch
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ env.KAMAL_REGISTRY_USERNAME }}
password: ${{ env.KAMAL_REGISTRY_PASSWORD }}

- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0'

- name: Build and push Docker image
run: |
dotnet publish --os linux --arch x64 -c Release -p:ContainerRepository=${{ env.image_repository_name }} -p:ContainerRegistry=ghcr.io -p:ContainerImageTags=latest -p:ContainerPort=80
256 changes: 80 additions & 176 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,202 +3,106 @@ permissions:
packages: write
contents: write
on:
# Triggered on new GitHub Release
release:
types: [published]
# Triggered on every successful Build action
workflow_run:
workflows: ["Build"]
branches: [main,master]
workflows: ["Build Container"]
types:
- completed
# Manual trigger for rollback to specific release or redeploy latest
branches:
- main
- master
workflow_dispatch:
inputs:
version:
default: latest
description: Tag you want to release.
required: true

env:
DOCKER_BUILDKIT: 1
KAMAL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
KAMAL_REGISTRY_USERNAME: ${{ github.actor }}

jobs:
push_to_registry:
release:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion != 'failure' }}
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
# Checkout latest or specific tag
- name: checkout
if: ${{ github.event.inputs.version == '' || github.event.inputs.version == 'latest' }}
uses: actions/checkout@v3
- name: checkout tag
if: ${{ github.event.inputs.version != '' && github.event.inputs.version != 'latest' }}
- name: Checkout code
uses: actions/checkout@v3
with:
ref: refs/tags/${{ github.event.inputs.version }}

# Assign environment variables used in subsequent steps
- name: Env variable assignment
run: echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
# TAG_NAME defaults to 'latest' if not a release or manual deployment
- name: Assign version

- name: Set up environment variables
run: |
echo "TAG_NAME=latest" >> $GITHUB_ENV
if [ "${{ github.event.release.tag_name }}" != "" ]; then
echo "TAG_NAME=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
fi;
if [ "${{ github.event.inputs.version }}" != "" ]; then
echo "TAG_NAME=${{ github.event.inputs.version }}" >> $GITHUB_ENV
fi;
if [ ! -z "${{ secrets.APPSETTINGS_PATCH }}" ]; then
echo "HAS_APPSETTINGS_PATCH=true" >> $GITHUB_ENV
echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
echo "repository_name=$(echo ${{ github.repository }} | cut -d '/' -f 2)" >> $GITHUB_ENV
echo "repository_name_lower=$(echo ${{ github.repository }} | cut -d '/' -f 2 | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
echo "org_name=$(echo ${{ github.repository }} | cut -d '/' -f 1)" >> $GITHUB_ENV
if find . -maxdepth 2 -type f -name "Configure.Db.Migrations.cs" | grep -q .; then
echo "HAS_MIGRATIONS=true" >> $GITHUB_ENV
else
echo "HAS_MIGRATIONS=false" >> $GITHUB_ENV
fi
if [ -n "${{ secrets.KAMAL_DEPLOY_IP }}" ]; then
echo "HAS_DEPLOY_ACTION=true" >> $GITHUB_ENV
else
echo "HAS_APPSETTINGS_PATCH=false" >> $GITHUB_ENV
fi;
echo "HAS_DEPLOY_ACTION=false" >> $GITHUB_ENV
fi
# This step is for the deployment of the templates only, safe to delete
- name: Modify deploy.yml
if: env.HAS_DEPLOY_ACTION == 'true'
run: |
sed -i "s/service: my-app/service: ${{ env.repository_name_lower }}/g" config/deploy.yml
sed -i "s#image: my-user/myapp#image: ${{ env.image_repository_name }}#g" config/deploy.yml
sed -i "s/- 192.168.0.1/- ${{ secrets.KAMAL_DEPLOY_IP }}/g" config/deploy.yml
sed -i "s/host: my-app.example.com/host: ${{ secrets.KAMAL_DEPLOY_HOST }}/g" config/deploy.yml
sed -i "s/MyApp/${{ env.repository_name }}/g" config/deploy.yml
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}


- name: Setup dotnet
uses: actions/setup-dotnet@v3
username: ${{ env.KAMAL_REGISTRY_USERNAME }}
password: ${{ env.KAMAL_REGISTRY_PASSWORD }}

- name: Set up SSH key
uses: webfactory/ssh-agent@v0.9.0
with:
dotnet-version: '8.0'

- name: Install x tool
if: env.HAS_APPSETTINGS_PATCH == 'true'
run: dotnet tool install -g x

- name: Apply Production AppSettings
if: env.HAS_APPSETTINGS_PATCH == 'true'
working-directory: ./AiServer
run: |
cat <<EOF >> appsettings.json.patch
${{ secrets.APPSETTINGS_PATCH }}
EOF
x patch appsettings.json.patch

# Build and push new docker image, skip for manual redeploy other than 'latest'
- name: Build and push Docker image
run: |
dotnet publish --os linux --arch x64 -c Release -p:ContainerRepository=${{ env.image_repository_name }} -p:ContainerRegistry=ghcr.io -p:ContainerImageTags=${{ env.TAG_NAME }} -p:ContainerPort=80
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

deploy_via_ssh:
needs: push_to_registry
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion != 'failure' }}
steps:
# Checkout latest or specific tag
- name: checkout
if: ${{ github.event.inputs.version == '' || github.event.inputs.version == 'latest' }}
uses: actions/checkout@v3
- name: checkout tag
if: ${{ github.event.inputs.version != '' && github.event.inputs.version != 'latest' }}
uses: actions/checkout@v3
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ref: refs/tags/${{ github.event.inputs.version }}
ruby-version: 3.3.0
bundler-cache: true

- name: repository name fix and env
- name: Install Kamal
run: gem install kamal -v 2.3.0

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: image=moby/buildkit:master

- name: Kamal bootstrap
run: kamal server bootstrap

- name: Check if first run and execute kamal app boot if necessary
run: |
echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
echo "TAG_NAME=latest" >> $GITHUB_ENV
if [ "${{ github.event.release.tag_name }}" != "" ]; then
echo "TAG_NAME=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
fi;
if [ "${{ github.event.inputs.version }}" != "" ]; then
echo "TAG_NAME=${{ github.event.inputs.version }}" >> $GITHUB_ENV
fi;
FIRST_RUN_FILE=".${{ env.repository_name }}"
if ! kamal server exec --no-interactive -q "test -f $FIRST_RUN_FILE"; then
kamal server exec --no-interactive -q "touch $FIRST_RUN_FILE" || true
kamal deploy -q -P --version latest || true
else
echo "Not first run, skipping kamal app boot"
fi
- name: Create .env file
- name: Ensure file permissions
run: |
echo "Generating .env file"
echo "# Autogenerated .env file" > .deploy/.env
echo "HOST_DOMAIN=${{ secrets.DEPLOY_HOST }}" >> .deploy/.env
echo "LETSENCRYPT_EMAIL=${{ secrets.LETSENCRYPT_EMAIL }}" >> .deploy/.env
echo "APP_NAME=${{ github.event.repository.name }}" >> .deploy/.env
echo "IMAGE_REPO=${{ env.image_repository_name }}" >> .deploy/.env
echo "RELEASE_VERSION=${{ env.TAG_NAME }}" >> .deploy/.env
echo "CIVIT_AI_API_KEY=${{ secrets.CIVIT_AI_API_KEY }}" >> .deploy/.env
echo "REPLICATE_API_KEY=${{ secrets.REPLICATE_API_KEY }}" >> .deploy/.env
echo "GOOGLE_API_KEY=${{ secrets.GOOGLE_API_KEY }}" >> .deploy/.env
echo "GROQ_API_KEY=${{ secrets.GROQ_API_KEY }}" >> .deploy/.env
echo "MISTRAL_API_KEY=${{ secrets.MISTRAL_API_KEY }}" >> .deploy/.env
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> .deploy/.env
echo "OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}" >> .deploy/.env
# Copy only the docker-compose.yml to remote server home folder
- name: copy files to target server via scp
uses: appleboy/scp-action@v0.1.3
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USERNAME }}
port: 22
key: ${{ secrets.DEPLOY_KEY }}
strip_components: 2
source: "./.deploy/docker-compose.yml,./.deploy/.env"
target: "~/.deploy/${{ github.event.repository.name }}/"

- name: Setup App_Data volume directory
uses: appleboy/ssh-action@v0.1.5
env:
APPTOKEN: ${{ secrets.GITHUB_TOKEN }}
USERNAME: ${{ secrets.DEPLOY_USERNAME }}
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USERNAME }}
key: ${{ secrets.DEPLOY_KEY }}
port: 22
envs: APPTOKEN,USERNAME
script: |
set -e
echo $APPTOKEN | docker login ghcr.io -u $USERNAME --password-stdin
cd ~/.deploy/${{ github.event.repository.name }}
docker compose pull
export APP_ID=$(docker compose run --entrypoint "id -u" --rm app)
docker compose run --entrypoint "chown $APP_ID:$APP_ID /app/App_Data" --user root --rm app
docker compose run --entrypoint "chown $APP_ID:$APP_ID /app/artifacts" --user root --rm app
docker compose run --entrypoint "chown $APP_ID:$APP_ID /app/files" --user root --rm app
kamal server exec --no-interactive "mkdir -p /opt/docker/${{ env.repository_name }}/App_Data && chown -R 1654:1654 /opt/docker/${{ env.repository_name }}"
- name: Run remote db migrations
uses: appleboy/ssh-action@v0.1.5
env:
APPTOKEN: ${{ secrets.GITHUB_TOKEN }}
USERNAME: ${{ secrets.DEPLOY_USERNAME }}
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USERNAME }}
key: ${{ secrets.DEPLOY_KEY }}
port: 22
envs: APPTOKEN,USERNAME
script: |
set -e
echo $APPTOKEN | docker login ghcr.io -u $USERNAME --password-stdin
cd ~/.deploy/${{ github.event.repository.name }}
docker compose pull
export APP_ID=$(docker compose run --entrypoint "id -u" --rm app)
docker compose run --entrypoint "chown $APP_ID:$APP_ID /app/App_Data" --user root --rm app
docker compose run --entrypoint "chown $APP_ID:$APP_ID /app/artifacts" --user root --rm app
docker compose run --entrypoint "chown $APP_ID:$APP_ID /app/files" --user root --rm app
docker compose up app-migration --exit-code-from app-migration
- name: Migration
if: env.HAS_MIGRATIONS == 'true'
run: |
kamal server exec --no-interactive 'echo "${{ env.KAMAL_REGISTRY_PASSWORD }}" | docker login ghcr.io -u ${{ env.KAMAL_REGISTRY_USERNAME }} --password-stdin'
kamal server exec --no-interactive "docker pull ghcr.io/${{ env.image_repository_name }}:latest || true"
kamal app exec --no-reuse --no-interactive --version=latest "--AppTasks=migrate"
# Deploy Docker image with your application using `docker compose up` remotely
- name: remote docker-compose up via ssh
uses: appleboy/ssh-action@v0.1.5
env:
APPTOKEN: ${{ secrets.GITHUB_TOKEN }}
USERNAME: ${{ secrets.DEPLOY_USERNAME }}
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USERNAME }}
key: ${{ secrets.DEPLOY_KEY }}
port: 22
envs: APPTOKEN,USERNAME
script: |
echo $APPTOKEN | docker login ghcr.io -u $USERNAME --password-stdin
cd ~/.deploy/${{ github.event.repository.name }}
docker compose pull
docker compose up app -d
- name: Deploy with Kamal
run: |
kamal lock release -v
kamal deploy -P --version latest
3 changes: 3 additions & 0 deletions .kamal/hooks/docker-setup.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh

echo "Docker set up on $KAMAL_HOSTS..."
Loading

0 comments on commit a79d445

Please sign in to comment.