diff --git a/.deploy/docker-compose.yml b/.deploy/docker-compose.yml
deleted file mode 100644
index 12174d1..0000000
--- a/.deploy/docker-compose.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-version: "3.9"
-services:
- app:
- image: ghcr.io/${IMAGE_REPO}:${RELEASE_VERSION}
- restart: always
- ports:
- - "8080"
- container_name: ${APP_NAME}_app
- environment:
- VIRTUAL_HOST: ${HOST_DOMAIN}
- VIRTUAL_PORT: 8080 # New default ASP.NET port -> https://learn.microsoft.com/en-us/dotnet/core/compatibility/containers/8.0/aspnet-port
- LETSENCRYPT_HOST: ${HOST_DOMAIN}
- LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL}
- volumes:
- - ./App_Data:/app/App_Data
-
- app-migration:
- image: ghcr.io/${IMAGE_REPO}:${RELEASE_VERSION}
- restart: "no"
- container_name: ${APP_NAME}_app_migration
- profiles:
- - migration
- command: --AppTasks=migrate
- volumes:
- - ./App_Data:/app/App_Data
-
-networks:
- default:
- external: true
- name: nginx
diff --git a/.deploy/nginx-proxy-compose.yml b/.deploy/nginx-proxy-compose.yml
deleted file mode 100644
index 14a709c..0000000
--- a/.deploy/nginx-proxy-compose.yml
+++ /dev/null
@@ -1,46 +0,0 @@
-version: "3.9"
-
-services:
- nginx-proxy:
- image: nginxproxy/nginx-proxy
- container_name: nginx-proxy
- restart: always
- ports:
- - "80:80"
- - "443:443"
- volumes:
- - conf:/etc/nginx/conf.d
- - vhost:/etc/nginx/vhost.d
- - html:/usr/share/nginx/html
- - dhparam:/etc/nginx/dhparam
- - certs:/etc/nginx/certs:ro
- - /var/run/docker.sock:/tmp/docker.sock:ro
- labels:
- - "com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy"
-
- letsencrypt:
- image: nginxproxy/acme-companion:2.2
- container_name: nginx-proxy-le
- restart: always
- depends_on:
- - "nginx-proxy"
- environment:
- - DEFAULT_EMAIL=you@example.com
- volumes:
- - certs:/etc/nginx/certs:rw
- - acme:/etc/acme.sh
- - vhost:/etc/nginx/vhost.d
- - html:/usr/share/nginx/html
- - /var/run/docker.sock:/var/run/docker.sock:ro
-
-networks:
- default:
- name: nginx
-
-volumes:
- conf:
- vhost:
- html:
- dhparam:
- certs:
- acme:
\ No newline at end of file
diff --git a/.github/workflows/README.md b/.github/workflows/README.md
deleted file mode 100644
index f6656a2..0000000
--- a/.github/workflows/README.md
+++ /dev/null
@@ -1,99 +0,0 @@
-## Overview
-
-This template uses the deployment configurations for a ServiceStack .NET 8 application. The application is containerized using Docker and is set up to be automatically built and deployed via GitHub Actions. The recommended deployment target is a stand-alone Linux server running Ubuntu, with an NGINX reverse proxy also containerized using Docker, which a Docker Compose file is included in the template under the `.deploy` directory.
-
-### Highlights
-- 🌐 **NGINX Reverse Proxy**: Utilizes an NGINX reverse proxy to handle web traffic and SSL termination.
-- 🚀 **GitHub Actions**: Leverages GitHub Actions for CI/CD, pushing Docker images to GitHub Container Registry and deploying them on a remote server.
-- 🐳 **Dockerized ServiceStack App**: The application is containerized, with the image built using `.NET 8`.
-- 🔄 **Automated Migrations**: Includes a separate service for running database migrations.
-
-### Technology Stack
-- **Web Framework**: ServiceStack
-- **Language**: C# (.NET 8)
-- **Containerization**: Docker
-- **Reverse Proxy**: NGINX
-- **CI/CD**: GitHub Actions
-- **OS**: Ubuntu 22.04 (Deployment Server)
-
-
-
-## Deployment Server Setup
-
-To successfully host your ServiceStack applications, there are several components you need to set up on your deployment server. This guide assumes you're working on a standalone Linux server (Ubuntu is recommended) with SSH access enabled.
-
-### Prerequisites
-
-1. **SSH Access**: Required for GitHub Actions to communicate with your server.
-2. **Docker**: To containerize your application.
-3. **Docker-Compose**: For orchestrating multiple containers.
-4. **Ports**: 80 and 443 should be open for web access.
-5. **nginx-reverse-proxy**: For routing traffic to multiple ServiceStack applications and managing TLS certificates.
-
-You can use any cloud-hosted or on-premises server like Digital Ocean, AWS, Azure, etc., for this setup.
-
-### Step-by-Step Guide
-
-#### 1. Install Docker and Docker-Compose
-
-It is best to follow the [latest installation instructions on the Docker website](https://docs.docker.com/engine/install/ubuntu/) to ensure to have the correct setup with the latest patches.
-
-#### 2. Configure SSH for GitHub Actions
-
-Generate a dedicated SSH key pair to be used by GitHub Actions:
-
-```bash
-ssh-keygen -t rsa -b 4096 -f ~/.ssh/github_actions
-```
-
-Add the public key to the `authorized_keys` file on your server:
-
-```bash
-cat ~/.ssh/github_actions.pub >> ~/.ssh/authorized_keys
-```
-
-Then, add the *private* key to your GitHub Secrets as `DEPLOY_KEY` to enable GitHub Actions to SSH into the server securely.
-
-#### 3. Set Up nginx-reverse-proxy
-
-You should have a `docker-compose` file similar to the `nginx-proxy-compose.yml` in your repository. Upload this file to your server:
-
-```bash
-scp nginx-proxy-compose.yml user@your_server:~/
-```
-
-To bring up the nginx reverse proxy and its companion container for handling TLS certificates, run:
-
-```bash
-docker compose -f ~/nginx-proxy-compose.yml up -d
-```
-
-This will start an nginx reverse proxy along with a companion container. They will automatically watch for additional Docker containers on the same network and initialize them with valid TLS certificates.
-
-
-
-## GitHub Repository Setup
-
-Configuring your GitHub repository is an essential step for automating deployments via GitHub Actions. This guide assumes you have a `release.yml` workflow file in your repository's `.github/workflows/` directory, and your deployment server has been set up according to the [Deployment Server Setup](#Deployment-Server-Setup) guidelines.
-
-### Secrets Configuration
-
-Your GitHub Actions workflow requires the following secrets to be set in your GitHub repository:
-
-1. **`DEPLOY_HOST`**: The hostname for SSH access. This can be either an IP address or a domain with an A-record pointing to your server.
-2. **`DEPLOY_USERNAME`**: The username for SSH login. Common examples include `ubuntu`, `ec2-user`, or `root`.
-3. **`DEPLOY_KEY`**: The SSH private key to securely access the deployment server. This should be the same key you've set up on your server for GitHub Actions.
-4. **`LETSENCRYPT_EMAIL`**: Your email address, required for Let's Encrypt automated TLS certificates.
-
-#### Using GitHub CLI for Secret Management
-
-You can conveniently set these secrets using the [GitHub CLI](https://cli.github.com/manual/gh_secret_set) like this:
-
-```bash
-gh secret set DEPLOY_HOST --body="your-host-or-ip"
-gh secret set DEPLOY_USERNAME --body="your-username"
-gh secret set DEPLOY_KEY --bodyFile="path/to/your/ssh-private-key"
-gh secret set LETSENCRYPT_EMAIL --body="your-email@example.com"
-```
-
-These secrets will populate environment variables within your GitHub Actions workflow and other configuration files, enabling secure and automated deployment of your ServiceStack applications.
diff --git a/.github/workflows/build-container.yml b/.github/workflows/build-container.yml
new file mode 100644
index 0000000..82925e1
--- /dev/null
+++ b/.github/workflows/build-container.yml
@@ -0,0 +1,94 @@
+name: Build Container
+permissions:
+ packages: write
+ contents: write
+on:
+ workflow_run:
+ workflows: ["Build"]
+ types:
+ - completed
+ 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###g' MyApp/MyApp.csproj
+
+ - name: Check for Client directory
+ id: check_client
+ run: |
+ if [ -d "MyApp.Client" ]; 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 <> 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
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index ac95bdc..6a41086 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -3,162 +3,99 @@ 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
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 "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_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: |
- 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;
-
+ 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 node
- uses: actions/setup-node@v3
+ username: ${{ env.KAMAL_REGISTRY_USERNAME }}
+ password: ${{ env.KAMAL_REGISTRY_PASSWORD }}
+
+ - name: Set up SSH key
+ uses: webfactory/ssh-agent@v0.9.0
with:
- node-version: 22
- # Run npm install
- - name: Install and build npm
- run: |
- npm install
- working-directory: ./MyApp.Client
-
- - name: Setup dotnet
- uses: actions/setup-dotnet@v3
+ ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
+
+ - name: Setup Ruby
+ uses: ruby/setup-ruby@v1
with:
- dotnet-version: '8.*'
-
- # 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
+ ruby-version: 3.3.0
+ bundler-cache: true
- 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: Install Kamal
+ run: gem install kamal -v 2.2.2
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
with:
- ref: refs/tags/${{ github.event.inputs.version }}
+ driver-opts: image=moby/buildkit:master
- - name: repository name fix and env
- 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;
+ - name: Kamal bootstrap
+ run: kamal server bootstrap
- - name: Create .env file
+ - name: Check if first run and execute kamal app boot if necessary
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
-
- # 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: 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 up app-migration --exit-code-from app-migration
+ 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
- # 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: Ensure file permissions
+ run: kamal server exec --no-interactive "mkdir -p /opt/docker/${{ env.repository_name }}/App_Data && chown -R 1654:1654 /opt/docker/${{ env.repository_name }}"
+
+ - name: Migration
+ if: env.HAS_MIGRATIONS == 'true'
+ run: kamal app exec --no-reuse --no-interactive --version=latest "--AppTasks=migrate"
+
+ - name: Deploy with Kamal
+ run: |
+ kamal lock release -v
+ kamal deploy -P --version latest
\ No newline at end of file
diff --git a/.kamal/hooks/docker-setup.sample b/.kamal/hooks/docker-setup.sample
new file mode 100644
index 0000000..2fb07d7
--- /dev/null
+++ b/.kamal/hooks/docker-setup.sample
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+echo "Docker set up on $KAMAL_HOSTS..."
diff --git a/.kamal/hooks/post-deploy.sample b/.kamal/hooks/post-deploy.sample
new file mode 100644
index 0000000..75efafc
--- /dev/null
+++ b/.kamal/hooks/post-deploy.sample
@@ -0,0 +1,14 @@
+#!/bin/sh
+
+# A sample post-deploy hook
+#
+# These environment variables are available:
+# KAMAL_RECORDED_AT
+# KAMAL_PERFORMER
+# KAMAL_VERSION
+# KAMAL_HOSTS
+# KAMAL_ROLE (if set)
+# KAMAL_DESTINATION (if set)
+# KAMAL_RUNTIME
+
+echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds"
diff --git a/.kamal/hooks/post-proxy-reboot.sample b/.kamal/hooks/post-proxy-reboot.sample
new file mode 100644
index 0000000..1435a67
--- /dev/null
+++ b/.kamal/hooks/post-proxy-reboot.sample
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+echo "Rebooted kamal-proxy on $KAMAL_HOSTS"
diff --git a/.kamal/hooks/pre-build.sample b/.kamal/hooks/pre-build.sample
new file mode 100644
index 0000000..f87d811
--- /dev/null
+++ b/.kamal/hooks/pre-build.sample
@@ -0,0 +1,51 @@
+#!/bin/sh
+
+# A sample pre-build hook
+#
+# Checks:
+# 1. We have a clean checkout
+# 2. A remote is configured
+# 3. The branch has been pushed to the remote
+# 4. The version we are deploying matches the remote
+#
+# These environment variables are available:
+# KAMAL_RECORDED_AT
+# KAMAL_PERFORMER
+# KAMAL_VERSION
+# KAMAL_HOSTS
+# KAMAL_ROLE (if set)
+# KAMAL_DESTINATION (if set)
+
+if [ -n "$(git status --porcelain)" ]; then
+ echo "Git checkout is not clean, aborting..." >&2
+ git status --porcelain >&2
+ exit 1
+fi
+
+first_remote=$(git remote)
+
+if [ -z "$first_remote" ]; then
+ echo "No git remote set, aborting..." >&2
+ exit 1
+fi
+
+current_branch=$(git branch --show-current)
+
+if [ -z "$current_branch" ]; then
+ echo "Not on a git branch, aborting..." >&2
+ exit 1
+fi
+
+remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1)
+
+if [ -z "$remote_head" ]; then
+ echo "Branch not pushed to remote, aborting..." >&2
+ exit 1
+fi
+
+if [ "$KAMAL_VERSION" != "$remote_head" ]; then
+ echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2
+ exit 1
+fi
+
+exit 0
diff --git a/.kamal/hooks/pre-connect.sample b/.kamal/hooks/pre-connect.sample
new file mode 100644
index 0000000..18e61d7
--- /dev/null
+++ b/.kamal/hooks/pre-connect.sample
@@ -0,0 +1,47 @@
+#!/usr/bin/env ruby
+
+# A sample pre-connect check
+#
+# Warms DNS before connecting to hosts in parallel
+#
+# These environment variables are available:
+# KAMAL_RECORDED_AT
+# KAMAL_PERFORMER
+# KAMAL_VERSION
+# KAMAL_HOSTS
+# KAMAL_ROLE (if set)
+# KAMAL_DESTINATION (if set)
+# KAMAL_RUNTIME
+
+hosts = ENV["KAMAL_HOSTS"].split(",")
+results = nil
+max = 3
+
+elapsed = Benchmark.realtime do
+ results = hosts.map do |host|
+ Thread.new do
+ tries = 1
+
+ begin
+ Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME)
+ rescue SocketError
+ if tries < max
+ puts "Retrying DNS warmup: #{host}"
+ tries += 1
+ sleep rand
+ retry
+ else
+ puts "DNS warmup failed: #{host}"
+ host
+ end
+ end
+
+ tries
+ end
+ end.map(&:value)
+end
+
+retries = results.sum - hosts.size
+nopes = results.count { |r| r == max }
+
+puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ]
diff --git a/.kamal/hooks/pre-deploy.sample b/.kamal/hooks/pre-deploy.sample
new file mode 100644
index 0000000..1b280c7
--- /dev/null
+++ b/.kamal/hooks/pre-deploy.sample
@@ -0,0 +1,109 @@
+#!/usr/bin/env ruby
+
+# A sample pre-deploy hook
+#
+# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds.
+#
+# Fails unless the combined status is "success"
+#
+# These environment variables are available:
+# KAMAL_RECORDED_AT
+# KAMAL_PERFORMER
+# KAMAL_VERSION
+# KAMAL_HOSTS
+# KAMAL_COMMAND
+# KAMAL_SUBCOMMAND
+# KAMAL_ROLE (if set)
+# KAMAL_DESTINATION (if set)
+
+# Only check the build status for production deployments
+if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production"
+ exit 0
+end
+
+require "bundler/inline"
+
+# true = install gems so this is fast on repeat invocations
+gemfile(true, quiet: true) do
+ source "https://rubygems.org"
+
+ gem "octokit"
+ gem "faraday-retry"
+end
+
+MAX_ATTEMPTS = 72
+ATTEMPTS_GAP = 10
+
+def exit_with_error(message)
+ $stderr.puts message
+ exit 1
+end
+
+class GithubStatusChecks
+ attr_reader :remote_url, :git_sha, :github_client, :combined_status
+
+ def initialize
+ @remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/")
+ @git_sha = `git rev-parse HEAD`.strip
+ @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
+ refresh!
+ end
+
+ def refresh!
+ @combined_status = github_client.combined_status(remote_url, git_sha)
+ end
+
+ def state
+ combined_status[:state]
+ end
+
+ def first_status_url
+ first_status = combined_status[:statuses].find { |status| status[:state] == state }
+ first_status && first_status[:target_url]
+ end
+
+ def complete_count
+ combined_status[:statuses].count { |status| status[:state] != "pending"}
+ end
+
+ def total_count
+ combined_status[:statuses].count
+ end
+
+ def current_status
+ if total_count > 0
+ "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..."
+ else
+ "Build not started..."
+ end
+ end
+end
+
+
+$stdout.sync = true
+
+puts "Checking build status..."
+attempts = 0
+checks = GithubStatusChecks.new
+
+begin
+ loop do
+ case checks.state
+ when "success"
+ puts "Checks passed, see #{checks.first_status_url}"
+ exit 0
+ when "failure"
+ exit_with_error "Checks failed, see #{checks.first_status_url}"
+ when "pending"
+ attempts += 1
+ end
+
+ exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS
+
+ puts checks.current_status
+ sleep(ATTEMPTS_GAP)
+ checks.refresh!
+ end
+rescue Octokit::NotFound
+ exit_with_error "Build status could not be found"
+end
diff --git a/.kamal/hooks/pre-proxy-reboot.sample b/.kamal/hooks/pre-proxy-reboot.sample
new file mode 100644
index 0000000..061f805
--- /dev/null
+++ b/.kamal/hooks/pre-proxy-reboot.sample
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+echo "Rebooting kamal-proxy on $KAMAL_HOSTS..."
diff --git a/.kamal/secrets b/.kamal/secrets
new file mode 100644
index 0000000..690aa96
--- /dev/null
+++ b/.kamal/secrets
@@ -0,0 +1,18 @@
+# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets,
+# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either
+# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git.
+
+# Option 1: Read secrets from the environment
+KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
+KAMAL_REGISTRY_USERNAME=$KAMAL_REGISTRY_USERNAME
+
+# Option 2: Read secrets via a command
+# RAILS_MASTER_KEY=$(cat config/master.key)
+
+# Option 3: Read secrets via kamal secrets helpers
+# These will handle logging in and fetching the secrets in as few calls as possible
+# There are adapters for 1Password, LastPass + Bitwarden
+#
+# SECRETS=$(kamal secrets fetch --adapter 1password --account my-account --from MyVault/MyItem KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY)
+# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD $SECRETS)
+# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)
diff --git a/MyApp/MyApp.csproj b/MyApp/MyApp.csproj
index 5db36c6..e362214 100644
--- a/MyApp/MyApp.csproj
+++ b/MyApp/MyApp.csproj
@@ -10,7 +10,11 @@
https://localhost:5173
MyApp
DefaultContainer
-
+
+
+
+
+
diff --git a/config/deploy.yml b/config/deploy.yml
new file mode 100644
index 0000000..56e655c
--- /dev/null
+++ b/config/deploy.yml
@@ -0,0 +1,57 @@
+# Name of your application. Used to uniquely configure containers.
+service: my-app
+
+# Name of the container image.
+image: my-user/myapp
+
+# Deploy to these servers.
+servers:
+ # IP address of server, optionally use env variable.
+ web:
+ - 192.168.0.1
+# - <%= ENV['KAMAL_DEPLOY_IP'] %>
+
+
+# Enable SSL auto certification via Let's Encrypt (and allow for multiple apps on one server).
+# If using something like Cloudflare, it is recommended to set encryption mode
+# in Cloudflare's SSL/TLS setting to "Full" to enable end-to-end encryption.
+proxy:
+ ssl: true
+ host: my-app.example.com
+ # kamal-proxy connects to your container over port 80, use `app_port` to specify a different port.
+ app_port: 8080
+
+ healthcheck:
+ interval: 3
+ path: /metadata
+ timeout: 3
+
+# Credentials for your image host.
+registry:
+ # Specify the registry server, if you're not using Docker Hub
+ server: ghcr.io
+ username:
+ - KAMAL_REGISTRY_USERNAME
+
+ # Always use an access token rather than real password (pulled from .kamal/secrets).
+ password:
+ - KAMAL_REGISTRY_PASSWORD
+
+# Configure builder setup.
+builder:
+ arch: amd64
+
+volumes:
+ - "/opt/docker/MyApp/App_Data:/app/App_Data"
+
+#accessories:
+# litestream:
+# roles: ["web"]
+# image: litestream/litestream
+# files: ["config/litestream.yml:/etc/litestream.yml"]
+# volumes: ["/opt/docker/MyApp/App_Data:/data"]
+# cmd: replicate
+# env:
+# secret:
+# - ACCESS_KEY_ID
+# - SECRET_ACCESS_KEY