Skip to content

Commit

Permalink
feat: kamal deploy (#307)
Browse files Browse the repository at this point in the history
* chore: add curl

* feat: add kamal deployment
  • Loading branch information
bjarneo authored Jun 8, 2024
1 parent a20a874 commit 8a20bcb
Show file tree
Hide file tree
Showing 12 changed files with 340 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings

SECRET_DATABASE_URL="file:../database/hemmelig.db"
SECRET_DATABASE_URL="file:../database/hemmelig.db"
49 changes: 49 additions & 0 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Deploy instance
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 15
env:
SECRET_MASTER_KEY: ${{ secrets.SECRET_MASTER_KEY }}
SECRET_JWT_SECRET: ${{ secrets.SECRET_JWT_SECRET }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Setup deploy file
run: echo ${{ secrets.KAMAL_DEPLOY_YAML }} | base64 -d > config/deploy.yml

- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3.0
bundler-cache: true

- name: Install Kamal
run: gem install kamal

- name: Start SSH Agent
uses: webfactory/[email protected]
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

- name: Release the kamal lock
run: kamal lock release

- name: Deploy
run: kamal redeploy
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

.env.private
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

Expand Down
7 changes: 7 additions & 0 deletions .kamal/hooks/docker-setup.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env ruby

# A sample docker-setup hook
#
# Sets up a Docker network which can then be used by the application’s containers

ssh user@example.com docker network create kamal
14 changes: 14 additions & 0 deletions .kamal/hooks/post-deploy.sample
Original file line number Diff line number Diff line change
@@ -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"
3 changes: 3 additions & 0 deletions .kamal/hooks/post-traefik-reboot.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh

echo "Rebooted Traefik on $KAMAL_HOSTS"
51 changes: 51 additions & 0 deletions .kamal/hooks/pre-build.sample
Original file line number Diff line number Diff line change
@@ -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
47 changes: 47 additions & 0 deletions .kamal/hooks/pre-connect.sample
Original file line number Diff line number Diff line change
@@ -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 ]
109 changes: 109 additions & 0 deletions .kamal/hooks/pre-deploy.sample
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions .kamal/hooks/pre-traefik-reboot.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh

echo "Rebooting Traefik on $KAMAL_HOSTS..."
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ RUN npm run build
# Get ready for step two of the docker image build
FROM node:20-alpine

RUN apk add curl

WORKDIR /home/node/hemmelig

COPY --from=0 /usr/src/app/client/build client/build
Expand Down
53 changes: 53 additions & 0 deletions config/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Name of your application. Used to uniquely configure containers.
service: hemmelig_app

# Name of the container image.
image: hemmeligapp/hemmelig

registry:
username:
- DOCKER_USERNAME
password:
- DOCKER_PASSWORD

# Deploy to these servers.
servers:
- <replace_me>

volumes:
- '/root/hemmelig/userdata/:/var/tmp/hemmelig/upload/files'
- '/root/hemmelig/database/:/home/node/hemmelig/database/'

# Inject ENV variables into containers (secrets come from .env).
# Remember to run `kamal env push` after making changes!
env:
clear:
SECRET_ROOT_USER: 'bjarneo'
SECRET_ROOT_PASSWORD: 'lolyouthinkthisisit'
SECRET_ROOT_EMAIL: '[email protected]'
SECRET_MAX_TEXT_SIZE: '20048'
SECRET_HOST: 'hemmelig.app'
secret:
- SECRET_MASTER_KEY
- SECRET_JWT_SECRET

# TODO: Implement non root user
# Use a different ssh user than root
# ssh:
# user: app

# Configure custom arguments for Traefik. Be sure to reboot traefik when you modify it.
traefik:
args:
accesslog: true
accesslog.format: json

# Configure a health check
healthcheck:
path: /api/healthz
port: 3000
interval: 10s

sshkit:
max_concurrent_starts: 10
pool_idle_timeout: 200

0 comments on commit 8a20bcb

Please sign in to comment.