Skip to Content
Welcome to the docs πŸŽ‰
Deployment

Deployment Guide

Byconvo uses automated CI/CD with GitHub Actions and Kamal 2 for zero-downtime deployments.

Deployment Overview

Every push to the main branch triggers an automated deployment of all three applications:

  1. Build Phase: Docker images are built for each app
  2. Push Phase: Images are pushed to Docker Hub
  3. Deploy Phase: Kamal deploys to the server with zero downtime

GitHub Actions Workflow

Main Workflow File

Location: .github/workflows/production.yml

Workflow Structure

The workflow contains three parallel jobs:

  • deploy-www - Deploys the marketing website
  • deploy-docs - Deploys the documentation site
  • deploy-monolith - Deploys the Rails application

Trigger Conditions

on: push: branches: [main] concurrency: group: "production" cancel-in-progress: false

Key Points:

  • Only main branch triggers deployment
  • Concurrent deployments are not allowed
  • New pushes wait for current deployment to finish

Deployment Steps

Each job follows these steps:

1. Checkout Code

- uses: actions/checkout@v4 with: fetch-depth: 0 # Full git history for versioning

Full git history is required for Kamal to generate version tags.

2. Docker Setup

- name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }}

Buildx enables advanced Docker features and multi-platform builds.

3. SSH Configuration

- name: Set up SSH uses: webfactory/[email protected] with: ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} - name: Add server to known hosts run: | mkdir -p ~/.ssh ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts

SSH is required for Kamal to connect to the deployment server.

4. Server Preparation

- name: Install Docker on server run: | ssh root@${{ secrets.SERVER_IP }} << 'ENDSSH' if ! command -v docker &> /dev/null; then curl -fsSL https://get.docker.com -o get-docker.sh sh get-docker.sh systemctl enable docker systemctl start docker fi ENDSSH

This step ensures Docker is installed on the server. It only installs if Docker is not already present.

5. Secrets Management

www & docs

- name: Create secrets file run: | mkdir -p apps/www/.kamal cat > apps/www/.kamal/secrets << EOF KAMAL_REGISTRY_PASSWORD=${KAMAL_REGISTRY_PASSWORD} EOF

monolith

The monolith has additional secret handling for RAILS_MASTER_KEY:

- name: Create secrets file run: | # Validate RAILS_MASTER_KEY is set if [ -z "${RAILS_MASTER_KEY}" ]; then echo "ERROR: RAILS_MASTER_KEY is not set!" exit 1 fi # Clean and validate key (must be 32 hex characters) RAILS_KEY_CLEAN=$(printf '%s' "${RAILS_MASTER_KEY}" | tr -d '[:space:]' | grep -oE '^[0-9a-f]{32}$' || echo "") # Write secrets file printf "KAMAL_REGISTRY_PASSWORD=%s\n" "${REGISTRY_PASS_CLEAN}" > .kamal/secrets printf "RAILS_MASTER_KEY=%s\n" "${RAILS_KEY_CLEAN}" >> .kamal/secrets

Validation:

  • Ensures RAILS_MASTER_KEY is exactly 32 hexadecimal characters
  • Strips whitespace and validates format
  • Fails deployment if validation fails

6. Kamal Deployment

Initial vs Update Deployment

- name: Deploy with Kamal run: | cd apps/www # Check if app is already running if ssh root@${{ secrets.SERVER_IP }} "docker ps | grep -q 'www-web.*Up'"; then KAMAL_CMD="deploy" # Zero-downtime update else KAMAL_CMD="setup" # Initial deployment fi docker run --rm \ -v "${GITHUB_WORKSPACE}:/workdir" \ -v "${SSH_AUTH_SOCK}:/ssh-agent" \ -v /var/run/docker.sock:/var/run/docker.sock \ -e "SSH_AUTH_SOCK=/ssh-agent" \ -e "KAMAL_REGISTRY_PASSWORD=${KAMAL_REGISTRY_PASSWORD}" \ -w "/workdir/apps/www" \ ghcr.io/basecamp/kamal:v2.2.1 ${KAMAL_CMD}

Commands:

  • setup - First-time deployment (creates proxy, networks, etc.)
  • deploy - Rolling update with zero downtime

Kamal Container:

  • Uses official Kamal 2.2.1 Docker image
  • Mounts repository root for git history
  • Mounts Docker socket for building images
  • Mounts SSH agent for server access

Monolith-Specific Deployment

Environment Verification

After deployment, the workflow verifies the Rails environment:

- name: Deploy monolith with Kamal run: | # ... deploy command ... DEPLOY_EXIT_CODE=$? # Check env file on server ssh root@${{ secrets.SERVER_IP }} " if [ -f .kamal/apps/monolith/env/roles/web.env ]; then grep -q '^RAILS_MASTER_KEY=' .kamal/apps/monolith/env/roles/web.env fi " # If deployment failed, check container environment if [ $DEPLOY_EXIT_CODE -ne 0 ]; then ssh root@${{ secrets.SERVER_IP }} " CONTAINER_NAME=\$(docker ps -a --filter 'service=monolith' --format '{{.Names}}' | head -1) docker exec \$CONTAINER_NAME sh -c 'echo \${RAILS_MASTER_KEY}' " exit $DEPLOY_EXIT_CODE fi

