Skip to content

Commit a79d445

Browse files
committed
Migrate to Kamal for deployment
1 parent 206d6ae commit a79d445

13 files changed

+536
-176
lines changed

.github/workflows/build-container.yml

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
name: Build Container
2+
permissions:
3+
packages: write
4+
contents: write
5+
on:
6+
workflow_run:
7+
workflows: ["Build"]
8+
types:
9+
- completed
10+
branches:
11+
- main
12+
- master
13+
workflow_dispatch:
14+
15+
env:
16+
DOCKER_BUILDKIT: 1
17+
KAMAL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
18+
KAMAL_REGISTRY_USERNAME: ${{ github.actor }}
19+
20+
jobs:
21+
build-container:
22+
runs-on: ubuntu-latest
23+
if: ${{ github.event.workflow_run.conclusion == 'success' }}
24+
steps:
25+
- name: Checkout code
26+
uses: actions/checkout@v3
27+
28+
- name: Set up environment variables
29+
run: |
30+
echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
31+
echo "repository_name=$(echo ${{ github.repository }} | cut -d '/' -f 2)" >> $GITHUB_ENV
32+
echo "repository_name_lower=$(echo ${{ github.repository }} | cut -d '/' -f 2 | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
33+
echo "org_name=$(echo ${{ github.repository }} | cut -d '/' -f 1)" >> $GITHUB_ENV
34+
if [ -n "${{ secrets.APPSETTINGS_PATCH }}" ]; then
35+
echo "HAS_APPSETTINGS_PATCH=true" >> $GITHUB_ENV
36+
else
37+
echo "HAS_APPSETTINGS_PATCH=false" >> $GITHUB_ENV
38+
fi
39+
if [ -n "${{ secrets.KAMAL_DEPLOY_IP }}" ]; then
40+
echo "HAS_DEPLOY_ACTION=true" >> $GITHUB_ENV
41+
else
42+
echo "HAS_DEPLOY_ACTION=false" >> $GITHUB_ENV
43+
fi
44+
45+
# This step is for the deployment of the templates only, safe to delete
46+
- name: Modify csproj for template deploy
47+
if: env.HAS_DEPLOY_ACTION == 'true'
48+
run: |
49+
sed -i 's#<ContainerLabel Include="service" Value="my-app" />#<ContainerLabel Include="service" Value="${{ env.repository_name_lower }}" />#g' MyApp/MyApp.csproj
50+
51+
- name: Check for Client directory and package.json
52+
id: check_client
53+
run: |
54+
if [ -d "MyApp.Client" ] && [ -f "MyApp.Client/package.json" ]; then
55+
echo "client_exists=true" >> $GITHUB_OUTPUT
56+
else
57+
echo "client_exists=false" >> $GITHUB_OUTPUT
58+
fi
59+
60+
- name: Setup Node.js
61+
if: steps.check_client.outputs.client_exists == 'true'
62+
uses: actions/setup-node@v3
63+
with:
64+
node-version: 22
65+
66+
- name: Install npm dependencies
67+
if: steps.check_client.outputs.client_exists == 'true'
68+
working-directory: ./MyApp.Client
69+
run: npm install
70+
71+
- name: Install x tool
72+
run: dotnet tool install -g x
73+
74+
- name: Apply Production AppSettings
75+
if: env.HAS_APPSETTINGS_PATCH == 'true'
76+
working-directory: ./MyApp
77+
run: |
78+
cat <<EOF >> appsettings.json.patch
79+
${{ secrets.APPSETTINGS_PATCH }}
80+
EOF
81+
x patch appsettings.json.patch
82+
83+
- name: Login to GitHub Container Registry
84+
uses: docker/login-action@v3
85+
with:
86+
registry: ghcr.io
87+
username: ${{ env.KAMAL_REGISTRY_USERNAME }}
88+
password: ${{ env.KAMAL_REGISTRY_PASSWORD }}
89+
90+
- name: Setup .NET
91+
uses: actions/setup-dotnet@v3
92+
with:
93+
dotnet-version: '8.0'
94+
95+
- name: Build and push Docker image
96+
run: |
97+
dotnet publish --os linux --arch x64 -c Release -p:ContainerRepository=${{ env.image_repository_name }} -p:ContainerRegistry=ghcr.io -p:ContainerImageTags=latest -p:ContainerPort=80

.github/workflows/release.yml

