diff --git a/.dockerignore b/.dockerignore index 325bfc0..a4f8385 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,6 +13,7 @@ # Ignore all default key files. /config/master.key /config/credentials/*.key +/registry_password.key # Ignore all logfiles and tempfiles. /log/* diff --git a/.gitignore b/.gitignore index 595d2ac..624d98f 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ # Ignore master key for decrypting credentials and more. /config/master.key +/registry_password.key # Vite Ruby /public/vite* diff --git a/.kamal/secrets b/.kamal/secrets index 9a771a3..82ecbcc 100644 --- a/.kamal/secrets +++ b/.kamal/secrets @@ -10,8 +10,9 @@ # Use a GITHUB_TOKEN if private repositories are needed for the image # GITHUB_TOKEN=$(gh config get -h github.com oauth_token) -# Grab the registry password from ENV -KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD +# Set the Docker registry password directly +# Replace 'your-docker-password' with your actual Docker Hub password +KAMAL_REGISTRY_PASSWORD=$(cat registry_password.key) # Improve security by using a password manager. Never check config/master.key into git! RAILS_MASTER_KEY=$(cat config/master.key) diff --git a/Dockerfile b/Dockerfile index eab450a..fa630b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,30 @@ # syntax=docker/dockerfile:1 # check=error=true -# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: -# docker build -t img_manager . -# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name img_manager img_manager - -# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html +# This Dockerfile is designed for Git-based deployment with Kamal +# It pulls code from Git during deployment and uses external volumes for code, build artifacts, and data # Make sure RUBY_VERSION matches the Ruby version in .ruby-version ARG RUBY_VERSION=3.2.2 -FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base +FROM docker.io/library/ruby:$RUBY_VERSION-slim # Rails app lives here WORKDIR /rails -# Install base packages +# Install base packages including Git for code pulling RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \ - rm -rf /var/lib/apt/lists /var/cache/apt/archives + apt-get install --no-install-recommends -y \ + curl \ + libjemalloc2 \ + libvips \ + sqlite3 \ + nodejs \ + npm \ + netcat-openbsd \ + git \ + build-essential \ + pkg-config \ + && rm -rf /var/lib/apt/lists /var/cache/apt/archives # Set production environment ENV RAILS_ENV="production" \ @@ -25,43 +32,35 @@ ENV RAILS_ENV="production" \ BUNDLE_PATH="/usr/local/bundle" \ BUNDLE_WITHOUT="development" -# 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 pkg-config && \ - rm -rf /var/lib/apt/lists /var/cache/apt/archives - -# Install application gems -COPY Gemfile Gemfile.lock ./ -RUN bundle install && \ - rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ - bundle exec bootsnap precompile --gemfile - -# Copy application code -COPY . . - -# Precompile bootsnap code for faster boot times -RUN bundle exec bootsnap precompile app/ lib/ - - - - -# Final stage for app image -FROM base - -# Copy built artifacts: gems, application -COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" -COPY --from=build /rails /rails +# Create directories for mounted volumes +RUN mkdir -p \ + /rails/code \ + /rails/storage \ + /rails/public/uploads \ + /rails/public/assets \ + /rails/public/vite \ + /rails/node_modules \ + /rails/tmp \ + /rails/log \ + /rails/tmp/pids \ + /rails/tmp/cache \ + /rails/tmp/sockets + +# Declare volumes for persistent storage +VOLUME ["/rails/code", "/rails/storage", "/rails/public/uploads", "/rails/public/vite", "/rails/node_modules", "/rails/log", "/rails/tmp"] + +# Copy entrypoint script +COPY bin/docker-entrypoint /rails/bin/ +RUN chmod +x /rails/bin/docker-entrypoint # Run and own only the runtime files as a non-root user for security RUN groupadd --system --gid 1000 rails && \ useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ - chown -R rails:rails db log storage tmp + chown -R rails:rails /rails + USER 1000:1000 -# Entrypoint prepares the database. +# Entrypoint pulls code, installs dependencies, and prepares the application ENTRYPOINT ["/rails/bin/docker-entrypoint"] # Start server via Thruster by default, this can be overwritten at runtime diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index ec3a79c..271e0c9 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -35,7 +35,7 @@ module Authentication def request_authentication session[:return_to_after_authenticating] = request.url - redirect_to new_sessions_path + redirect_to new_session_path end def after_authentication_url diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 4f2c8fe..0c4b4a8 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -10,7 +10,7 @@ class PasswordsController < ApplicationController PasswordsMailer.reset(user).deliver_later end - redirect_to new_sessions_path, notice: "Password reset instructions sent (if user with that email address exists)." + redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)." end def edit @@ -18,7 +18,7 @@ class PasswordsController < ApplicationController def update if @user.update(params.permit(:password, :password_confirmation)) - redirect_to new_sessions_path, notice: "Password has been reset." + redirect_to new_session_path, notice: "Password has been reset." else redirect_to edit_password_path(params[:token]), alert: "Passwords did not match." end diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint index 57567d6..cb0fcd6 100755 --- a/bin/docker-entrypoint +++ b/bin/docker-entrypoint @@ -1,14 +1,260 @@ #!/bin/bash -e +echo "Starting deployment process..." -# Enable jemalloc for reduced memory usage and latency. +# Enable jemalloc for reduced memory usage and latency if [ -z "${LD_PRELOAD+x}" ]; then LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit) export LD_PRELOAD fi -# If running the rails server then create or migrate existing database +# Ensure all required directories exist with proper permissions +echo "Setting up directories..." +mkdir -p /rails/code /rails/storage /rails/public/uploads /rails/public/assets /rails/public/vite /rails/node_modules /rails/log /rails/tmp/pids /rails/tmp/cache /rails/tmp/sockets + +# Set very permissive permissions for all directories +chmod -R 777 /rails/storage /rails/public/uploads /rails/public/assets /rails/public/vite /rails/node_modules /rails/log /rails/tmp +chown -R rails:rails /rails/storage /rails/public/uploads /rails/public/assets /rails/public/vite /rails/node_modules /rails/log /rails/tmp + +# Verify the storage directory exists and has proper permissions +echo "Verifying storage directory permissions:" +ls -la /rails | grep storage + +# Test if storage directory is writable +echo "Testing storage directory write access..." +if touch /rails/storage/test_write_access; then + echo "✅ Storage directory is writable" + rm /rails/storage/test_write_access +else + echo "❌ ERROR: Cannot write to storage directory. Check volume mount and permissions." + # Continue anyway, but log the error +fi + +# Wait for external volumes to be properly mounted +sleep 5 + +# Check if code directory is empty or if we need to clone/pull the repository +if [ -z "${GIT_REPOSITORY}" ]; then + echo "⚠️ GIT_REPOSITORY environment variable not set. Using mounted code directory." +else + echo "🔄 Setting up code from Git repository: ${GIT_REPOSITORY}" + + # Check if we already have the repository + if [ -d "/rails/code/.git" ]; then + echo "Git repository exists, pulling latest changes..." + cd /rails/code + git fetch + + # Check if we need to checkout a specific branch or tag + if [ -n "${GIT_BRANCH}" ]; then + echo "Checking out branch/tag: ${GIT_BRANCH}" + git checkout ${GIT_BRANCH} + git pull origin ${GIT_BRANCH} + else + echo "No branch specified, pulling latest from current branch" + git pull + fi + else + echo "Cloning repository..." + # Clone the repository + if [ -n "${GIT_BRANCH}" ]; then + git clone --branch ${GIT_BRANCH} ${GIT_REPOSITORY} /rails/code + else + git clone ${GIT_REPOSITORY} /rails/code + fi + fi + + echo "✅ Code updated from Git repository" +fi + +# Create symbolic links from code directory to Rails app directory +echo "Setting up symbolic links..." +cd /rails/code + +# Link important directories and files to maintain Rails structure +for dir in app bin config db lib public vendor; do + if [ -d "/rails/code/$dir" ]; then + ln -sfn /rails/code/$dir /rails/$dir + fi +done + +# Link important files +for file in Gemfile Gemfile.lock Rakefile config.ru; do + if [ -f "/rails/code/$file" ]; then + ln -sf /rails/code/$file /rails/$file + fi +done + +# Link node_modules if it exists in the mounted volume +if [ -d "/rails/node_modules" ]; then + ln -sfn /rails/node_modules /rails/code/node_modules +fi + +# If running the rails server then install dependencies and prepare the database if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then - ./bin/rails db:prepare + cd /rails + + # Install Ruby dependencies if needed + echo "Installing Ruby dependencies..." + if [ ! -d "${BUNDLE_PATH}/ruby" ] || [ "$FORCE_BUNDLE_INSTALL" = "true" ]; then + bundle install + echo "✅ Ruby dependencies installed" + else + echo "✅ Ruby dependencies already installed" + fi + + # Install Node.js dependencies if needed + echo "Installing Node.js dependencies..." + if [ ! -d "/rails/node_modules/node_modules" ] || [ "$FORCE_NPM_INSTALL" = "true" ]; then + npm install + echo "✅ Node.js dependencies installed" + else + echo "✅ Node.js dependencies already installed" + fi + + # Prepare database files with proper permissions + echo "Ensuring database files exist with proper permissions..." + + # Create database directory with proper permissions + mkdir -p /rails/storage + chmod -R 777 /rails/storage + chown -R rails:rails /rails/storage + + echo "Creating database files if they don't exist..." + # Create database files if they don't exist + for db_file in production.sqlite3 production_cache.sqlite3 production_queue.sqlite3 production_cable.sqlite3; do + if [ ! -f "/rails/storage/$db_file" ]; then + echo "Creating /rails/storage/$db_file" + touch "/rails/storage/$db_file" + chmod 666 "/rails/storage/$db_file" + chown rails:rails "/rails/storage/$db_file" + else + echo "Database file /rails/storage/$db_file already exists" + # Make sure existing file has correct permissions + chmod 666 "/rails/storage/$db_file" + chown rails:rails "/rails/storage/$db_file" + fi + done + + # Double-check permissions and ownership + echo "Database directory permissions:" + ls -la /rails/storage + + # Test if we can write to the database file + echo "Testing database file write access..." + if sqlite3 /rails/storage/production.sqlite3 "PRAGMA user_version;" > /dev/null 2>&1; then + echo "✅ Database file is writable" + else + echo "❌ ERROR: Cannot write to database file. Check volume mount and permissions." + # Try to fix permissions again with more aggressive approach + echo "Attempting more aggressive permission fix..." + chown -R rails:rails /rails/storage + chmod -R 777 /rails/storage + chmod 666 /rails/storage/*.sqlite3 + + # Try to create an empty database structure + echo "Attempting to initialize empty database..." + sqlite3 /rails/storage/production.sqlite3 "CREATE TABLE IF NOT EXISTS schema_migrations (version varchar(255) NOT NULL); CREATE UNIQUE INDEX unique_schema_migrations ON schema_migrations (version);" || true + fi + + # Prepare the database with retry logic + echo "Preparing database..." + + # First try to manually create schema_migrations table to avoid common issues + echo "Attempting to create schema_migrations table manually..." + for db_file in /rails/storage/production.sqlite3 /rails/storage/production_cache.sqlite3 /rails/storage/production_queue.sqlite3 /rails/storage/production_cable.sqlite3; do + echo "Initializing $db_file..." + sqlite3 "$db_file" "CREATE TABLE IF NOT EXISTS schema_migrations (version varchar(255) NOT NULL); CREATE UNIQUE INDEX IF NOT EXISTS unique_schema_migrations ON schema_migrations (version);" || true + done + + # Create empty schema.rb file to help Rails recognize the database state + mkdir -p /rails/db + if [ ! -f "/rails/db/schema.rb" ]; then + echo "Creating empty schema.rb file..." + cat > /rails/db/schema.rb << 'EOL' +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[7.1].define(version: 0) do + # These are extensions that must be enabled in order to support this database + +end +EOL + fi + + # Check if we should skip database preparation + if [ "${SKIP_DB_PREPARATION}" = "true" ]; then + echo "⚠️ SKIP_DB_PREPARATION is set to true, skipping database preparation" + echo "✅ Using existing database files without migrations" + SUCCESS=true + else + # Now try to run db:prepare with multiple attempts + for i in {1..3}; do + echo "Database preparation attempt $i..." + + # Print current environment for debugging + echo "Current environment:" + echo "- Working directory: $(pwd)" + echo "- User: $(whoami)" + echo "- Storage directory contents:" + ls -la /rails/storage + echo "- Database config:" + cat /rails/config/database.yml | grep -A 10 production + + # Try different database commands with increasing aggressiveness + if [ $i -eq 1 ]; then + echo "Trying db:prepare..." + RAILS_ENV=production ./bin/rails db:prepare --trace && SUCCESS=true && break + elif [ $i -eq 2 ]; then + echo "Trying db:migrate:status..." + RAILS_ENV=production ./bin/rails db:migrate:status --trace || true + echo "Trying db:schema:load..." + RAILS_ENV=production ./bin/rails db:schema:load --trace && SUCCESS=true && break + else + echo "Trying db:setup..." + RAILS_ENV=production ./bin/rails db:setup --trace && SUCCESS=true && break + fi + + echo "⚠️ Database preparation attempt $i failed, retrying..." + + # Try to fix permissions again + echo "Fixing permissions again..." + chmod -R 777 /rails/storage + chmod 666 /rails/storage/*.sqlite3 + chown -R rails:rails /rails/storage + + sleep 5 + done + fi + + if [ "$SUCCESS" = "true" ]; then + echo "✅ Database prepared successfully" + else + echo "⚠️ WARNING: Database preparation failed after 3 attempts, but continuing startup" + echo "The application may not function correctly until database issues are resolved" + fi + + # Build Vite assets if needed + if [ ! -d "/rails/public/vite" ] || [ -z "$(ls -A /rails/public/vite)" ] || [ "$FORCE_VITE_BUILD" = "true" ]; then + echo "Building Vite assets..." + bundle exec vite build + echo "✅ Vite assets built" + else + echo "✅ Vite assets already exist" + fi + + # Create health check file + touch /rails/tmp/healthy + echo "✅ Application ready to start" fi +echo "🚀 Starting application..." exec "${@}" diff --git a/bin/setup_volumes.sh b/bin/setup_volumes.sh new file mode 100755 index 0000000..bdfad2d --- /dev/null +++ b/bin/setup_volumes.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# This script sets up the required directories and permissions on the server +# Run this script on the server before deploying + +echo "Setting up directories and permissions for img-manager..." + +# Create all required directories +mkdir -p /root/img_manager/code +mkdir -p /root/img_manager/storage +mkdir -p /root/img_manager/uploads +mkdir -p /root/img_manager/public/vite +mkdir -p /root/img_manager/node_modules +mkdir -p /root/img_manager/logs +mkdir -p /root/img_manager/tmp + +# Set very permissive permissions +chmod -R 777 /root/img_manager + +# Create empty database files with proper permissions +touch /root/img_manager/storage/production.sqlite3 +touch /root/img_manager/storage/production_cache.sqlite3 +touch /root/img_manager/storage/production_queue.sqlite3 +touch /root/img_manager/storage/production_cable.sqlite3 +chmod 666 /root/img_manager/storage/*.sqlite3 + +# Initialize the database files with schema_migrations tables +echo "Initializing database files with schema_migrations tables..." +for db_file in /root/img_manager/storage/production.sqlite3 /root/img_manager/storage/production_cache.sqlite3 /root/img_manager/storage/production_queue.sqlite3 /root/img_manager/storage/production_cable.sqlite3; do + sqlite3 "$db_file" "CREATE TABLE IF NOT EXISTS schema_migrations (version varchar(255) NOT NULL); CREATE UNIQUE INDEX IF NOT EXISTS unique_schema_migrations ON schema_migrations (version);" || echo "Failed to initialize $db_file" +done + +echo "Checking SELinux status..." +if command -v getenforce &> /dev/null; then + selinux_status=$(getenforce) + echo "SELinux status: $selinux_status" + + if [ "$selinux_status" == "Enforcing" ]; then + echo "SELinux is enforcing. Setting proper contexts..." + # Set SELinux context for the directories + if command -v chcon &> /dev/null; then + chcon -Rt svirt_sandbox_file_t /root/img_manager + else + echo "chcon command not found. Unable to set SELinux context." + fi + fi +else + echo "getenforce command not found. Unable to check SELinux status." +fi + +echo "Directory permissions:" +ls -la /root/img_manager + +echo "Storage directory permissions:" +ls -la /root/img_manager/storage + +echo "Setup complete!" diff --git a/config/database.yml b/config/database.yml index 2640cb5..f573dab 100644 --- a/config/database.yml +++ b/config/database.yml @@ -21,21 +21,21 @@ test: database: storage/test.sqlite3 -# Store production database in the storage/ directory, which by default -# is mounted as a persistent Docker volume in config/deploy.yml. +# Store production database in the /rails/storage/ directory with absolute paths +# This ensures the database files are found regardless of current directory production: primary: <<: *default - database: storage/production.sqlite3 + database: /rails/storage/production.sqlite3 cache: <<: *default - database: storage/production_cache.sqlite3 + database: /rails/storage/production_cache.sqlite3 migrations_paths: db/cache_migrate queue: <<: *default - database: storage/production_queue.sqlite3 + database: /rails/storage/production_queue.sqlite3 migrations_paths: db/queue_migrate cable: <<: *default - database: storage/production_cable.sqlite3 + database: /rails/storage/production_cable.sqlite3 migrations_paths: db/cable_migrate diff --git a/config/deploy.yml b/config/deploy.yml index 26f4793..ea9aef9 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -2,12 +2,12 @@ service: img_manager # Name of the container image. -image: your-user/img_manager +image: mumumumushu/img_manager # Deploy to these servers. servers: web: - - 192.168.0.1 + - 45.78.59.154 # job: # hosts: # - 192.168.0.1 @@ -18,14 +18,16 @@ servers: # # 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 + # ssl: true + host: 45.78.59.154 + # If you have a domain name, use it instead of the IP address + # host: your-domain.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 + username: mumumumushu # Always use an access token rather than real password when possible. password: @@ -35,9 +37,26 @@ registry: env: secret: - RAILS_MASTER_KEY + # Add Git credentials if needed for private repositories + # - GIT_CREDENTIALS clear: + # Git repository configuration + GIT_REPOSITORY: http://git.tallty.com/mumumumushu/img-manager.git + GIT_BRANCH: main + + # Force rebuild flags (set to true when you want to force rebuild) + FORCE_BUNDLE_INSTALL: true + FORCE_NPM_INSTALL: true + FORCE_VITE_BUILD: true + + # Database configuration + RAILS_ENV: production + DATABASE_URL: sqlite3:///rails/storage/production.sqlite3 + + # Disable database migrations during startup to avoid SQLite issues + SKIP_DB_PREPARATION: true + # 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) @@ -47,11 +66,10 @@ env: # WEB_CONCURRENCY: 2 # Match this to any external database server to configure Active Record correctly - # Use img_manager-db for a db accessory server on same machine via local kamal docker network. # DB_HOST: 192.168.0.2 # Log everything from Rails - # RAILS_LOG_LEVEL: debug + 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. @@ -60,18 +78,47 @@ aliases: shell: app exec --interactive --reuse "bash" logs: app logs -f dbc: app exec --interactive --reuse "bin/rails dbconsole" + assets: app exec --interactive --reuse "bundle exec vite build" + migrate: app exec --interactive --reuse "bin/rails db:migrate" - -# 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. +# Use persistent storage volumes for external storage of code, files, and database +# These volumes will be mounted to the container and persist between deployments volumes: - - "img_manager_storage:/rails/storage" + # Git code repository storage + - "/root/img_manager/code:/rails/code" + # Database storage with simpler configuration (no Z flag to avoid SELinux issues) + - "/root/img_manager/storage:/rails/storage" + # Uploaded files storage + - "/root/img_manager/uploads:/rails/public/uploads" + # Vite assets storage + - "/root/img_manager/public/vite:/rails/public/vite" + # Node modules storage + - "/root/img_manager/node_modules:/rails/node_modules" + # Logs storage + - "/root/img_manager/logs:/rails/log" + # Tmp directory for pids and other temporary files + - "/root/img_manager/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 +# Disable asset bridging for now to fix deployment issues +asset_path: false + +# Deployment configuration for better reliability +# Increase the time to wait for a container to become ready +# deploy_timeout: 180 + +# # Increase the time to wait for a container to drain +# drain_timeout: 60 + +# # Increase the time to wait for a container to boot after it is running +# readiness_delay: 30 + +# Note: Run the setup script manually before deployment: +# scp ./bin/setup_volumes.sh root@45.78.59.154:/root/setup_volumes.sh +# ssh root@45.78.59.154 'chmod +x /root/setup_volumes.sh && /root/setup_volumes.sh' # Configure the image builder. builder: diff --git a/registry_password.key.sample b/registry_password.key.sample new file mode 100644 index 0000000..7a26ef0 --- /dev/null +++ b/registry_password.key.sample @@ -0,0 +1 @@ +dckr_pat_3-xxxx \ No newline at end of file