Skip to content
Latchkey

Deploy production workflow (vas3k/vas3k.club)

The Deploy production workflow from vas3k/vas3k.club, explained and optimized by Latchkey.

A

CI health: A - excellent

Point runs-on at Latchkey and get job timeouts, self-healing for flaky steps, and up to 58% lower cost, applied automatically.

Grade your own workflow free or run it on Latchkey →
Source: vas3k/vas3k.club.github/workflows/deploy.ymlLicense MITView source

What it does

This is the Deploy production workflow from the vas3k/vas3k.club repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.

Below, Latchkey shows a faster, safer version produced by its optimization engine.

The workflow

workflow (.yml)
name: Deploy production

on:
  push:
    branches:
      - master
  workflow_dispatch:

permissions:
  contents: read
  packages: write

concurrency:
  group: deploy-production
  cancel-in-progress: false

jobs:
  tests:
    name: Run tests
    uses: ./.github/workflows/tests.yml

  build:
    name: Build image
    needs: tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

      - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0

      - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.TOKEN }}

      - id: meta
        uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
        with:
          images: ghcr.io/${{ github.actor }}/club
          tags: |
            type=sha
            type=raw,value=latest

      - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
        with:
          context: .
          push: true
          build-args: MODE=production
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=registry,ref=ghcr.io/${{ github.actor }}/club:buildcache
          cache-to: type=registry,ref=ghcr.io/${{ github.actor }}/club:buildcache,mode=max

  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    needs: build
    env:
      SSH_KEY_PATH: /tmp/ssh_key
    steps:
      - name: Checkout
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
      - name: Make envfile
        run: export | grep "secret_" | sed "s/declare -x secret_//" > .env
        env:
          secret_SECRET_KEY: ${{ secrets.SECRET_KEY }}
          secret_APP_HOST: ${{ secrets.APP_HOST }}
          secret_MEDIA_UPLOAD_URL: ${{ secrets.MEDIA_UPLOAD_URL }}
          secret_MEDIA_UPLOAD_CODE: ${{ secrets.MEDIA_UPLOAD_CODE }}
          secret_SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
          secret_EMAIL_HOST: ${{ secrets.EMAIL_HOST }}
          secret_EMAIL_HOST_USER: ${{ secrets.EMAIL_HOST_USER }}
          secret_EMAIL_HOST_PASSWORD: ${{ secrets.EMAIL_HOST_PASSWORD }}
          secret_TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
          secret_TELEGRAM_API_ID: ${{ secrets.TELEGRAM_API_ID }}
          secret_TELEGRAM_API_HASH: ${{ secrets.TELEGRAM_API_HASH }}
          secret_TELEGRAM_BOT_URL: ${{ secrets.TELEGRAM_BOT_URL }}
          secret_TELEGRAM_ADMIN_CHAT_ID: ${{ secrets.TELEGRAM_ADMIN_CHAT_ID }}
          secret_TELEGRAM_CLUB_CHANNEL_URL: ${{ secrets.TELEGRAM_CLUB_CHANNEL_URL }}
          secret_TELEGRAM_CLUB_CHANNEL_ID: ${{ secrets.TELEGRAM_CLUB_CHANNEL_ID }}
          secret_TELEGRAM_CLUB_CHAT_ID: ${{ secrets.TELEGRAM_CLUB_CHAT_ID }}
          secret_TELEGRAM_ONLINE_CHANNEL_URL: ${{ secrets.TELEGRAM_ONLINE_CHANNEL_URL }}
          secret_TELEGRAM_ONLINE_CHANNEL_ID: ${{ secrets.TELEGRAM_ONLINE_CHANNEL_ID }}
          secret_TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_DISCUSSION_ID: ${{ secrets.TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_DISCUSSION_ID }}
          secret_TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_ID: ${{ secrets.TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_ID }}
          secret_TELEGRAM_HELP_DESK_BOT_TOKEN: ${{ secrets.TELEGRAM_HELP_DESK_BOT_TOKEN }}
          secret_OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          secret_STRIPE_ACTIVE: ${{ secrets.STRIPE_ACTIVE }}
          secret_STRIPE_API_KEY: ${{ secrets.STRIPE_API_KEY }}
          secret_STRIPE_TICKETS_API_KEY: ${{ secrets.STRIPE_TICKETS_API_KEY }}
          secret_STRIPE_PUBLIC_KEY: ${{ secrets.STRIPE_PUBLIC_KEY }}
          secret_STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
          secret_STRIPE_TICKETS_WEBHOOK_SECRET: ${{ secrets.STRIPE_TICKETS_WEBHOOK_SECRET }}
          secret_PATREON_CLIENT_ID: ${{ secrets.PATREON_CLIENT_ID }}
          secret_PATREON_CLIENT_SECRET: ${{ secrets.PATREON_CLIENT_SECRET }}
          secret_YOOKASSA_API_KEY: ${{ secrets.YOOKASSA_API_KEY }}
          secret_YOOKASSA_SHOP_ID: ${{ secrets.YOOKASSA_SHOP_ID }}
          secret_JWT_PRIVATE_KEY: ${{ secrets.JWT_PRIVATE_KEY }}
          secret_WEBHOOK_SECRETS: ${{ secrets.WEBHOOK_SECRETS }}
          secret_POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
      - run: echo "GITHUB_SHA=sha-${GITHUB_SHA::7}" >> .env
      - run: echo "${{ secrets.PRODUCTION_SSH_KEY }}" > ${{ env.SSH_KEY_PATH }} && chmod 600 ${{ env.SSH_KEY_PATH }}
      - run: scp -o StrictHostKeyChecking=no -i ${{ env.SSH_KEY_PATH }} .env ${{ secrets.PRODUCTION_SSH_USERNAME }}@${{ secrets.PRODUCTION_SSH_HOST }}:/home/vas3k/vas3k.club/.env
      - run: scp -o StrictHostKeyChecking=no -i ${{ env.SSH_KEY_PATH }} docker-compose.production.yml ${{ secrets.PRODUCTION_SSH_USERNAME }}@${{ secrets.PRODUCTION_SSH_HOST }}:/home/vas3k/vas3k.club/docker-compose.production.yml
      - run: ssh -i ${{ env.SSH_KEY_PATH }} ${{ secrets.PRODUCTION_SSH_USERNAME }}@${{ secrets.PRODUCTION_SSH_HOST }} "cd /home/vas3k/vas3k.club && mkdir -p gdpr/downloads && chown 1000:1000 gdpr/downloads && docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.TOKEN }} && docker pull ghcr.io/${{ github.actor }}/club:sha-${GITHUB_SHA::7} && docker compose -f docker-compose.production.yml --env-file=.env up -d && docker image prune --filter 'until=72h' --force"