This helps debug environment variable issues in production.


Monolith CI Workflow

Location: apps/monolith/.github/workflows/ci.yml

Runs on pull requests and main branch pushes.

Jobs

1. JavaScript Linting (lint_js)

- Check prettier formatting - Check for linting errors (ESLint) - Check TypeScript types

Uses Node.js 22 with npm cache.

2. Ruby Security Scanning (scan_ruby)

- Brakeman (static security analysis) - bundler-audit (known vulnerabilities)

3. Ruby Linting (lint)

- RuboCop (style and quality) - Uses cache for faster runs

4. Testing (test)

- Install system dependencies (Chrome, etc.) - Install npm and Ruby dependencies - Run RSpec test suite - Upload failure screenshots

Test Environment:

  • SQLite database
  • Chrome for system tests
  • Screenshots saved on failure

Manual Deployment

You can deploy manually from your local machine:

Prerequisites

  1. Install Kamal:

    gem install kamal
  2. Configure SSH access to the server

  3. Create .kamal/secrets file with required secrets

Deploy Commands

Deploy www

cd apps/www kamal setup # First time only kamal deploy # Updates

Deploy docs

cd apps/docs kamal setup # First time only kamal deploy # Updates

Deploy monolith

cd apps/monolith kamal setup # First time only kamal deploy # Updates

Useful Kamal Commands

# View logs kamal app logs -f # Run Rails console (monolith) kamal app exec --interactive --reuse "bin/rails console" # Run shell in container kamal app exec --interactive --reuse "bash" # Check container status kamal details # Rollback to previous version kamal rollback # SSH into container kamal app exec --interactive --reuse "bash"

Deployment Flow Diagram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Developer pushes to main branch β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ GitHub Actions Triggered β”‚ β”‚ - Concurrent: false (wait for current deployment) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Parallel Jobs Start β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚deploy-wwwβ”‚ β”‚deploy-docsβ”‚ β”‚deploy-monolithβ”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ For Each Job: β”‚ β”‚ 1. Checkout code (full git history) β”‚ β”‚ 2. Setup Docker Buildx β”‚ β”‚ 3. Login to Docker Hub β”‚ β”‚ 4. Configure SSH access β”‚ β”‚ 5. Ensure Docker installed on server β”‚ β”‚ 6. Create secrets file β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Kamal Deployment β”‚ β”‚ - Run Kamal in Docker container β”‚ β”‚ - Mount workspace, SSH agent, Docker socket β”‚ β”‚ - Execute setup (first time) or deploy (update) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Kamal Orchestration on Server β”‚ β”‚ 1. Build Docker image (or pull from registry) β”‚ β”‚ 2. Push to Docker Hub β”‚ β”‚ 3. Pull image on server β”‚ β”‚ 4. Start new container β”‚ β”‚ 5. Wait for health check to pass β”‚ β”‚ 6. Switch traffic to new container β”‚ β”‚ 7. Stop old container β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Deployment Complete β”‚ β”‚ - Zero downtime achieved β”‚ β”‚ - Old container cleaned up β”‚ β”‚ - Logs available via GitHub Actions β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Troubleshooting

Common Issues

1. Docker Not Installed on Server

Error: command not found: docker

Solution: The workflow automatically installs Docker. If manual intervention needed:

ssh [email protected] curl -fsSL https://get.docker.com -o get-docker.sh sh get-docker.sh systemctl enable docker systemctl start docker

2. SSH Connection Failed

Error: Permission denied (publickey)

Solution:

  • Verify SSH_PRIVATE_KEY secret is correctly set in GitHub
  • Ensure the public key is in ~/.ssh/authorized_keys on the server
  • Check server IP is correct in secrets

3. RAILS_MASTER_KEY Invalid

Error: RAILS_MASTER_KEY length is X, expected exactly 32 characters!

Solution:

  • Verify the GitHub secret has exactly 32 hexadecimal characters
  • No extra whitespace or newlines
  • Copy directly from config/master.key file

4. Health Check Failed

Error: Container starts but health check fails

Solution:

  • Check application logs: kamal app logs -f
  • Verify /up endpoint returns 200 OK
  • Ensure container has correct environment variables

5. Port Already in Use

Error: address already in use

Solution:

ssh [email protected] docker ps # Find conflicting container kamal proxy restart # Restart Kamal proxy

Viewing Logs

GitHub Actions Logs

View deployment logs in GitHub Actions tab of repository.

Application Logs

# From local machine cd apps/<app-name> kamal app logs -f # Or via SSH ssh [email protected] docker logs -f <container-name>

Find Container Names

ssh [email protected] docker ps --filter "label=service=monolith" docker ps --filter "label=service=www" docker ps --filter "label=service=docs"

Best Practices

Pre-Deployment Checklist

  • Tests pass locally
  • Code reviewed and approved
  • No secrets committed to repository
  • Database migrations tested (monolith)
  • Environment variables updated if needed

Monitoring Deployment

  1. Watch GitHub Actions progress
  2. Check application immediately after deployment
  3. Monitor error tracking (if configured)
  4. Verify SSL certificates are valid

Rollback Strategy

If issues are detected post-deployment:

cd apps/<app-name> kamal rollback

This reverts to the previous Docker image within seconds.