Sachith Dassanayake Software Engineering Laravel API on Docker with GHCR – End-to-End Playbook

Laravel API on Docker with GHCR – End-to-End Playbook

Tech stack: Laravel (APIs & Scheduler), PHP 8.4, MySQL 8.4, Nginx — everything in Docker containers.

Environments: Local development → Dev → Prod

Source control: Private GitHub repo (my-laravel-project)

Security stance: No secrets in Git ever; never bake secrets into images; pin prod images by digest; use Docker secrets in prod.


Table of Contents

  1. Phase 0 — Prepare the Repository
  2. Phase 1 — Local Development with Docker
  3. Phase 1.5 — Git Beginner Workflow
  4. Phase 2 — Push Images to GHCR Manually
  5. Phase 3 — Deploy on Dev VM (RHEL)
  6. Phase 4 — Plan & Deploy on Prod VM (RHEL, Pinned Digest)
  7. Phase 5 — GitHub Actions (PR verify, build & push, deploy)
  8. RHEL / SELinux / Firewall Notes
  9. Security Checklist
  10. Troubleshooting
  11. Appendix — File & Variable Cheat-Sheet

Phase 0 — Prepare the Repository (one-time)

Your repo root is the Laravel project directory: my-laravel-project/.

Recommended structure

    my-laravel-project/
    ├─ app/ … (Laravel code)
    ├─ bootstrap/
    ├─ config/
    ├─ database/
    ├─ public/
    ├─ resources/
    ├─ routes/
    ├─ storage/
    ├─ vendor/                      # ignored by Git
    ├─ .env                         # optional for local non-Docker runs; never committed
    ├─ .env.example                 # commit a safe template
    ├─ .gitignore
    ├─ .dockerignore
    ├─ Dockerfile                   # prod (php-fpm)
    ├─ Dockerfile.dev               # dev
    ├─ docker-compose.dev.yml
    ├─ docker-compose.dev.server.yml
    ├─ docker-compose.prod.yml
    └─ deploy/
       ├─ nginx.conf
       ├─ entrypoint.sh
       ├─ php.ini                   # optional PHP overrides
       ├─ dev.app.env.example       # commit a template (no secrets)
       ├─ prod.app.env.example      # commit a template (no secrets)
       └─ README-secrets.md         # explain where to store real secrets

.gitignore

