--- title: Deploying with Kamal --- All project templates includes the necessary GitHub Actions for CI/CD with automatic Docker image builds and production deployment with Kamal to any Linux server with SSH. Kamal is a CLI tool from the BaseCamp team that simplifies containerized application deployments, wrapping SSH and Docker to streamline self-hosting while maintaining GitHub Actions as the CI runner. ## Overview Kamal enables simple commands to deploy containerized applications to Linux hosts, handling: - **Docker containerization** with optimized .NET images - **Bootstrap configuration** - **Reverse proxy setup** - **SSL auto-certification** via Let's Encrypt - **Zero-downtime deployments** - **Health checks on deploy** - **Rolling back changes** - **Volume persistence** for App_Data including any SQLite database - **GitHub Container Registry** integration ::: info Kamal is built by the BaseCamp team, developers of Hey.com and BaseCamp.com. For comprehensive documentation on all Kamal features, visit [https://kamal-deploy.org](https://kamal-deploy.org) ::: ### Hosting Recommendation If you don't have a Linux Server, we recommend Hetzner [who we've found](https://docs.servicestack.net/ormlite/litestream#savings-at-scale) offers the best value [US Cloud VMs](https://cloud.hetzner.com) offering **$15/mo** for a dedicated VM with **2vCPU / 8GB** RAM which is being used to host **30 Docker Apps**, including all project template live demos - using the GitHub Actions included in each template. ## Getting Started ### Prerequisites - A domain name - A Linux host (e.g., VPS from providers like Hetzner) - GitHub account for CI/CD ### Initial Setup 1. Create a new ServiceStack application: :::sh npx create-net blazor-vue MyApp ::: 2. Create the SSH Private and Public Keys: :::sh ssh-keygen -t ed25519 -C "deploy@myapp" -f ./deploy-key ::: 3. Copy Public Key to deployment server to enable SSH access: :::sh cat ~/deploy-key.pub | ssh @ "cat >> ~/.ssh/authorized_keys" ::: 4. Configure GitHub Secrets for Kamal to access your deployment server: - SSH private key to access the server: ssh-rsa ... :::sh gh secret set SSH_PRIVATE_KEY < ~/deploy-key ::: - IP address of the server to deploy to: 100.100.100.100 :::sh gh secret set KAMAL_DEPLOY_IP [your.ip] ::: - Email for Let's Encrypt SSL certificate: me@example.org :::sh gh secret set LETSENCRYPT_EMAIL [your@email] ::: These Required variables can be globally configured in your GitHub Organization or User secrets which will enable deploying all your Repositories to the same server. Optionally configure any other global secrets to be shared by all Apps here: :::sh gh secret set SERVICESTACK_LICENSE [license-key] ::: 5. Configure GitHub Secrets for your App The only secret that needs to be configured per App is: Hostname used for SSL certificate and Kamal proxy: www.example.org :::sh gh secret set KAMAL_DEPLOY_HOST [www.example.org] ::: ### App Settings Secrets You could register any App-specific secrets here, although our preference is to instead save all your secrets in a single `APPSETTINGS_JSON` GitHub Action Secret which will get written inside the Docker container `appsettings.Production.json`, e.g: ```json { "ConnectionStrings": { "DefaultConnection": "Server=service-postgres;Port=5432;User Id=dbuser;Password=dbpass;Database=dbname;Pooling=true;" }, "SmtpConfig": { "UserName": "SmtpUser", "Password": "SmtpPass", "Host": "email-smtp.us-east-1.amazonaws.com", "Port": 587, "From": "noreply@example.org", "FromName": "MyApp", "Bcc": "copy@example.org" } }, "Admins": ["admin1@email.com","admin2@email.com"] } ``` After changing `appsettings.Production.json` update your `APPSETTINGS_JSON` GitHub Action Secret with: ```bash npm run secret:prod ``` This uses the GitHub CLI to add your `appsettings.Production.json` to your GitHub repository's Action secrets: ```bash gh secret set APPSETTINGS_JSON < appsettings.Production.json ``` **How It Works:** 1. **Development** - Create `appsettings.Production.json` locally with your production configuration 2. **Upload** - Run `npm run secret:prod` to store it as a GitHub Action secret (never committed to git) 3. **Deployment** - GitHub Actions injects the secret as the `APPSETTINGS_JSON_BASE64` environment variable 4. **Runtime** - The container startup script decodes and writes it to `/app/dotnet/appsettings.Production.json` 5. **Isolation** - The file is written with root-only permissions, preventing Node.js access Configuration in [config/deploy.yml](https://github.com/NetCoreTemplates/next-rsc/blob/main/config/deploy.yml): ```yaml # config/deploy.yml env: secret: - APPSETTINGS_JSON_BASE64 # Base64-encoded production config ``` **Benefits:** - Secrets never committed to git repository - Secrets never baked into Docker image layers - Same Docker image can be used across all environments - Production configuration remains isolated from Node.js process ### Inferred Variables These variables are inferred from the GitHub Action context and don't need to be configured. | Variable | Source | Description | |----------|--------|-------------| | `GITHUB_REPOSITORY` | `${{github.repository}}` | `acme/example.org` - used for service name and image | | `KAMAL_REGISTRY_USERNAME` | `${{github.actor}}` | GitHub username for container registry | | `KAMAL_REGISTRY_PASSWORD` | `${{secrets.GITHUB_TOKEN}}` | GitHub token for container registry auth | ### GitHub Actions The template includes a GitHub Actions workflow that is broken up into 3 steps that trigger on push to the `main` branch, and then on successful build and test, it will deploy the application to your server. ### Structure The deployment scripts are embedded in the templates [/.github/workflows](https://github.com/NetCoreTemplates/blazor-vue/tree/main/.github/workflows): ```files /.github/workflows build.yml build-container.yml release.yml /.kamal /hooks secrets /config deploy.yml ``` Which is triggered after a commit to main: - **build.yml** - Builds and tests the application, triggered on push to main - **build-container.yml** - Builds a Docker image from the application and pushes it to GitHub Container Registry - **release.yml** - Runs any pending DB Migrations, if successful Deploys the Docker image to server Your App's Kamal deployment is configured in [config/deploy.yml](https://github.com/NetCoreTemplates/blazor-vue/blob/main/config/deploy.yml) with additional Kamal hooks and configuration in [.kamal](https://github.com/NetCoreTemplates/blazor-vue/tree/main/.kamal), see [Kamal documentation](https://kamal-deploy.org/docs/configuration/) for more information. ### Configuration The [/config/deploy.yml](https://github.com/NetCoreTemplates/next-static/blob/main/config/deploy.yml) configuration is designed to be reusable across projects as it dynamically derives service names, image paths, and volume mounts from environment variables, so you only need to configure your server's IP and hostname using GitHub Action secrets. Once you make these changes, commit and push to your repository to trigger the GitHub Actions workflow. Kamal will deploy the required services including Docker and Kamal Proxy if your server doesn't already have them installed. :::info When using ASP.NET Core applications with Kamal-Proxy, ensure your application is running with the environment variable `ASPNETCORE_FORWARDEDHEADERS_ENABLED` set to `true`. The template has this by default, but if you are getting errors with 302 redirects, ensure this is set. ::: The authentication between GitHub Container Registry (ghcr.io) and your server is handled by the GitHub Actions workflow, the `deploy.yml` and [Kamal Secrets](https://kamal-deploy.org/docs/configuration/environment-variables/#secrets). ### Using Kamal Locally To also be able to use kamal locally to inspect logs or deploy from your own server you can add the GitHub Action Secrets into a local `.env` file, e.g: ```bash # Required for Kamal deploy.yml template processing GITHUB_REPOSITORY=acme/example.org # Server deployment details KAMAL_DEPLOY_IP=100.100.100.100 KAMAL_DEPLOY_HOST=example.org # Container registry credentials (for ghcr.io) KAMAL_REGISTRY_USERNAME=my-user GITHUB_TOKEN=ghp_xxx # Login to GitHub Container Registry #echo $KAMAL_REGISTRY_PASSWORD | docker login ghcr.io -u my-user --password-stdin ``` You can then load the environment variables into your shell with: :::sh source ./load-env.sh ::: Which will allow you to use kamal locally to access your deployment server, e.g: :::sh kamal app logs -f ::: :::info You can still use the Kamal CLI locally, but if you want to directly push deployments with `kamal deploy`, you will need to locally populate `KAMAL_REGISTRY_USER` and `KAMAL_REGISTRY_PASSWORD` with your GitHub username and a GitHub Personal Access Token with `read:packages` scope. ::: ### Hard code App specific variables Alternatively if you don't want to maintain a `.env` with GitHub Action Secrets you can hard-code all App-specific variables in your `deploy.yml` file so it doesn't need to perform any template processing for its Environment Variable substitutions. ## Common Kamal Commands Kamal provides several useful commands for managing your deployment: - View deployment details :::sh kamal details ::: - Check application logs :::sh kamal app logs -f ::: - Deploy new version :::sh kamal deploy ::: - Restart application :::sh kamal app boot ::: :::info Kamal commands are context-aware and will use the configuration from your current application directory. This makes managing multiple applications across different servers a lot easier as more applications are added. ::: ## Additional Containers Kamal supports extensive configuration options including "accessories" for additional features like databases, caches, and more. See the [Kamal documentation](https://kamal-deploy.org/docs/configuration/accessories/) for more information. ## Troubleshooting ### Initial deployment fails If you are having issues with the initial deployment, an earlier bootstrap of the server via GitHub Actions may have failed. Delete the `.` file in your deployment user's home directory and re-run the GitHub Actions workflow to re-bootstrap the server. ### Missing the service label If you are getting: Image ghcr.io/netcoreapps/northwindauto:latest is missing the 'service' label Ensure your AppHost **csproj** has `ContainerLabel` with the value matching the `service` in your `deploy.yml`.