Lines changed: 80 additions & 176 deletions
Original file line numberDiff line numberDiff line change
@@ -3,202 +3,106 @@ permissions:
33
packages: write
44
contents: write
55
on:
6-
# Triggered on new GitHub Release
7-
release:
8-
types: [published]
9-
# Triggered on every successful Build action
106
workflow_run:
11-
workflows: ["Build"]
12-
branches: [main,master]
7+
workflows: ["Build Container"]
138
types:
149
- completed
15-
# Manual trigger for rollback to specific release or redeploy latest
10+
branches:
11+
- main
12+
- master
1613
workflow_dispatch:
17-
inputs:
18-
version:
19-
default: latest
20-
description: Tag you want to release.
21-
required: true
14+
15+
env:
16+
DOCKER_BUILDKIT: 1
17+
KAMAL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
18+
KAMAL_REGISTRY_USERNAME: ${{ github.actor }}
2219

2320
jobs:
24-
push_to_registry:
21+
release:
2522
runs-on: ubuntu-latest
26-
if: ${{ github.event.workflow_run.conclusion != 'failure' }}
23+
if: ${{ github.event.workflow_run.conclusion == 'success' }}
2724
steps:
28-
# Checkout latest or specific tag
29-
- name: checkout
30-
if: ${{ github.event.inputs.version == '' || github.event.inputs.version == 'latest' }}
31-
uses: actions/checkout@v3
32-
- name: checkout tag
33-
if: ${{ github.event.inputs.version != '' && github.event.inputs.version != 'latest' }}
25+
- name: Checkout code
3426
uses: actions/checkout@v3
35-
with:
36-
ref: refs/tags/${{ github.event.inputs.version }}
37-
38-
# Assign environment variables used in subsequent steps
39-
- name: Env variable assignment
40-
run: echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
41-
# TAG_NAME defaults to 'latest' if not a release or manual deployment
42-
- name: Assign version
27+
28+
- name: Set up environment variables
4329
run: |
44-
echo "TAG_NAME=latest" >> $GITHUB_ENV
45-
if [ "${{ github.event.release.tag_name }}" != "" ]; then
46-
echo "TAG_NAME=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
47-
fi;
48-
if [ "${{ github.event.inputs.version }}" != "" ]; then
49-
echo "TAG_NAME=${{ github.event.inputs.version }}" >> $GITHUB_ENV
50-
fi;
51-
if [ ! -z "${{ secrets.APPSETTINGS_PATCH }}" ]; then
52-
echo "HAS_APPSETTINGS_PATCH=true" >> $GITHUB_ENV
30+
echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
31+
echo "repository_name=$(echo ${{ github.repository }} | cut -d '/' -f 2)" >> $GITHUB_ENV
32+
echo "repository_name_lower=$(echo ${{ github.repository }} | cut -d '/' -f 2 | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
33+
echo "org_name=$(echo ${{ github.repository }} | cut -d '/' -f 1)" >> $GITHUB_ENV
34+
if find . -maxdepth 2 -type f -name "Configure.Db.Migrations.cs" | grep -q .; then
35+
echo "HAS_MIGRATIONS=true" >> $GITHUB_ENV
36+
else
37+
echo "HAS_MIGRATIONS=false" >> $GITHUB_ENV
38+
fi
39+
if [ -n "${{ secrets.KAMAL_DEPLOY_IP }}" ]; then
40+
echo "HAS_DEPLOY_ACTION=true" >> $GITHUB_ENV
5341
else
54-
echo "HAS_APPSETTINGS_PATCH=false" >> $GITHUB_ENV
55-
fi;
56-
42+
echo "HAS_DEPLOY_ACTION=false" >> $GITHUB_ENV
43+
fi
44+
45+
# This step is for the deployment of the templates only, safe to delete
46+
- name: Modify deploy.yml
47+
if: env.HAS_DEPLOY_ACTION == 'true'
48+
run: |
49+
sed -i "s/service: my-app/service: ${{ env.repository_name_lower }}/g" config/deploy.yml
50+
sed -i "s#image: my-user/myapp#image: ${{ env.image_repository_name }}#g" config/deploy.yml
51+
sed -i "s/- 192.168.0.1/- ${{ secrets.KAMAL_DEPLOY_IP }}/g" config/deploy.yml
52+
sed -i "s/host: my-app.example.com/host: ${{ secrets.KAMAL_DEPLOY_HOST }}/g" config/deploy.yml
53+
sed -i "s/MyApp/${{ env.repository_name }}/g" config/deploy.yml
54+
5755
- name: Login to GitHub Container Registry
58-
uses: docker/login-action@v2
56+
uses: docker/login-action@v3
5957
with:
6058
registry: ghcr.io
61-
username: ${{ github.actor }}
62-
password: ${{ secrets.GITHUB_TOKEN }}
63-
64-
65-
- name: Setup dotnet
66-
uses: actions/setup-dotnet@v3
59+
username: ${{ env.KAMAL_REGISTRY_USERNAME }}
60+
password: ${{ env.KAMAL_REGISTRY_PASSWORD }}
61+
62+
- name: Set up SSH key
63+
uses: webfactory/[email protected]
6764
with:
68-
dotnet-version: '8.0'
69-
70-
- name: Install x tool
71-
if: env.HAS_APPSETTINGS_PATCH == 'true'
72-
run: dotnet tool install -g x
73-
74-
- name: Apply Production AppSettings
75-
if: env.HAS_APPSETTINGS_PATCH == 'true'
76-
working-directory: ./AiServer
77-
run: |
78-
cat <<EOF >> appsettings.json.patch
79-
${{ secrets.APPSETTINGS_PATCH }}
80-
EOF
81-
x patch appsettings.json.patch
82-
83-
84-
# Build and push new docker image, skip for manual redeploy other than 'latest'
85-
- name: Build and push Docker image
86-
run: |
87-
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
65+
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
8866

