Deploy apps using GitHub Actions
Using GitHub Actions, you can automatically deploy your apps when you push or merge changes to your git repository.
The guide below shows how to deploy the following types of apps:
To deploy other apps, use one of the GitHub Actions workflows below as a starting point for creating a workflow to deploy your app.
Create a deployment SSH key
Create an SSH key that GitHub Actions will use to SSH into your server when deploying the app.
- SSH into the server as the app’s system user.
- Create an SSH key pair without a password.
Terminal window ssh-keygen -f ~/.ssh/github_actions -q -N "" - Add the public key to the user’s
.ssh/authorized_keys
file.Terminal window umask 077 && mkdir -p ~/.ssh && touch ~/.ssh/authorized_keyscat ~/.ssh/github_actions.pub >> ~/.ssh/authorized_keys - Display the private key.
You’ll need the private key when configuring the GitHub repository’s environment.
Terminal window cat ~/.ssh/github_actions
Configure a GitHub environment
The GitHub Actions workflow that does the deployment will use secrets and environment variables that are specific to the environment. In most cases, you will at least have a “prod” or “production” environment. You may also have other environments such as “dev” and “stage”.
- In GitHub, go to the repository’s Settings page.
- Click Environments.
- If the desired environment already exists, click the name of the environment.
- If the desired environment does not already exist:
- Click New environment.
- Enter a name for the environment (for example, “prod”).
- Click Configure environment.
- Click Add environment secret.
- For Name, enter
SSH_PRIVATE_KEY
. - For Value, paste the private key.
- Click Add secret.
- For Name, enter
- Click Add environment variable.
- For Name, enter
REMOTE_HOST
. - For Value, enter the server’s IP address.
- Click Add variable.
- For Name, enter
- Click Add environment variable.
- For Name, enter
REMOTE_USER
. - For Value, enter the name of the app’s system user.
- Click Add variable.
- For Name, enter
- Click Add environment variable.
- For Name, enter
APP_NAME
. - For Value, enter the app’s name.
- Click Add variable.
- For Name, enter
Next, add secrets and environment variables that are specific to the type of application you’re deploying.
- Click Add environment variable.
- For Name, enter
DB_DATABASE
. - For Value, enter the app’s database name.
- Click Add variable.
- For Name, enter
- Click Add environment variable.
- For Name, enter
DB_USERNAME
. - For Value, enter the app’s database username.
- Click Add variable.
- For Name, enter
- Click Add environment secret.
- For Name, enter
DB_PASSWORD
. - For Value, enter the app’s database password.
- Click Add secret.
- For Name, enter
- Click Add environment secret.
- For Name, enter
LARAVEL_APP_KEY
. - For Value, paste the value of the Laravel app’s
APP_KEY
. Include thebase64:
at the start of the value. - Click Add secret.
- For Name, enter
There are no additional environment settings required for Astro.
Create a workflow
GitHub Actions workflows are YAML files you create in your
git repo’s .github/workflows/
directory.
The workflow defines the steps performed by GitHub Actions when
you push code or merge a PR to your default branch.
- In your git repo, create the file
.github/workflows/deploy.yaml
with the following contents..github/workflows/deploy.yaml name: Deploy Laravelon:# Run the workflow on pushes to the default branch.push:branches: [$default-branch]# Allows running this workflow manually from the Actions tab.workflow_dispatch:# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.concurrency:group: ${{ github.workflow }}-${{ github.ref }}cancel-in-progress: falsejobs:deploy:name: Deployruns-on: ubuntu-latestenvironment:name: prodsteps:- name: Checkoutuses: actions/checkout@v4- name: Create .env filerun: |cat <<-END_OF_ENV >>.envAPP_NAME=LaravelAPP_ENV=localAPP_KEY=${{ secrets.LARAVEL_APP_KEY }}APP_DEBUG=trueAPP_URL=http://localhostAPP_LOCALE=enAPP_FALLBACK_LOCALE=enAPP_FAKER_LOCALE=en_USAPP_MAINTENANCE_DRIVER=file# APP_MAINTENANCE_STORE=databasePHP_CLI_SERVER_WORKERS=4BCRYPT_ROUNDS=12LOG_CHANNEL=stackLOG_STACK=singleLOG_DEPRECATIONS_CHANNEL=nullLOG_LEVEL=debugDB_CONNECTION=mysqlDB_HOST=127.0.0.1DB_PORT=3306DB_DATABASE=${{ vars.DB_DATABASE }}DB_USERNAME=${{ vars.DB_USERNAME }}DB_PASSWORD=${{ secrets.DB_PASSWORD }}SESSION_DRIVER=databaseSESSION_LIFETIME=120SESSION_ENCRYPT=falseSESSION_PATH=/SESSION_DOMAIN=nullBROADCAST_CONNECTION=logFILESYSTEM_DISK=localQUEUE_CONNECTION=databaseCACHE_STORE=database# CACHE_PREFIX=REDIS_CLIENT=phpredisREDIS_HOST=127.0.0.1REDIS_PASSWORD=nullREDIS_PORT=6379MAIL_MAILER=logMAIL_SCHEME=nullMAIL_HOST=127.0.0.1MAIL_PORT=2525MAIL_USERNAME=nullMAIL_PASSWORD=nullMAIL_FROM_ADDRESS="hello@example.com"MAIL_FROM_NAME="\${APP_NAME}"END_OF_ENV- name: Install composer dependenciesrun: |composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader- name: Add SSH keyenv:SSH_AUTH_SOCK: /tmp/ssh_agent.sockrun: |umask 077 && mkdir -p ~/.sshumask 077 && touch ~/.ssh/github_actionsecho "${{ secrets.SSH_PRIVATE_KEY }}" >> ~/.ssh/github_actionsssh-agent -a $SSH_AUTH_SOCK > /dev/nullssh-add ~/.ssh/github_actionsecho -e "Host *\n\tStrictHostKeyChecking no" >> ~/.ssh/config- name: Deploy to ServerPilot appenv:SSH_AUTH_SOCK: /tmp/ssh_agent.sockrun: |set -eux -o pipefailCOMMIT_SHA=${{ github.sha }}COMMIT_SHORT_SHA=${COMMIT_SHA:0:7}VERSION=$(date '+%Y%m%d%H%M%S').${COMMIT_SHORT_SHA}DIST=dist.${VERSION}rsync -a ./ ${{ vars.REMOTE_USER }}@${{ vars.REMOTE_HOST }}:apps/${{ vars.APP_NAME }}/${DIST}/ssh ${{ vars.REMOTE_USER }}@${{ vars.REMOTE_HOST }} /bin/bash << EOFset -eux -o pipefailcd ~/apps/${{ vars.APP_NAME }}/${DIST}php artisan optimizephp artisan migratecd ~/apps/${{ vars.APP_NAME }}# If public exists but is not a symlink, move it out of the way.[[ ! -L public ]] && [[ -e public ]] && mv public public.bak.$(date '+%Y%m%d%H%M%S')# Create a new symlink and then atomically replace the existing symlink.ln -s ${DIST}/public public.${VERSION}mv --no-target-directory public.${VERSION} public# Delete all dist.* directories except the new one.shopt -s extglobrm -rf dist.!(${VERSION})EOF - Add and commit the file
.github/workflows/deploy.yaml
. - Push your changes to the default branch or merge to your default branch using a PR.
- In your git repo, create the file
.github/workflows/deploy.yaml
with the following contents..github/workflows/deploy.yaml name: Deploy Astroon:# Run the workflow on pushes to the default branch.push:branches: [$default-branch]# Allows running this workflow manually from the Actions tab.workflow_dispatch:# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.concurrency:group: ${{ github.workflow }}-${{ github.ref }}cancel-in-progress: falseenv:# The path to the Astro site in the repository.BUILD_PATH: "."jobs:build:name: Buildruns-on: ubuntu-lateststeps:- name: Checkoutuses: actions/checkout@v4- name: Detect package managerid: detect-package-managerrun: |if [ -f "${{ github.workspace }}/yarn.lock" ]; thenecho "manager=yarn" >> $GITHUB_OUTPUTecho "command=install" >> $GITHUB_OUTPUTecho "runner=yarn" >> $GITHUB_OUTPUTecho "lockfile=yarn.lock" >> $GITHUB_OUTPUTexit 0elif [ -f "${{ github.workspace }}/package.json" ]; thenecho "manager=npm" >> $GITHUB_OUTPUTecho "command=ci" >> $GITHUB_OUTPUTecho "runner=npx --no-install" >> $GITHUB_OUTPUTecho "lockfile=package-lock.json" >> $GITHUB_OUTPUTexit 0elseecho "Unable to determine package manager"exit 1fi- name: Setup Nodeuses: actions/setup-node@v4with:node-version: "20"cache: ${{ steps.detect-package-manager.outputs.manager }}cache-dependency-path: ${{ env.BUILD_PATH }}/${{ steps.detect-package-manager.outputs.lockfile }}- name: Install dependenciesrun: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }}working-directory: ${{ env.BUILD_PATH }}- name: Build with Astrorun: |${{ steps.detect-package-manager.outputs.runner }} astro buildls -al distworking-directory: ${{ env.BUILD_PATH }}- name: Store build artifactsuses: actions/upload-artifact@v4with:name: distpath: |${{ env.BUILD_PATH }}/distdeploy:environment:name: prodneeds: buildruns-on: ubuntu-latestname: Deploysteps:- name: Download build artifactsuses: actions/download-artifact@v4with:name: distpath: |dist- name: Add SSH keyenv:SSH_AUTH_SOCK: /tmp/ssh_agent.sockrun: |umask 077 && mkdir -p ~/.sshumask 077 && touch ~/.ssh/github_actionsecho "${{ secrets.SSH_PRIVATE_KEY }}" >> ~/.ssh/github_actionsssh-agent -a $SSH_AUTH_SOCK > /dev/nullssh-add ~/.ssh/github_actionsecho -e "Host *\n\tStrictHostKeyChecking no" >> ~/.ssh/config- name: Deploy to ServerPilot appenv:SSH_AUTH_SOCK: /tmp/ssh_agent.sockrun: |set -eux -o pipefailCOMMIT_SHA=${{ github.sha }}COMMIT_SHORT_SHA=${COMMIT_SHA:0:7}VERSION=$(date '+%Y%m%d%H%M%S').${COMMIT_SHORT_SHA}DIST=dist.${VERSION}rsync -a ./dist/ ${{ vars.REMOTE_USER }}@${{ vars.REMOTE_HOST }}:apps/${{ vars.APP_NAME }}/${DIST}/ssh ${{ vars.REMOTE_USER }}@${{ vars.REMOTE_HOST }} /bin/bash << EOFset -eux -o pipefailcd ~/apps/${{ vars.APP_NAME }}# If public exists but is not a symlink, move it out of the way.[[ ! -L public ]] && [[ -e public ]] && mv public public.bak.$(date '+%Y%m%d%H%M%S')# Create a new symlink and then atomically replace the existing symlink.ln -s ${DIST} public.${VERSION}mv --no-target-directory public.${VERSION} public# Delete all dist.* directories except the new one.shopt -s extglobrm -rf dist.!(${VERSION})EOF - Add and commit the file
.github/workflows/deploy.yaml
. - Push your changes to the default branch or merge to your default branch using a PR.
Manually trigger the workflow
The workflow will automatically run when changes are pushed or merged to your repository’s default branch.
To run the workflow manually, do the following.
- In GitHub, go to the repository’s Actions page.
- Click on the name of the workflow (for example, Deploy Laravel).
- Click Run workflow, select the desired branch, then click Run workflow.
- Refresh the page to see the new workflow run.
- Click on the name of the workflow run to see detailed status.