/vendor /node_modules /.idea /.vscode .env deploy/*.env deploy/*.secret 

.dockerignore (must be at repo root)

.git vendor node_modules storage/*.key .env .env.* deploy/*.env deploy/*.secret docker-compose*.yml 

Why: keeps secrets and heavy folders out of the build context so they never get included in an image.


Phase 1 — Local Development with Docker

1) Install prerequisites

  • Windows/macOS: install Docker Desktop.
  • Linux: install Docker Engine + Docker Compose plugin.
  • RHEL notes are at the end (Docker CE install, SELinux, firewalld).

2) Dockerfile (prod, PHP 8.4 FPM)

Save as Dockerfile (repo root).

# syntax=docker/dockerfile:1.7 FROM php:8.4-fpm-alpine

RUN apk add --no-cache icu-dev libzip-dev oniguruma-dev bash curl tzdata
&& docker-php-ext-install -j"$(nproc)" pdo pdo_mysql intl zip opcache
&& { echo "opcache.enable=1"; echo "opcache.enable_cli=0"; echo "opcache.validate_timestamps=0"; }
> /usr/local/etc/php/conf.d/opcache.ini

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html

COPY composer.json composer.lock ./
RUN composer install --no-dev --prefer-dist --no-interaction --no-ansi --no-progress

COPY . .

RUN composer dump-autoload -o
&& mkdir -p storage bootstrap/cache
&& chown -R www-data:www-data storage bootstrap/cache

Optional PHP overrides
COPY deploy/php.ini /usr/local/etc/php/conf.d/99-custom.ini

COPY deploy/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

EXPOSE 9000
ENTRYPOINT ["/entrypoint.sh"]
CMD ["php-fpm","-F"]

3) Dockerfile.dev (development image)

Save as Dockerfile.dev.

# syntax=docker/dockerfile:1.7 FROM php:8.4-fpm-alpine ARG INSTALL_XDEBUG=false

RUN apk add --no-cache icu-dev libzip-dev oniguruma-dev bash curl tzdata git
&& docker-php-ext-install -j"$(nproc)" pdo pdo_mysql intl zip

RUN if [ "$INSTALL_XDEBUG" = "true" ]; then
apk add --no-cache $PHPIZE_DEPS && pecl install xdebug && docker-php-ext-enable xdebug &&
{ echo "xdebug.mode=develop,debug"; echo "xdebug.client_host=host.docker.internal"; }
> /usr/local/etc/php/conf.d/xdebug.ini ;
fi

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
CMD ["php-fpm","-F"]

4) Nginx reverse proxy

Save as deploy/nginx.conf.

server { listen 8080; server_name _; root /var/www/html/public; index index.php;

location / {
try_files $uri /index.php?$query_string;
}

location ~ .php$ {
include fastcgi_params;
fastcgi_pass php:9000;
fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;
fastcgi_read_timeout 180;
}

location ^~ /storage/ {
expires 1h;
add_header Cache-Control "public";
try_files $uri =404;
}

client_max_body_size 20m;
}

5) entrypoint.sh (what it does)

Save as deploy/entrypoint.sh (make it executable).

#!/usr/bin/env bash set -euo pipefail
Load docker secrets into env if present (files under /run/secrets/*)

load_secret(){ [ -f "/run/secrets/$1" ] && [ -z "${!1:-}" ] && export "$1"="$(cat "/run/secrets/$1")"; }
for s in APP_KEY DB_PASSWORD DB_USERNAME DB_HOST DB_DATABASE; do load_secret "$s" || true; done

cd /var/www/html

Permissions Laravel needs

mkdir -p storage bootstrap/cache
chown -R www-data:www-data storage bootstrap/cache || true
chmod -R ug+rw storage bootstrap/cache || true

Enforce APP_KEY in prod; auto-generate for dev if missing

if [ -z "${APP_KEY:-}" ]; then
if [ "${APP_ENV:-production}" = "production" ]; then
echo "ERROR: APP_KEY missing in production." >&2
exit 1
else
export APP_KEY="$(php artisan key:generate --show || true)"
fi
fi

Warm caches (prod)

if [ "${APP_ENV:-production}" = "production" ]; then
php artisan config:cache || true
php artisan route:cache || true
php artisan view:cache || true
fi

Auto-migrate if enabled

[ "${RUN_MIGRATIONS:-false}" = "true" ] && php artisan migrate --force || true

exec "$@"

6) docker-compose.dev.yml (local dev)

services: php: build: context: . dockerfile: Dockerfile.dev args: { INSTALL_XDEBUG: "false" } # flip to true for debugging env_file: [ ./deploy/dev.app.env ] # created locally; .gitignored volumes: - ./:/var/www/html - composer-cache:/var/www/html/vendor depends_on: [ db ] networks: [ appnet ]

nginx:
image: nginx:1.27-alpine
ports: [ "8080:8080" ]
depends_on: [ php ]
volumes:
- ./deploy/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./public:/var/www/html/public:ro
- ./storage/app/public:/var/www/html/public/storage
networks: [ appnet ]

db:
image: mysql:8.4
command: ["mysqld","--character-set-server=utf8mb4","--collation-server=utf8mb4_unicode_ci"]
environment:
MYSQL_DATABASE: app
MYSQL_USER: app
MYSQL_PASSWORD: secret
MYSQL_ROOT_PASSWORD: root
ports: [ "3306:3306" ] # dev only
volumes: [ dbdata:/var/lib/mysql ]
networks: [ appnet ]

volumes:
dbdata:
composer-cache:
networks:
appnet:

Create deploy/dev.app.env (locally, do NOT commit):

APP_ENV=local APP_DEBUG=true APP_URL=http://localhost:8080

DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=app
DB_USERNAME=app
DB_PASSWORD=secret

QUEUE_CONNECTION=database

Start local dev:

docker compose -f docker-compose.dev.yml up -d --build docker compose -f docker-compose.dev.yml exec php php artisan migrate docker compose -f docker-compose.dev.yml logs -f php 

Phase 1.5 — Git Beginner Workflow

# first clone git clone git@github.com:<ORG>/<REPO>.git cd my-laravel-project
create feature from development

git checkout development
git pull
git checkout -b feature/add-users-endpoint

work locally, run tests, then commit

git add .
git commit -m "feat(users): add index endpoint + tests"

push and open PR

git push -u origin feature/add-users-endpoint

Branching convention: development → Dev VM, main → Prod VM.


Phase 2 — Push Images to GHCR Manually (no Actions yet)

1) Create a GitHub Personal Access Token (PAT)

  • Fine-grained PAT scoped to your repo/org: Packages: Read & Write.
  • If the org enforces SSO, authorize the PAT to that org.

2) Docker login to GHCR (from your laptop)

export GH_USER="<github_username_or_org_service_account>" export GH_PAT="<pat_with_write:packages>" echo "$GH_PAT" | docker login ghcr.io -u "$GH_USER" --password-stdin 

3) Build & push the Dev image

export OWNER="<github-org-or-username&gt>" export REPO="<repo-name>" export IMG="ghcr.io/${OWNER,,}/${REPO,,}-php" export SHA=$(git rev-parse --short HEAD)

docker build -f Dockerfile.dev -t "$IMG:dev" -t "$IMG:dev-$SHA" .
docker push "$IMG:dev" "$IMG:dev-$SHA"

4) Build & push the Prod image and capture digest

docker build -f Dockerfile -t "$IMG:prod" -t "$IMG:prod-$SHA" . docker push "$IMG:prod" "$IMG:prod-$SHA"
print immutable digest to pin in prod compose

docker pull "$IMG:prod"
docker inspect --format='{{index .RepoDigests 0}}' "$IMG:prod"

Example: ghcr.io/owner/repo-php@sha256:abcdef...

On Apple Silicon (ARM) with AMD64 servers? Publish multi-arch:

docker buildx create --name multi --use >/dev/null 2>&1 || true

docker buildx build --platform linux/amd64,linux/arm64
-f Dockerfile.dev -t "$IMG:dev" -t "$IMG:dev-$SHA" --push .

docker buildx build --platform linux/amd64,linux/arm64
-f Dockerfile -t "$IMG:prod" -t "$IMG:prod-$SHA" --push .

Phase 3 — Deploy on Dev VM (RHEL)

0) Install Docker CE on RHEL (summary)

sudo dnf -y install dnf-plugins-core sudo dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo sudo dnf -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin sudo systemctl enable --now docker sudo usermod -aG docker $USER newgrp docker 

SELinux: prefer named volumes. For bind mounts (e.g. nginx.conf), add :Z to relabel.

1) Create project folder on Dev VM

sudo mkdir -p /opt/myapi/deploy cd /opt/myapi 

2) docker-compose.dev.server.yml (image-based dev)

Save on the Dev VM: /opt/myapi/docker-compose.dev.server.yml

services: php: image: ghcr.io/<owner>/<repo>-php:dev pull_policy: always env_file: [ ./deploy/dev.app.env ] environment: { RUN_MIGRATIONS: "true", APP_ENV: local, APP_DEBUG: "true" } depends_on: [ db ] networks: [ appnet ] volumes: - storage:/var/www/html/storage - cache:/var/www/html/bootstrap/cache

nginx:
image: nginx:1.27-alpine
depends_on: [ php ]
ports: [ "8080:8080" ]
volumes:
- ./deploy/nginx.conf:/etc/nginx/conf.d/default.conf:ro,Z
- storage:/var/www/html/public/storage:ro
networks: [ appnet ]
restart: unless-stopped

db:
image: mysql:8.4
command: ["mysqld","--character-set-server=utf8mb4","--collation-server=utf8mb4_unicode_ci"]
env_file: [ ./deploy/dev.db.env ]
volumes: [ dbdata:/var/lib/mysql ]
networks: [ appnet ]
restart: unless-stopped

volumes:
dbdata:
storage:
cache:
networks:
appnet:

3) Env files on Dev VM (do NOT commit)

Save /opt/myapi/deploy/dev.app.env:

APP_NAME=MyAPI APP_ENV=local APP_DEBUG=true APP_URL=http://<dev-ip>:8080

DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=app
DB_USERNAME=app
DB_PASSWORD=secret

QUEUE_CONNECTION=database

Save /opt/myapi/deploy/dev.db.env:

MYSQL_DATABASE=app MYSQL_USER=app MYSQL_PASSWORD=secret MYSQL_ROOT_PASSWORD=root 

4) Login to GHCR on the Dev VM & deploy

export GH_USER="<github_user_or_service_acct>" export GH_PAT="<pat_with_read:packages>" echo "$GH_PAT" | docker login ghcr.io -u "$GH_USER" --password-stdin

docker compose -f docker-compose.dev.server.yml pull
docker compose -f docker-compose.dev.server.yml up -d

Phase 4 — Plan & Deploy on Prod VM (RHEL, Pinned Digest)

1) docker-compose.prod.yml (use pinned digest + secrets)

Save on the Prod VM: /opt/myapi/docker-compose.prod.yml

services: php: image: ghcr.io/<owner>/<repo>-php:prod@sha256:<digest> pull_policy: always env_file: [ ./deploy/app.env ] # server-local, non-secret secrets: [ APP_KEY, DB_PASSWORD ] # real secrets as files environment: { APP_ENV: production, APP_DEBUG: "false", RUN_MIGRATIONS: "false" } volumes: - storage:/var/www/html/storage - cache:/var/www/html/bootstrap/cache networks: [ private ] restart: unless-stopped security_opt: [ "no-new-privileges:true" ] tmpfs: [ /tmp ]

nginx:
image: nginx:1.27-alpine
depends_on: [ php ]
ports: [ "80:8080" ] # or terminate TLS here on 443
volumes:
- ./deploy/nginx.conf:/etc/nginx/conf.d/default.conf:ro,Z
- storage:/var/www/html/public/storage:ro
networks: [ public, private ]
restart: unless-stopped
read_only: true
tmpfs: [ /var/run, /var/cache/nginx ]
cap_drop: [ "ALL" ]
security_opt: [ "no-new-privileges:true" ]

db:
image: mysql:8.4
env_file: [ ./deploy/db.env ]
volumes: [ dbdata:/var/lib/mysql ]
networks: [ private ]
restart: unless-stopped

secrets:
APP_KEY: { file: ./deploy/APP_KEY.secret } # base64:... from artisan key:generate --show
DB_PASSWORD: { file: ./deploy/DB_PASSWORD.secret }

volumes:
dbdata:
storage:
cache:

networks:
public: { driver: bridge }
private: { driver: bridge }

2) Server-local files (never commit)

Save /opt/myapi/deploy/app.env (non-secret runtime settings):

APP_NAME=MyAPI APP_ENV=production APP_DEBUG=false APP_URL=https://api.example.com

DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=app
DB_USERNAME=app

DB_PASSWORD via secret

QUEUE_CONNECTION=database
LOG_LEVEL=info

Save /opt/myapi/deploy/db.env (DB bootstrap):

MYSQL_DATABASE=app MYSQL_USER=app MYSQL_PASSWORD=<strong-password> MYSQL_ROOT_PASSWORD=<very-strong-root> 

Save secrets (files):

  • deploy/APP_KEY.secret → output of php artisan key:generate --show
  • deploy/DB_PASSWORD.secret → same value used as DB user password

3) Login & deploy

echo "<PAT_with_read:packages>" | docker login ghcr.io -u "<github_user>" --password-stdin docker compose -f docker-compose.prod.yml pull docker compose -f docker-compose.prod.yml up -d
Migrations (manual, explicit)

docker compose -f docker-compose.prod.yml exec php php artisan migrate --force

Next releases: push a new :prod, capture the new digest, update compose, then pull and up -d.


Phase 5 — GitHub Actions (PR verify, build & push, deploy)

Start manually (Phases 2–4). When stable, add automation:

GitHub Actions secrets to add

  • DEV_SSH_HOST, DEV_SSH_USER, DEV_SSH_KEY (for Dev VM deploy via SSH).
  • PROD_SSH_HOST, PROD_SSH_USER, PROD_SSH_KEY (optional; use environment protection).
  • GHCR_PAT_READ (optional). Most steps can use GITHUB_TOKEN on runner; remote servers still need their own login.

A) PR verification (build & test; no push)

Save as .github/workflows/ci-pr.yml

name: CI — PR Verify on: pull_request: { branches: [ development, main ] }

jobs:
verify:
runs-on: ubuntu-latest
permissions: { contents: read, packages: read }
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
tools: composer:v2
- run: composer install --no-interaction --prefer-dist --no-progress
- run: php artisan test --no-interaction || vendor/bin/phpunit
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.dev
push: false
tags: pr-test:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- uses: aquasecurity/trivy-action@0.24.0
with:
image-ref: pr-test:${{ github.sha }}
vuln-type: 'os,library'
severity: 'CRITICAL,HIGH'
ignore-unfixed: true
exit-code: '1'

B) Build & Push Dev on development

Save as .github/workflows/dev-build.yml

name: Build & Push — Dev on: push: { branches: [ development ] }

jobs:
build-dev:
runs-on: ubuntu-latest
permissions: { contents: read, packages: write }
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/setup-buildx-action@v3
- name: Build & push dev (multi-arch)
run: |
IMAGE="ghcr.io/${GITHUB_REPOSITORY,,}-php"
docker buildx build --platform linux/amd64,linux/arm64
-f Dockerfile.dev -t "$IMAGE:dev" -t "$IMAGE:dev-${GITHUB_SHA}" --push .

C) Auto-deploy Dev (SSH to Dev VM)

Save as .github/workflows/dev-deploy.yml

name: Deploy — Dev on: workflow_run: workflows: [ "Build & Push — Dev" ] types: [ completed ]

jobs:
deploy:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Write SSH key
run: |
echo "${{ secrets.DEV_SSH_KEY }}" > id_rsa
chmod 600 id_rsa
- name: Add host key
run: |
mkdir -p ~/.ssh
ssh-keyscan -t ed25519,rsa ${{ secrets.DEV_SSH_HOST }} >> ~/.ssh/known_hosts
- name: SSH deploy
env:
GHCR_USER: ${{ github.actor }}
GHCR_PAT: ${{ secrets.GITHUB_TOKEN }}
run: |
ssh -i id_rsa ${{ secrets.DEV_SSH_USER }}@${{ secrets.DEV_SSH_HOST }} <<'EOF'
set -e
cd /opt/myapi
echo "$GHCR_PAT" | docker login ghcr.io -u "$GHCR_USER" --password-stdin
docker compose -f docker-compose.dev.server.yml pull
docker compose -f docker-compose.dev.server.yml up -d
EOF

D) Build & Push Prod on main (emit digest)

Save as .github/workflows/prod-build.yml

name: Build & Push — Prod on: push: branches: [ main ] tags: [ 'v*.*.*' ]

jobs:
build-prod:
runs-on: ubuntu-latest
permissions: { contents: read, packages: write }
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/setup-buildx-action@v3
- name: Build & push prod (multi-arch)
run: |
IMAGE="ghcr.io/${GITHUB_REPOSITORY,,}-php"
docker buildx build --platform linux/amd64,linux/arm64
-f Dockerfile -t "$IMAGE:prod" -t "$IMAGE:prod-${GITHUB_SHA}" --push .
- name: Output digest (for compose pinning)
run: |
IMAGE="ghcr.io/${GITHUB_REPOSITORY,,}-php:prod"
docker buildx imagetools inspect "$IMAGE"

E) Deploy Prod (approval-gated)

Save as .github/workflows/prod-deploy.yml

name: Deploy — Prod on: workflow_dispatch: inputs: digest: description: "Image digest (ghcr.io/owner/repo-php@sha256:...)" required: true

env:
TARGET_DIR: /opt/myapi

jobs:
deploy:
runs-on: ubuntu-latest
environment: production # require reviewers in Environments
steps:
- name: Write SSH key
run: |
echo "${{ secrets.PROD_SSH_KEY }}" > id_rsa
chmod 600 id_rsa
- name: Add host key
run: |
mkdir -p ~/.ssh
ssh-keyscan -t ed25519,rsa ${{ secrets.PROD_SSH_HOST }} >> ~/.ssh/known_hosts
- name: Deploy via SSH
env:
DIGEST: ${{ inputs.digest }}
GHCR_USER: ${{ github.actor }}
GHCR_PAT: ${{ secrets.GITHUB_TOKEN }}
run: |
ssh -i id_rsa ${{ secrets.PROD_SSH_USER }}@${{ secrets.PROD_SSH_HOST }} <<'EOF'
set -e
cd $TARGET_DIR
# Update compose to use the new $DIGEST (template your compose to read an env var, or edit file then pull)
echo "$GHCR_PAT" | docker login ghcr.io -u "$GHCR_USER" --password-stdin
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
EOF

Tip: Template your prod compose (e.g., set image: ${PHP_IMAGE}) and pass PHP_IMAGE as the digest string to avoid editing files on the server.


RHEL / SELinux / Firewall Notes

  • SELinux: Prefer named volumes. For bind mounts, add :Z to relabel paths for containers.
  • firewalld: sudo firewall-cmd --add-service=http --add-service=https --permanent && sudo firewall-cmd --reload
  • Enable Docker: sudo systemctl enable --now docker
  • Podman vs Docker: This guide uses Docker CE. Podman works but compose flags differ.

Security Checklist (must-do)

  • Never commit secrets. Keep deploy/*.env and deploy/*.secret out of Git.
  • .dockerignore excludes .env, so secrets never enter images.
  • Use Docker secrets in prod (mounted under /run/secrets, loaded by entrypoint.sh).
  • Pin prod images by digest (@sha256:…). Avoid :latest in prod.
  • Expose only Nginx to the host. Keep PHP-FPM and MySQL on a private network; do not publish port 3306 in prod.
  • Harden containers: no-new-privileges, read_only (Nginx), tmpfs for tmp paths.
  • Rotate DB credentials periodically; rotate PATs as needed.
  • Back up volumes: dbdata (and storage if you store uploads).

Troubleshooting

  • 502 from Nginx → Check PHP logs: docker compose logs -f php nginx; ensure fastcgi_pass php:9000 and service names match.
  • Migrations fail on first boot → Set RUN_MIGRATIONS=true for Dev; in Prod, run migrations manually.
  • Server still runs old image → Run docker compose pull before up -d; verify tag/digest.
  • GHCR denied → Ensure PAT scopes (read:packages on servers, write:packages when pushing) and SSO org authorization.
  • Apple Silicon vs AMD64 → Use buildx multi-arch commands as shown.

Appendix — File & Variable Cheat-Sheet

Files you create

  • Dockerfile, Dockerfile.dev, deploy/nginx.conf, deploy/entrypoint.sh, docker-compose.dev.yml
  • docker-compose.dev.server.yml (Dev VM), docker-compose.prod.yml (Prod VM)
  • deploy/dev.app.env (local + Dev VM), deploy/dev.db.env (Dev VM)
  • deploy/app.env, deploy/db.env, deploy/APP_KEY.secret, deploy/DB_PASSWORD.secret (Prod VM)

Variable glossary

  • OWNER / REPO → your GitHub org/user and repo name (lowercase in GHCR paths).
  • IMGghcr.io/<owner>/<repo>-php.
  • GH_USER → GitHub username or org service account used for registry login.
  • GH_PAT → Personal Access Token; write:packages to push, read:packages to pull.
  • SHAgit rev-parse --short HEAD (handy for image tagging).

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Related Post