The same workflow, on Latchkey

Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.

name: Deploy production
 
on:
  push:
    branches:
      - master
  workflow_dispatch:
 
permissions:
  contents: read
  packages: write
 
concurrency:
  group: deploy-production
  cancel-in-progress: false
 
jobs:
  tests:
    timeout-minutes: 30
    name: Run tests
    uses: ./.github/workflows/tests.yml
 
  build:
    timeout-minutes: 30
    name: Build image
    needs: tests
    runs-on: latchkey-small
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
 
      - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
 
      - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.TOKEN }}
 
      - id: meta
        uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
        with:
          images: ghcr.io/${{ github.actor }}/club
          tags: |
            type=sha
            type=raw,value=latest
 
      - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
        with:
          context: .
          push: true
          build-args: MODE=production
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=registry,ref=ghcr.io/${{ github.actor }}/club:buildcache
          cache-to: type=registry,ref=ghcr.io/${{ github.actor }}/club:buildcache,mode=max
 
  deploy:
    timeout-minutes: 30
    name: Deploy
    runs-on: latchkey-small
    needs: build
    env:
      SSH_KEY_PATH: /tmp/ssh_key
    steps:
      - name: Checkout
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
      - name: Make envfile
        run: export | grep "secret_" | sed "s/declare -x secret_//" > .env
        env:
          secret_SECRET_KEY: ${{ secrets.SECRET_KEY }}
          secret_APP_HOST: ${{ secrets.APP_HOST }}
          secret_MEDIA_UPLOAD_URL: ${{ secrets.MEDIA_UPLOAD_URL }}
          secret_MEDIA_UPLOAD_CODE: ${{ secrets.MEDIA_UPLOAD_CODE }}
          secret_SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
          secret_EMAIL_HOST: ${{ secrets.EMAIL_HOST }}
          secret_EMAIL_HOST_USER: ${{ secrets.EMAIL_HOST_USER }}
          secret_EMAIL_HOST_PASSWORD: ${{ secrets.EMAIL_HOST_PASSWORD }}
          secret_TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
          secret_TELEGRAM_API_ID: ${{ secrets.TELEGRAM_API_ID }}
          secret_TELEGRAM_API_HASH: ${{ secrets.TELEGRAM_API_HASH }}
          secret_TELEGRAM_BOT_URL: ${{ secrets.TELEGRAM_BOT_URL }}
          secret_TELEGRAM_ADMIN_CHAT_ID: ${{ secrets.TELEGRAM_ADMIN_CHAT_ID }}
          secret_TELEGRAM_CLUB_CHANNEL_URL: ${{ secrets.TELEGRAM_CLUB_CHANNEL_URL }}
          secret_TELEGRAM_CLUB_CHANNEL_ID: ${{ secrets.TELEGRAM_CLUB_CHANNEL_ID }}
          secret_TELEGRAM_CLUB_CHAT_ID: ${{ secrets.TELEGRAM_CLUB_CHAT_ID }}
          secret_TELEGRAM_ONLINE_CHANNEL_URL: ${{ secrets.TELEGRAM_ONLINE_CHANNEL_URL }}
          secret_TELEGRAM_ONLINE_CHANNEL_ID: ${{ secrets.TELEGRAM_ONLINE_CHANNEL_ID }}
          secret_TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_DISCUSSION_ID: ${{ secrets.TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_DISCUSSION_ID }}
          secret_TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_ID: ${{ secrets.TELEGRAM_HELP_DESK_BOT_QUESTION_CHANNEL_ID }}
          secret_TELEGRAM_HELP_DESK_BOT_TOKEN: ${{ secrets.TELEGRAM_HELP_DESK_BOT_TOKEN }}
          secret_OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          secret_STRIPE_ACTIVE: ${{ secrets.STRIPE_ACTIVE }}
          secret_STRIPE_API_KEY: ${{ secrets.STRIPE_API_KEY }}
          secret_STRIPE_TICKETS_API_KEY: ${{ secrets.STRIPE_TICKETS_API_KEY }}
          secret_STRIPE_PUBLIC_KEY: ${{ secrets.STRIPE_PUBLIC_KEY }}
          secret_STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
          secret_STRIPE_TICKETS_WEBHOOK_SECRET: ${{ secrets.STRIPE_TICKETS_WEBHOOK_SECRET }}
          secret_PATREON_CLIENT_ID: ${{ secrets.PATREON_CLIENT_ID }}
          secret_PATREON_CLIENT_SECRET: ${{ secrets.PATREON_CLIENT_SECRET }}
          secret_YOOKASSA_API_KEY: ${{ secrets.YOOKASSA_API_KEY }}
          secret_YOOKASSA_SHOP_ID: ${{ secrets.YOOKASSA_SHOP_ID }}
          secret_JWT_PRIVATE_KEY: ${{ secrets.JWT_PRIVATE_KEY }}
          secret_WEBHOOK_SECRETS: ${{ secrets.WEBHOOK_SECRETS }}
          secret_POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
      - run: echo "GITHUB_SHA=sha-${GITHUB_SHA::7}" >> .env
      - run: echo "${{ secrets.PRODUCTION_SSH_KEY }}" > ${{ env.SSH_KEY_PATH }} && chmod 600 ${{ env.SSH_KEY_PATH }}
      - run: scp -o StrictHostKeyChecking=no -i ${{ env.SSH_KEY_PATH }} .env ${{ secrets.PRODUCTION_SSH_USERNAME }}@${{ secrets.PRODUCTION_SSH_HOST }}:/home/vas3k/vas3k.club/.env
      - run: scp -o StrictHostKeyChecking=no -i ${{ env.SSH_KEY_PATH }} docker-compose.production.yml ${{ secrets.PRODUCTION_SSH_USERNAME }}@${{ secrets.PRODUCTION_SSH_HOST }}:/home/vas3k/vas3k.club/docker-compose.production.yml
      - run: ssh -i ${{ env.SSH_KEY_PATH }} ${{ secrets.PRODUCTION_SSH_USERNAME }}@${{ secrets.PRODUCTION_SSH_HOST }} "cd /home/vas3k/vas3k.club && mkdir -p gdpr/downloads && chown 1000:1000 gdpr/downloads && docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.TOKEN }} && docker pull ghcr.io/${{ github.actor }}/club:sha-${GITHUB_SHA::7} && docker compose -f docker-compose.production.yml --env-file=.env up -d && docker image prune --filter 'until=72h' --force"
 

What changed

What Latchkey heals here

This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:

This workflow runs 3 jobs per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.

Actions used in this workflow