Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,9 @@ yarn-debug.log*
OAUTH_TESTING.md
config/credentials/development.yml.enc
config/credentials/test.yml.enc

/config/credentials/production.key
config/credentials/production.yml.enc
.kamal/secrets
.gitignore
KAMAL.md
13 changes: 11 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ ENV RAILS_ENV="production" \
# Throw-away build stage to reduce size of final image
FROM base AS build

# Install packages needed to build gems
# Install packages needed to build gems and Node.js for TailwindCSS
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config && \
apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config curl && \
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install --no-install-recommends -y nodejs && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Install application gems
Expand All @@ -39,9 +41,16 @@ RUN bundle install && \
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
bundle exec bootsnap precompile --gemfile

# Install JavaScript dependencies
COPY package.json package-lock.json ./
RUN npm ci --include=dev

# Copy application code
COPY . .

# Build TailwindCSS
RUN npm run build:css

# Precompile bootsnap code for faster boot times
RUN bundle exec bootsnap precompile app/ lib/

Expand Down
2 changes: 1 addition & 1 deletion app/mailers/application_mailer.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class ApplicationMailer < ActionMailer::Base
default from: "from@example.com"
default from: ENV.fetch("MAILER_FROM_EMAIL", "noreply@stackoverflow-clone.com")
layout "mailer"
end
1 change: 0 additions & 1 deletion app/models/answer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ class Answer < ApplicationRecord
include Votable

include Elasticsearch::Model
include Elasticsearch::Model::Callbacks

settings index: { number_of_shards: 1, number_of_replicas: 0 } do
mappings dynamic: false do
Expand Down
1 change: 0 additions & 1 deletion app/models/comment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ class Comment < ApplicationRecord
validates :body, presence: true, length: { maximum: 2000 }

include Elasticsearch::Model
include Elasticsearch::Model::Callbacks

settings index: { number_of_shards: 1, number_of_replicas: 0 } do
mappings dynamic: false do
Expand Down
1 change: 0 additions & 1 deletion app/models/question.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ class Question < ApplicationRecord
include Votable

include Elasticsearch::Model
include Elasticsearch::Model::Callbacks

settings index: { number_of_shards: 1, number_of_replicas: 0 } do
mappings dynamic: false do
Expand Down
1 change: 0 additions & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ class User < ApplicationRecord
:confirmable, :omniauthable, omniauth_providers: [ :google_oauth2 ]

include Elasticsearch::Model
include Elasticsearch::Model::Callbacks

settings index: { number_of_shards: 1, number_of_replicas: 0 } do
mappings dynamic: false do
Expand Down
161 changes: 73 additions & 88 deletions config/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,116 +1,101 @@
# Name of your application. Used to uniquely configure containers.
service: stackoverflow_clone

# Name of the container image.
image: your-user/stackoverflow_clone
image: amsak701/stclone

# Deploy to these servers.
servers:
web:
- 192.168.0.1
# job:
# hosts:
# - 192.168.0.1
# cmd: bin/jobs
- 90.156.228.95

# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer.
#
# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
proxy:
ssl: true
host: app.example.com

# Credentials for your image host.
registry:
# Specify the registry server, if you're not using Docker Hub
# server: registry.digitalocean.com / ghcr.io / ...
username: your-user

# Always use an access token rather than real password when possible.
username:
- KAMAL_REGISTRY_USERNAME
password:
- KAMAL_REGISTRY_PASSWORD

# Inject ENV variables into containers (secrets come from .kamal/secrets).
env:
secret:
- RAILS_MASTER_KEY
- SECRET_KEY_BASE
- GOOGLE_CLIENT_ID
- GOOGLE_CLIENT_SECRET
- SIDEKIQ_USERNAME
- SIDEKIQ_PASSWORD
- SMTP_ADDRESS
- SMTP_USERNAME
- SMTP_PASSWORD

clear:
# Run the Solid Queue Supervisor inside the web server's Puma process to do jobs.
# When you start using multiple servers, you should split out job processing to a dedicated machine.
SOLID_QUEUE_IN_PUMA: true

# Set number of processes dedicated to Solid Queue (default: 1)
# JOB_CONCURRENCY: 3

# Set number of cores available to the application on each server (default: 1).
# WEB_CONCURRENCY: 2

# Match this to any external database server to configure Active Record correctly
# Use stackoverflow_clone-db for a db accessory server on same machine via local kamal docker network.
# DB_HOST: 192.168.0.2
RAILS_ENV: production
RAILS_LOG_TO_STDOUT: "1"
RAILS_SERVE_STATIC_FILES: "true"
WEB_CONCURRENCY: "2"
RAILS_MAX_THREADS: "5"
REDIS_URL: redis://stackoverflow_clone-redis:6379/0
ELASTICSEARCH_URL: http://stackoverflow_clone-elasticsearch:9200
APP_HOST: "90.156.228.95"
MAILER_FROM_EMAIL: "noreply@stackoverflow-clone.com"
SMTP_PORT: "587"
SMTP_DOMAIN: "90.156.228.95"

# Log everything from Rails
# RAILS_LOG_LEVEL: debug

# Aliases are triggered with "bin/kamal <alias>". You can overwrite arguments on invocation:
# "bin/kamal logs -r job" will tail logs from the first server in the job section.
aliases:
console: app exec --interactive --reuse "bin/rails console"
shell: app exec --interactive --reuse "bash"
logs: app logs -f
dbc: app exec --interactive --reuse "bin/rails dbconsole"


# Use a persistent storage volume for sqlite database files and local Active Storage files.
# Recommended to change this to a mounted volume path that is backed up off server.
volumes:
- "stackoverflow_clone_storage:/rails/storage"
- "stackoverflow_clone_log:/rails/log"
- "stackoverflow_clone_tmp:/rails/tmp"


# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
# hitting 404 on in-flight requests. Combines all files from new and old
# version inside the asset_path.
asset_path: /rails/public/assets

# Configure the image builder.
builder:
remote: 90.156.228.95
arch: amd64

# # Build image via remote server (useful for faster amd64 builds on arm64 computers)
# remote: ssh://docker@docker-builder-server
#
# # Pass arguments and secrets to the Docker build process
# args:
# RUBY_VERSION: 3.2.6
# secrets:
# - GITHUB_TOKEN
# - RAILS_MASTER_KEY

# Use a different ssh user than root
# ssh:
# user: app

# Use accessory services (secrets come from .kamal/secrets).
# accessories:
# db:
# image: mysql:8.0
# host: 192.168.0.2
# # Change to 3306 to expose port to the world instead of just local network.
# port: "127.0.0.1:3306:3306"
# env:
# clear:
# MYSQL_ROOT_HOST: '%'
# secret:
# - MYSQL_ROOT_PASSWORD
# files:
# - config/mysql/production.cnf:/etc/mysql/my.cnf
# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql
# directories:
# - data:/var/lib/mysql
# redis:
# image: redis:7.0
# host: 192.168.0.2
# port: 6379
# directories:
# - data:/data
proxy:
host: 90.156.228.95


accessories:
redis:
image: redis:7-alpine
host: 90.156.228.95
port: "6379"
directories:
- redis-data:/data

elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.13.4
host: 90.156.228.95
port: "9200"
env:
clear:
discovery.type: single-node
xpack.security.enabled: "false"
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
directories:
- es-data:/usr/share/elasticsearch/data

sidekiq:
image: amsak701/stclone
host: 90.156.228.95
cmd: bundle exec sidekiq -C config/sidekiq.yml
volumes:
- "stackoverflow_clone_storage:/rails/storage"
- "stackoverflow_clone_log:/rails/log"
- "stackoverflow_clone_tmp:/rails/tmp"
env:
secret:
- SECRET_KEY_BASE
- SMTP_ADDRESS
- SMTP_USERNAME
- SMTP_PASSWORD
clear:
RAILS_ENV: production
REDIS_URL: redis://stackoverflow_clone-redis:6379/0
ELASTICSEARCH_URL: http://stackoverflow_clone-elasticsearch:9200
APP_HOST: "90.156.228.95"
MAILER_FROM_EMAIL: "noreply@stackoverflow-clone.com"
SMTP_PORT: "587"
SMTP_DOMAIN: "90.156.228.95"
92 changes: 67 additions & 25 deletions config/environments/production.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@
config.active_storage.service = :local

# Assume all access to the app is happening through a SSL-terminating reverse proxy.
config.assume_ssl = true
# Disabled for IP-based deployment without SSL certificate
config.assume_ssl = false

# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
config.force_ssl = true
# Note: Disabled for IP-based deployment. Enable when using a domain with proper SSL certificate.
config.force_ssl = false

# Skip http-to-https redirect for the default health check endpoint.
# config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } }
config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } }

# Log to STDOUT with the current request id as a default log tag.
config.log_tags = [ :request_id ]
Expand All @@ -49,25 +51,63 @@
# Replace the default in-process memory cache store with a durable alternative.
config.cache_store = :solid_cache_store

# Replace the default in-process and non-durable queuing backend for Active Job.
config.active_job.queue_adapter = :solid_queue
config.solid_queue.connects_to = { database: { writing: :queue } }
# Use Sidekiq for background jobs
config.active_job.queue_adapter = :sidekiq

# Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
# config.action_mailer.raise_delivery_errors = false
# Mailer configuration
config.action_mailer.raise_delivery_errors = true
config.action_mailer.perform_deliveries = true
config.action_mailer.deliver_later_queue_name = :default

# Set host to be used by links generated in mailer templates.
config.action_mailer.default_url_options = { host: "example.com" }

# Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit.
# config.action_mailer.smtp_settings = {
# user_name: Rails.application.credentials.dig(:smtp, :user_name),
# password: Rails.application.credentials.dig(:smtp, :password),
# address: "smtp.example.com",
# port: 587,
# authentication: :plain
# }
config.action_mailer.default_url_options = {
host: ENV.fetch("APP_HOST", "90.156.228.95"),
protocol: "http" # Use http for IP-based deployment without SSL
}

smtp_credentials = if ENV["RAILS_MASTER_KEY"].present? && ENV["RAILS_MASTER_KEY"].length == 32
begin
{
address: Rails.application.credentials.dig(:smtp, :address),
port: Rails.application.credentials.dig(:smtp, :port),
domain: Rails.application.credentials.dig(:smtp, :domain),
user_name: Rails.application.credentials.dig(:smtp, :user_name),
password: Rails.application.credentials.dig(:smtp, :password)
}
rescue ActiveSupport::MessageEncryptor::InvalidMessage, ArgumentError => e
puts "⚠ Could not decrypt credentials: #{e.class}"
{}
rescue => e
puts "⚠ Credentials error: #{e.message}"
{}
end
else
puts "⚠ RAILS_MASTER_KEY not configured, using ENV variables only"
{}
end

smtp_address = ENV["SMTP_ADDRESS"] || smtp_credentials[:address]

if smtp_address.present?
config.action_mailer.perform_deliveries = true
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: smtp_address,
port: (ENV["SMTP_PORT"] || smtp_credentials[:port] || 587).to_i,
domain: ENV["SMTP_DOMAIN"] || smtp_credentials[:domain],
user_name: ENV["SMTP_USERNAME"] || smtp_credentials[:user_name],
password: ENV["SMTP_PASSWORD"] || smtp_credentials[:password],
authentication: :plain,
enable_starttls_auto: true,
open_timeout: 10,
read_timeout: 10
}
else
# SMTP not configured - disable email delivery (dev mode)
config.action_mailer.perform_deliveries = false
config.action_mailer.delivery_method = :test
# Logger not available during initialization, warning will be logged at runtime if needed
end

# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation cannot be found).
Expand All @@ -80,11 +120,13 @@
config.active_record.attributes_for_inspect = [ :id ]

# 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 = [
"90.156.228.95" # Allow requests from server IP
# Add your domain here when you have one:
# "yourdomain.com",
# /.*\.yourdomain\.com/
]

# Skip DNS rebinding protection for the default health check endpoint.
# config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
end
Loading