Set up Kamal
This commit is contained in:
parent
b38c8cf814
commit
a456212ccf
|
@ -0,0 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# A sample docker-setup hook
|
||||
#
|
||||
# Sets up a Docker network which can then be used by the application’s containers
|
||||
|
||||
ssh root@ttcompanion.com "docker network inspect kamal || docker network create kamal"
|
|
@ -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
|
|
@ -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"
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
echo "Rebooted Traefik on $KAMAL_HOSTS"
|
|
@ -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
|
|
@ -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 ]
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
echo "Rebooting Traefik on $KAMAL_HOSTS..."
|
|
@ -13,16 +13,18 @@ ENV RAILS_ENV="production" \
|
|||
BUNDLE_PATH="/usr/local/bundle" \
|
||||
BUNDLE_WITHOUT="development"
|
||||
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install --no-install-recommends -y libvips libpq-dev
|
||||
|
||||
# Throw-away build stage to reduce size of final image
|
||||
FROM base as build
|
||||
|
||||
# Install packages needed to build gems
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install --no-install-recommends -y build-essential git libvips pkg-config
|
||||
apt-get install --no-install-recommends -y build-essential git pkg-config
|
||||
|
||||
# Install application gems
|
||||
COPY Gemfile Gemfile.lock ./
|
||||
COPY Gemfile Gemfile.lock .ruby-version ./
|
||||
RUN bundle install && \
|
||||
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
|
||||
bundle exec bootsnap precompile --gemfile
|
||||
|
|
|
@ -1 +1 @@
|
|||
EI74C8uxmka2Mejn5kTdq8qxetc8d8SJd4w+VNKkQdMUa6NgzZnz6Tf7RJZaQnUUtHPfVtFiltrkPcIiOzY1a5meO1TeydtWl+5ij2vpKFDP9tpcmVswhWJYvkuf6LRh/L8Yy1HeMBh5zM94JVc+s2X6aqfMjd19ORJEenyR4C6VogSUMWOOs+C9f9+8z+O3hptwfRlj6AwM5k5sn6PEKaUVUrnXa7TRLvVnK5Sm7s2WXTrTI6I3n+CaGp2iNRUf2Y3w34YE3QydWJ4YsmoFByMQIqkUtmasBuGYGX7CG4umd2/uUD/HJNnB/BriwsqbSV8QM4ErVO+8pSzKu7RJGH2SVZdYiz8WeVvoKHXPJ8cwVrW4Q45beYtUFMCPiYkUl4oZbzx1h94k0+AM4rRf6OCut9BW--3rZrEMdP2rRRfpnL--GFSUvgc2V69L+ERe1xIkiQ==
|
||||
RWzqbXYOBdxgpheefubYOD+li8SgrYjDuQjZ3SxRbSI8HhMmEi0iSuGUFBl/OlRTmhmUcswbpO4QXmcuS/Ej8dxz2WZ03RnozYO3ftYGSMQy23mqJx4WGxG+Z3p5hxSmeJ4bLoM91bQPkwYJGGi2ZLZXb6+QLopTtBHCZ17dmlvCE22F6zz70KZVgLN76jMTtjfLTQc5XrIdg23rFQfkgsSTrX9spKr/6mgUAILRQ/baPzlT43v9z13YfA8xe3BM/Giv7tBTmnZ6SVs12TT7jcsp5RfdWyxHHgpKUf0E1Mz5P98xOabo/pWYL3TQ8AzkW2y7G8q3WaR8FLpS80UOSBTgx0/iE6Josoc8Kql22VInLJMUz4+Haxt3t22dU2DE4ERmEjvhgCBTFgw6NyBy+tPxmdIh--mZf16ejQbkcvs4cT--V5RGldMxw37X+LXoyc8XYw==
|
|
@ -3,7 +3,7 @@ default: &default
|
|||
encoding: unicode
|
||||
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
||||
timeout: 5000
|
||||
host: localhost
|
||||
host: <%= ENV.fetch("DB_HOST") { "localhost" } %>
|
||||
username: <%= ENV.fetch("POSTGRES_USER") { "postgres" } %>
|
||||
password: <%= ENV.fetch("POSTGRES_PASSWORD") { "postgres" } %>
|
||||
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
service: ttcompanion
|
||||
|
||||
image: tsvallender/ttcompanion
|
||||
|
||||
traefik:
|
||||
options:
|
||||
publish:
|
||||
- "443:443"
|
||||
volume:
|
||||
- "/letsencrypt/acme.json:/letsencrypt/acme.json" # To save the configuration file.
|
||||
network: kamal
|
||||
args:
|
||||
entryPoints.web.address: ":80"
|
||||
entryPoints.websecure.address: ":443"
|
||||
entryPoints.web.http.redirections.entryPoint.to: websecure # We want to force https
|
||||
entryPoints.web.http.redirections.entryPoint.scheme: https
|
||||
entryPoints.web.http.redirections.entrypoint.permanent: true
|
||||
certificatesResolvers.letsencrypt.acme.email: "trevor@ttcompanion.com"
|
||||
certificatesResolvers.letsencrypt.acme.storage: "/letsencrypt/acme.json"
|
||||
certificatesResolvers.letsencrypt.acme.httpchallenge: true
|
||||
certificatesResolvers.letsencrypt.acme.httpchallenge.entrypoint: web #
|
||||
|
||||
servers:
|
||||
web:
|
||||
hosts:
|
||||
- ttcompanion.com
|
||||
labels:
|
||||
traefik.http.routers.ttcompanion.entrypoints: websecure
|
||||
traefik.http.routers.ttcompanion.rule: Host(`ttcompanion.com`)
|
||||
traefik.http.routers.ttcompanion.tls.certresolver: letsencrypt
|
||||
options:
|
||||
network: kamal
|
||||
|
||||
volumes:
|
||||
- /srv/ttcompanion/storage:/rails/storage
|
||||
|
||||
registry:
|
||||
username: tsvallender
|
||||
password:
|
||||
- KAMAL_REGISTRY_PASSWORD
|
||||
|
||||
env:
|
||||
clear:
|
||||
APPLICATION_HOST: ttcompanion.com
|
||||
DB_HOST: ttcompanion-postgres
|
||||
RAILS_LOG_TO_STDOUT: true
|
||||
RAILS_SERVE_STATIC_FILES: true
|
||||
secret:
|
||||
- RAILS_MASTER_KEY
|
||||
- POSTGRES_PASSWORD
|
||||
|
||||
builder:
|
||||
multiarch: false
|
||||
|
||||
accessories:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
host: ttcompanion.com
|
||||
env:
|
||||
clear:
|
||||
POSTGRES_USER: postgres
|
||||
secret:
|
||||
- POSTGRES_PASSWORD
|
||||
directories:
|
||||
- /srv/ttcompanion/data:/var/lib/postgresql/data
|
||||
options:
|
||||
network: kamal
|
||||
|
||||
healthcheck:
|
||||
log_lines: 1000
|
||||
|
||||
asset_path: /rails/public/assets
|
|
@ -90,10 +90,11 @@ Rails.application.configure do
|
|||
config.active_record.dump_schema_after_migration = false
|
||||
|
||||
# Enable DNS rebinding protection and other `Host` header attacks.
|
||||
# config.hosts = [
|
||||
# "example.com", # Allow requests from example.com
|
||||
# /.*\.example\.com/ # Allow requests from subdomains like `www.example.com`
|
||||
# ]
|
||||
config.hosts = [
|
||||
"localhost",
|
||||
"ttcompanion.com",
|
||||
/.*\.ttcompanion\.com/
|
||||
]
|
||||
# Skip DNS rebinding protection for the default health check endpoint.
|
||||
# config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue