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:
- Build Phase: Docker images are built for each app
- Push Phase: Images are pushed to Docker Hub
- 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 websitedeploy-docs- Deploys the documentation sitedeploy-monolith- Deploys the Rails application
Trigger Conditions
on:
push:
branches: [main]
concurrency:
group: "production"
cancel-in-progress: falseKey Points:
- Only
mainbranch 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 versioningFull 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_hostsSSH 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
ENDSSHThis 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}
EOFmonolith
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/secretsValidation:
- Ensures
RAILS_MASTER_KEYis 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
fiThis 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 typesUses 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 runs4. Testing (test)
- Install system dependencies (Chrome, etc.)
- Install npm and Ruby dependencies
- Run RSpec test suite
- Upload failure screenshotsTest Environment:
- SQLite database
- Chrome for system tests
- Screenshots saved on failure
Manual Deployment
You can deploy manually from your local machine:
Prerequisites
-
Install Kamal:
gem install kamal -
Configure SSH access to the server
-
Create
.kamal/secretsfile with required secrets
Deploy Commands
Deploy www
cd apps/www
kamal setup # First time only
kamal deploy # UpdatesDeploy docs
cd apps/docs
kamal setup # First time only
kamal deploy # UpdatesDeploy monolith
cd apps/monolith
kamal setup # First time only
kamal deploy # UpdatesUseful 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 docker2. SSH Connection Failed
Error: Permission denied (publickey)
Solution:
- Verify
SSH_PRIVATE_KEYsecret is correctly set in GitHub - Ensure the public key is in
~/.ssh/authorized_keyson 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.keyfile
4. Health Check Failed
Error: Container starts but health check fails
Solution:
- Check application logs:
kamal app logs -f - Verify
/upendpoint 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 proxyViewing 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
- Watch GitHub Actions progress
- Check application immediately after deployment
- Monitor error tracking (if configured)
- Verify SSL certificates are valid
Rollback Strategy
If issues are detected post-deployment:
cd apps/<app-name>
kamal rollbackThis reverts to the previous Docker image within seconds.