Continuous deployment¶
For CD on a completely locked server we use a Github Actions self-hosted runner. It does not require any incoming ports to be open — the runner grabs tasks via long polling.
How it works¶
push to stg/main
│
▼
GitHub Actions
│ long polling (no open ports required)
▼
Self-hosted runner (staging/production server)
│
├─ 1. Pre-deploy DB backup
├─ 2. git pull + docker compose build + up
├─ 3. Health check loop → GET /ht/
└─ 4. Clean old backups (retain 7)
Initial server setup¶
1. Create GitHub environments¶
In your repo navigate to Settings → Environments and create two environments: staging and production.
2. Add a self-hosted runner¶
Navigate to Settings → Actions → Runners → New self-hosted runner and follow the instructions. When prompted for labels during setup, add the matching environment label (staging or production).
# Switch to appuser and navigate to home
su - appuser
cd /home/appuser
# Follow the runner download + configure instructions shown by GitHub
# When asked for labels, enter:
staging # for the staging server
# or
production # for the production server
3. Install the runner as a service¶
# Switch to root
sudo su
# Navigate to the runner folder
cd /home/appuser/actions-runner
# Install and start the service running as appuser
sudo ./svc.sh install appuser
sudo ./svc.sh start
# Verify it's running
sudo ./svc.sh status
Deployment workflow¶
The workflow file lives at .github/workflows/deploy-staging.yml. It runs automatically on every push to stg.
name: Deploy to Staging
on:
push:
branches:
- stg
jobs:
deploy:
runs-on:
- self-hosted
- staging
environment: staging
env:
APP_DIR: /home/appuser/${{ github.event.repository.name }}
steps:
- name: Start deployment
run: echo "🚀 Starting deployment to staging..."
- name: Pre-deploy database backup
working-directory: ${{ env.APP_DIR }}
run: task prod.db.backup
- name: Deploy application
working-directory: ${{ env.APP_DIR }}
run: task prod.deploy
- name: Wait for app to be healthy
working-directory: ${{ env.APP_DIR }}
run: task prod.health
- name: Clean old backups
working-directory: ${{ env.APP_DIR }}
run: task prod.db.backup.clean
- name: Deployment complete
run: echo "✅ Staging deployment successful"
Taskfile tasks involved¶
| Task | When | What it does |
|---|---|---|
prod.db.backup |
Pre-deploy | Dumps DB via greenmask before any changes |
prod.deploy |
Deploy | git pull → docker compose build → up -d → prune |
prod.health |
Post-deploy | Polls $SITE_URL/ht/ every 5s, up to 2 minutes |
prod.db.backup.clean |
Post-deploy | Removes backups older than the 7 most recent |
The prod.health task reads SITE_URL from your .env file automatically via Taskfile's dotenv directive.
Deployment flow per push¶
git push origin stg
│
▼
runner picks up the job
│
├─ greenmask dump → S3/storage
│
├─ git pull
├─ docker compose build
├─ docker compose up -d
├─ docker system prune -f
│
├─ GET /ht/ every 5s (max 2 min)
│ ├─ HTTP 200 → proceed
│ └─ timeout → dump logs + fail
│
└─ greenmask delete --retain-recent 7