This guide shows how to use Shipd for automated deployments in GitHub Actions CI/CD pipelines.
Add the following secrets to your GitHub repository (Settings → Secrets and variables → Actions):
Required Secrets:
SSH_PRIVATE_KEY # SSH private key for server access
SSH_HOST # SSH host address (e.g., user@example.com)
CONTAINER_IMAGE # Container image (e.g., ghcr.io/username/app)
CONTAINER_NAME # Container name (e.g., myapp-prod)
Optional Secrets:
PORT_MAPPINGS # Port mappings (e.g., 80:3000,443:3001)
GHCR_USERNAME # GitHub Container Registry username
GHCR_TOKEN # GitHub Container Registry token
ENV_FILE # Environment variables (multiline)
Copy the example file:
mkdir -p .github/workflows
cp .github/workflows/deploy.yml.example .github/workflows/deploy.ymlOr manually create .github/workflows/deploy.yml:
name: Deploy with Shipd
on:
push:
tags:
- 'v*'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Install Shipd
run: |
curl -L https://github.com/guo/shipd/archive/refs/tags/v1.0.3.tar.gz | tar xz
cd shipd-1.0.3
sudo ./install.sh
shipd --version
- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
cat >> ~/.ssh/config <<EOF
Host *
IdentityFile ~/.ssh/deploy_key
StrictHostKeyChecking no
EOF
- name: Setup target
run: |
mkdir -p ~/.shipd/targets/myapp-prod
cat > ~/.shipd/targets/myapp-prod/.config <<EOF
CONTAINER_IMAGE="${{ secrets.CONTAINER_IMAGE }}"
SSH_HOST="${{ secrets.SSH_HOST }}"
CONTAINER_NAME="${{ secrets.CONTAINER_NAME }}"
PORT_MAPPINGS="${{ secrets.PORT_MAPPINGS }}"
GHCR_USERNAME="${{ secrets.GHCR_USERNAME }}"
GHCR_TOKEN="${{ secrets.GHCR_TOKEN }}"
EOF
echo "${{ secrets.ENV_FILE }}" > ~/.shipd/targets/myapp-prod/.env
- name: Deploy
run: shipd deploy -y myapp-prod ${{ github.ref_name }}git add .github/workflows/deploy.yml
git commit -m "Add GitHub Actions deployment"
git push# Create tag to trigger deployment
git tag v1.0.0
git push origin v1.0.0
# Or use GitHub UI to manually triggeron:
push:
tags:
- 'v*'When to use: Automatically deploy when creating version tags
git tag v1.2.3
git push origin v1.2.3 # Triggers deploymenton:
push:
branches:
- mainWhen to use: Auto-deploy to staging on every merge to main
git push origin main # Auto-deployson:
workflow_dispatch:
inputs:
target:
description: 'Target environment'
required: true
type: choice
options:
- production
- staging
image_tag:
description: 'Image tag'
required: false
default: 'latest'When to use: Manually select environment and version from GitHub UI
on:
push:
tags:
- 'v*'
jobs:
deploy-staging:
runs-on: ubuntu-latest
steps:
# ... setup
- run: shipd deploy -y myapp-staging ${{ github.ref_name }}
deploy-production:
runs-on: ubuntu-latest
needs: deploy-staging # Wait for staging success
steps:
# ... setup
- run: shipd deploy -y myapp-prod ${{ github.ref_name }}When to use: Deploy to staging first, then production after success
- name: Setup Compose target
run: |
mkdir -p ~/.shipd/targets/myapp-prod
# Compose mode only needs SSH_HOST
cat > ~/.shipd/targets/myapp-prod/.config <<EOF
SSH_HOST="${{ secrets.SSH_HOST }}"
GHCR_USERNAME="${{ secrets.GHCR_USERNAME }}"
GHCR_TOKEN="${{ secrets.GHCR_TOKEN }}"
EOF
# Copy compose file
cp docker-compose.yml ~/.shipd/targets/myapp-prod/compose.yml
# Environment file
echo "${{ secrets.ENV_FILE }}" > ~/.shipd/targets/myapp-prod/.env- name: Setup Caddy target
run: |
mkdir -p ~/.shipd/targets/myapp-prod
cat > ~/.shipd/targets/myapp-prod/.config <<EOF
USE_CADDY="true"
DOMAIN="${{ secrets.DOMAIN }}"
APP_PORT="3000"
HEALTH_CHECK_PATH="/health"
CONTAINER_IMAGE="${{ secrets.CONTAINER_IMAGE }}"
SSH_HOST="${{ secrets.SSH_HOST }}"
GHCR_USERNAME="${{ secrets.GHCR_USERNAME }}"
GHCR_TOKEN="${{ secrets.GHCR_TOKEN }}"
EOF
# First-time Caddy setup
- name: Setup Caddy (first time only)
run: shipd setup-caddy myapp-prod
if: github.run_number == 1 # Only on first run- name: Deploy to all targets
run: |
# Setup all targets
for target in staging demo production; do
mkdir -p ~/.shipd/targets/$target
# ... configure each target
done
# Batch deploy
shipd deploy-multi -y --alljobs:
deploy:
runs-on: ubuntu-latest
strategy:
matrix:
target: [staging, demo, production]
steps:
- name: Deploy to ${{ matrix.target }}
run: shipd deploy -y ${{ matrix.target }} ${{ github.ref_name }}# Generate new key pair
ssh-keygen -t ed25519 -C "github-actions" -f ~/.ssh/github_actions_key -N ""
# Copy public key to server
ssh-copy-id -i ~/.ssh/github_actions_key.pub user@server.com
# Copy private key content for GitHub Secrets
cat ~/.ssh/github_actions_key
# Copy entire output, including BEGIN and END lines- Go to repository Settings → Secrets and variables → Actions
- Click "New repository secret"
- Name:
SSH_PRIVATE_KEY - Value: Paste entire private key content (including header/footer)
- Click "Add secret"
# In GitHub Secrets
ENV_FILE = "DATABASE_URL=postgres://...\nAPI_KEY=secret123"# In GitHub Secrets, paste directly with newlines
DATABASE_URL=postgres://...
API_KEY=secret123
PORT=3000
NODE_ENV=productionjobs:
deploy-staging:
environment: staging
steps:
- run: shipd deploy -y staging ${{ github.ref_name }}
deploy-production:
environment: production
needs: deploy-staging
steps:
- run: shipd deploy -y production ${{ github.ref_name }}- name: Notify success
if: success()
run: |
curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
-H 'Content-Type: application/json' \
-d '{"text":"✅ Deployment succeeded: ${{ github.ref_name }}"}'
- name: Notify failure
if: failure()
run: |
curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
-H 'Content-Type: application/json' \
-d '{"text":"❌ Deployment failed: ${{ github.ref_name }}"}'- name: Cache Shipd
uses: actions/cache@v3
with:
path: ~/shipd
key: shipd-v1.0.3
- name: Install Shipd
run: |
if [ ! -f ~/shipd/shipd.sh ]; then
cd ~
curl -L https://github.com/guo/shipd/archive/refs/tags/v1.0.3.tar.gz | tar xz
mv shipd-1.0.3 shipd
fi
cd ~/shipd && sudo ./install.sh- name: Deploy
run: shipd deploy -y myapp-prod ${{ github.ref_name }}
- name: Verify deployment
run: |
# Wait for container to start
sleep 10
# Health check
curl -f https://myapp.com/health || exit 1
echo "✅ Deployment verified"- name: Test SSH connection
run: |
ssh -i ~/.ssh/deploy_key ${{ secrets.SSH_HOST }} "echo 'SSH connection OK'"- name: Deploy with debug
run: |
set -x # Enable debug output
shipd deploy -y myapp-prod ${{ github.ref_name }}- name: Upload logs
if: failure()
uses: actions/upload-artifact@v3
with:
name: deployment-logs
path: |
deploy-*.log
~/.shipd/See complete example: .github/workflows/deploy.yml.example
- ✅ Use dedicated SSH keys (not personal keys)
- ✅ Limit SSH key permissions (deploy only, no code modification)
- ✅ Use GitHub Environments to protect production
- ✅ Rotate keys and tokens regularly
- ✅ Don't print sensitive info in logs
- ✅ Use principle of least privilege