89-
deploy_via_ssh:
90-
needs: push_to_registry
91-
runs-on: ubuntu-latest
92-
if: ${{ github.event.workflow_run.conclusion != 'failure' }}
93-
steps:
94-
# Checkout latest or specific tag
95-
- name: checkout
96-
if: ${{ github.event.inputs.version == '' || github.event.inputs.version == 'latest' }}
97-
uses: actions/checkout@v3
98-
- name: checkout tag
99-
if: ${{ github.event.inputs.version != '' && github.event.inputs.version != 'latest' }}
100-
uses: actions/checkout@v3
67+
- name: Setup Ruby
68+
uses: ruby/setup-ruby@v1
10169
with:
102-
ref: refs/tags/${{ github.event.inputs.version }}
70+
ruby-version: 3.3.0
71+
bundler-cache: true
10372

104-
- name: repository name fix and env
73+
- name: Install Kamal
74+
run: gem install kamal -v 2.3.0
75+
76+
- name: Set up Docker Buildx
77+
uses: docker/setup-buildx-action@v3
78+
with:
79+
driver-opts: image=moby/buildkit:master
80+
81+
- name: Kamal bootstrap
82+
run: kamal server bootstrap
83+
84+
- name: Check if first run and execute kamal app boot if necessary
10585
run: |
106-
echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
107-
echo "TAG_NAME=latest" >> $GITHUB_ENV
108-
if [ "${{ github.event.release.tag_name }}" != "" ]; then
109-
echo "TAG_NAME=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
110-
fi;
111-
if [ "${{ github.event.inputs.version }}" != "" ]; then
112-
echo "TAG_NAME=${{ github.event.inputs.version }}" >> $GITHUB_ENV
113-
fi;
86+
FIRST_RUN_FILE=".${{ env.repository_name }}"
87+
if ! kamal server exec --no-interactive -q "test -f $FIRST_RUN_FILE"; then
88+
kamal server exec --no-interactive -q "touch $FIRST_RUN_FILE" || true
89+
kamal deploy -q -P --version latest || true
90+
else
91+
echo "Not first run, skipping kamal app boot"
92+
fi
11493
115-
- name: Create .env file
94+
- name: Ensure file permissions
11695
run: |
117-
echo "Generating .env file"
118-
119-
echo "# Autogenerated .env file" > .deploy/.env
120-
echo "HOST_DOMAIN=${{ secrets.DEPLOY_HOST }}" >> .deploy/.env
121-
echo "LETSENCRYPT_EMAIL=${{ secrets.LETSENCRYPT_EMAIL }}" >> .deploy/.env
122-
echo "APP_NAME=${{ github.event.repository.name }}" >> .deploy/.env
123-
echo "IMAGE_REPO=${{ env.image_repository_name }}" >> .deploy/.env
124-
echo "RELEASE_VERSION=${{ env.TAG_NAME }}" >> .deploy/.env
125-
echo "CIVIT_AI_API_KEY=${{ secrets.CIVIT_AI_API_KEY }}" >> .deploy/.env
126-
echo "REPLICATE_API_KEY=${{ secrets.REPLICATE_API_KEY }}" >> .deploy/.env
127-
echo "GOOGLE_API_KEY=${{ secrets.GOOGLE_API_KEY }}" >> .deploy/.env
128-
echo "GROQ_API_KEY=${{ secrets.GROQ_API_KEY }}" >> .deploy/.env
129-
echo "MISTRAL_API_KEY=${{ secrets.MISTRAL_API_KEY }}" >> .deploy/.env
130-
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> .deploy/.env
131-
echo "OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}" >> .deploy/.env
132-
133-
# Copy only the docker-compose.yml to remote server home folder
134-
- name: copy files to target server via scp
135-
uses: appleboy/[email protected]
136-
with:
137-
host: ${{ secrets.DEPLOY_HOST }}
138-
username: ${{ secrets.DEPLOY_USERNAME }}
139-
port: 22
140-
key: ${{ secrets.DEPLOY_KEY }}
141-
strip_components: 2
142-
source: "./.deploy/docker-compose.yml,./.deploy/.env"
143-
target: "~/.deploy/${{ github.event.repository.name }}/"
144-
145-
- name: Setup App_Data volume directory
146-
uses: appleboy/[email protected]
147-
env:
148-
APPTOKEN: ${{ secrets.GITHUB_TOKEN }}
149-
USERNAME: ${{ secrets.DEPLOY_USERNAME }}
150-
with:
151-
host: ${{ secrets.DEPLOY_HOST }}
152-
username: ${{ secrets.DEPLOY_USERNAME }}
153-
key: ${{ secrets.DEPLOY_KEY }}
154-
port: 22
155-
envs: APPTOKEN,USERNAME
156-
script: |
157-
set -e
158-
echo $APPTOKEN | docker login ghcr.io -u $USERNAME --password-stdin
159-
cd ~/.deploy/${{ github.event.repository.name }}
160-
docker compose pull
161-
export APP_ID=$(docker compose run --entrypoint "id -u" --rm app)
162-
docker compose run --entrypoint "chown $APP_ID:$APP_ID /app/App_Data" --user root --rm app
163-
docker compose run --entrypoint "chown $APP_ID:$APP_ID /app/artifacts" --user root --rm app
164-
docker compose run --entrypoint "chown $APP_ID:$APP_ID /app/files" --user root --rm app
96+
kamal server exec --no-interactive "mkdir -p /opt/docker/${{ env.repository_name }}/App_Data && chown -R 1654:1654 /opt/docker/${{ env.repository_name }}"
16597
166-
- name: Run remote db migrations
167-
uses: appleboy/[email protected]
168-
env:
169-
APPTOKEN: ${{ secrets.GITHUB_TOKEN }}
170-
USERNAME: ${{ secrets.DEPLOY_USERNAME }}
171-
with:
172-
host: ${{ secrets.DEPLOY_HOST }}
173-
username: ${{ secrets.DEPLOY_USERNAME }}
174-
key: ${{ secrets.DEPLOY_KEY }}
175-
port: 22
176-
envs: APPTOKEN,USERNAME
177-
script: |
178-
set -e
179-
echo $APPTOKEN | docker login ghcr.io -u $USERNAME --password-stdin
180-
cd ~/.deploy/${{ github.event.repository.name }}
181-
docker compose pull
182-
export APP_ID=$(docker compose run --entrypoint "id -u" --rm app)
183-
docker compose run --entrypoint "chown $APP_ID:$APP_ID /app/App_Data" --user root --rm app
184-
docker compose run --entrypoint "chown $APP_ID:$APP_ID /app/artifacts" --user root --rm app
185-
docker compose run --entrypoint "chown $APP_ID:$APP_ID /app/files" --user root --rm app
186-
docker compose up app-migration --exit-code-from app-migration
98+
- name: Migration
99+
if: env.HAS_MIGRATIONS == 'true'
100+
run: |
101+
kamal server exec --no-interactive 'echo "${{ env.KAMAL_REGISTRY_PASSWORD }}" | docker login ghcr.io -u ${{ env.KAMAL_REGISTRY_USERNAME }} --password-stdin'
102+
kamal server exec --no-interactive "docker pull ghcr.io/${{ env.image_repository_name }}:latest || true"
103+
kamal app exec --no-reuse --no-interactive --version=latest "--AppTasks=migrate"
187104
188-
# Deploy Docker image with your application using `docker compose up` remotely
189-
- name: remote docker-compose up via ssh
190-
uses: appleboy/[email protected]
191-
env:
192-
APPTOKEN: ${{ secrets.GITHUB_TOKEN }}
193-
USERNAME: ${{ secrets.DEPLOY_USERNAME }}
194-
with:
195-
host: ${{ secrets.DEPLOY_HOST }}
196-
username: ${{ secrets.DEPLOY_USERNAME }}
197-
key: ${{ secrets.DEPLOY_KEY }}
198-
port: 22
199-
envs: APPTOKEN,USERNAME
200-
script: |
201-
echo $APPTOKEN | docker login ghcr.io -u $USERNAME --password-stdin
202-
cd ~/.deploy/${{ github.event.repository.name }}
203-
docker compose pull
204-
docker compose up app -d
105+
- name: Deploy with Kamal
106+
run: |
107+
kamal lock release -v
108+
kamal deploy -P --version latest

.kamal/hooks/docker-setup.sample

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/sh
2+
3+
echo "Docker set up on $KAMAL_HOSTS..."

0 commit comments

Comments
 (0)