feat(docker): publish multi-arch image to GHCR and add self-host compose
Add a release workflow that builds docker/Dockerfile (production target) for linux/amd64 and linux/arm64 on native runners and publishes a single multi-arch manifest to ghcr.io/trypostit/trypost on every v*.*.* tag, tagged with semver (1.2.3, 1.2, 1) and latest. Add compose.prod.yaml: a self-host stack that pulls the published image alongside Postgres and Redis with persistent volumes. Config is inline with sectioned comments, including a commented S3/R2 block (storage already uses the default disk, so switching is config-only). No application code is changed.
This commit is contained in:
parent
0602a02706
commit
5b9226569f
2 changed files with 275 additions and 0 deletions
128
.github/workflows/release-docker.yml
vendored
Normal file
128
.github/workflows/release-docker.yml
vendored
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
name: Publish Docker image
|
||||||
|
|
||||||
|
# Builds the production image and publishes it to GitHub Container Registry
|
||||||
|
# (GHCR) on every version tag. amd64 and arm64 are built on native runners,
|
||||||
|
# then stitched into one multi-arch manifest — no QEMU emulation.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*.*.*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build (${{ matrix.platform }})
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- platform: linux/amd64
|
||||||
|
runner: ubuntu-latest
|
||||||
|
- platform: linux/arm64
|
||||||
|
runner: ubuntu-24.04-arm
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Prepare platform slug
|
||||||
|
run: echo "PLATFORM_PAIR=${platform//\//-}" >> "$GITHUB_ENV"
|
||||||
|
env:
|
||||||
|
platform: ${{ matrix.platform }}
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Docker metadata (labels)
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push by digest
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker/Dockerfile
|
||||||
|
target: production
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha,scope=${{ env.PLATFORM_PAIR }}
|
||||||
|
cache-to: type=gha,mode=max,scope=${{ env.PLATFORM_PAIR }}
|
||||||
|
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||||
|
|
||||||
|
- name: Export digest
|
||||||
|
run: |
|
||||||
|
mkdir -p "${{ runner.temp }}/digests"
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
|
- name: Upload digest
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: digests-${{ env.PLATFORM_PAIR }}
|
||||||
|
path: ${{ runner.temp }}/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
merge:
|
||||||
|
name: Merge multi-arch manifest
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build]
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Download digests
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: ${{ runner.temp }}/digests
|
||||||
|
pattern: digests-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Docker metadata (tags)
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
type=raw,value=latest
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Create and push manifest list
|
||||||
|
working-directory: ${{ runner.temp }}/digests
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create \
|
||||||
|
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
|
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||||
|
|
||||||
|
- name: Inspect published image
|
||||||
|
run: docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||||
147
compose.prod.yaml
Normal file
147
compose.prod.yaml
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
# TryPost — self-hosted production stack.
|
||||||
|
#
|
||||||
|
# 1. Generate an app key: docker compose -f compose.prod.yaml run --rm app php artisan key:generate --show
|
||||||
|
# Paste the value into APP_KEY below.
|
||||||
|
# 2. Edit APP_URL and the passwords marked "change me".
|
||||||
|
# 3. Start: docker compose -f compose.prod.yaml up -d
|
||||||
|
#
|
||||||
|
# This pulls the published image — no local build. Postgres, Redis, the queue
|
||||||
|
# workers, the scheduler and the WebSocket server all run for you.
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: ghcr.io/trypostit/trypost:latest
|
||||||
|
container_name: trypost
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
# ===== Required =====
|
||||||
|
APP_NAME: TryPost
|
||||||
|
APP_ENV: production
|
||||||
|
APP_DEBUG: "false"
|
||||||
|
APP_KEY: "" # <- run key:generate (see header) and paste here
|
||||||
|
APP_URL: http://localhost:8000 # <- your public URL, e.g. https://post.yourdomain.com
|
||||||
|
SELF_HOSTED: "true"
|
||||||
|
TRYPOST_TARGET: production
|
||||||
|
|
||||||
|
# ===== Database (bundled postgres service below) =====
|
||||||
|
DB_CONNECTION: pgsql
|
||||||
|
DB_HOST: pgsql
|
||||||
|
DB_PORT: "5432"
|
||||||
|
DB_DATABASE: trypost
|
||||||
|
DB_USERNAME: trypost
|
||||||
|
DB_PASSWORD: trypost-password # <- change me (must match POSTGRES_PASSWORD below)
|
||||||
|
|
||||||
|
# ===== Redis / queue / cache / broadcasting =====
|
||||||
|
REDIS_HOST: redis
|
||||||
|
REDIS_PORT: "6379"
|
||||||
|
QUEUE_CONNECTION: redis
|
||||||
|
CACHE_STORE: redis
|
||||||
|
SESSION_DRIVER: database
|
||||||
|
BROADCAST_CONNECTION: reverb
|
||||||
|
|
||||||
|
# ===== WebSockets (Reverb) =====
|
||||||
|
REVERB_APP_ID: "1001"
|
||||||
|
REVERB_APP_KEY: trypost-reverb-key
|
||||||
|
REVERB_APP_SECRET: change-me-reverb-secret # <- change me
|
||||||
|
REVERB_HOST: localhost # <- for a real domain, set to that host (and PORT 443 / SCHEME https)
|
||||||
|
REVERB_PORT: "8080"
|
||||||
|
REVERB_SCHEME: http
|
||||||
|
|
||||||
|
# ===== Storage =====
|
||||||
|
# Default: local disk, persisted in the "storage" volume below.
|
||||||
|
FILESYSTEM_DISK: public
|
||||||
|
#
|
||||||
|
# --- Cloudflare R2: set FILESYSTEM_DISK=r2 and fill these ---
|
||||||
|
# R2_ACCESS_KEY_ID: ""
|
||||||
|
# R2_SECRET_ACCESS_KEY: ""
|
||||||
|
# R2_ENDPOINT: ""
|
||||||
|
# R2_BUCKET: ""
|
||||||
|
# R2_URL: "" # public bucket URL (needed for media to display)
|
||||||
|
#
|
||||||
|
# --- AWS S3: set FILESYSTEM_DISK=s3 and fill these ---
|
||||||
|
# AWS_ACCESS_KEY_ID: ""
|
||||||
|
# AWS_SECRET_ACCESS_KEY: ""
|
||||||
|
# AWS_DEFAULT_REGION: us-east-1
|
||||||
|
# AWS_BUCKET: ""
|
||||||
|
# AWS_URL: ""
|
||||||
|
|
||||||
|
# ===== Mail =====
|
||||||
|
# Defaults to "log" (emails written to the container log). Configure SMTP
|
||||||
|
# for real password-reset / team-invite emails.
|
||||||
|
MAIL_MAILER: log
|
||||||
|
MAIL_FROM_ADDRESS: hello@example.com
|
||||||
|
MAIL_FROM_NAME: TryPost
|
||||||
|
# MAIL_MAILER: smtp
|
||||||
|
# MAIL_HOST: ""
|
||||||
|
# MAIL_PORT: "587"
|
||||||
|
# MAIL_USERNAME: ""
|
||||||
|
# MAIL_PASSWORD: ""
|
||||||
|
# MAIL_SCHEME: tls
|
||||||
|
|
||||||
|
# ===== Social platforms (fill in when you connect each network) =====
|
||||||
|
# Redirect URI in each portal: ${APP_URL}/accounts/<platform>/callback
|
||||||
|
# LINKEDIN_CLIENT_ID: ""
|
||||||
|
# LINKEDIN_CLIENT_SECRET: ""
|
||||||
|
# X_CLIENT_ID: ""
|
||||||
|
# X_CLIENT_SECRET: ""
|
||||||
|
# FACEBOOK_CLIENT_ID: ""
|
||||||
|
# FACEBOOK_CLIENT_SECRET: ""
|
||||||
|
# INSTAGRAM_CLIENT_ID: ""
|
||||||
|
# INSTAGRAM_CLIENT_SECRET: ""
|
||||||
|
# THREADS_CLIENT_ID: ""
|
||||||
|
# THREADS_CLIENT_SECRET: ""
|
||||||
|
# TIKTOK_CLIENT_ID: ""
|
||||||
|
# TIKTOK_CLIENT_SECRET: ""
|
||||||
|
# PINTEREST_CLIENT_ID: ""
|
||||||
|
# PINTEREST_CLIENT_SECRET: ""
|
||||||
|
# GOOGLE_CLIENT_ID: "" # YouTube + Google login
|
||||||
|
# GOOGLE_CLIENT_SECRET: ""
|
||||||
|
|
||||||
|
# ===== AI (optional — leave blank to disable AI features) =====
|
||||||
|
# OPENAI_API_KEY: ""
|
||||||
|
# ANTHROPIC_API_KEY: ""
|
||||||
|
# GEMINI_API_KEY: ""
|
||||||
|
ports:
|
||||||
|
- "8000:80" # app (nginx)
|
||||||
|
- "8080:8080" # Reverb WebSocket
|
||||||
|
volumes:
|
||||||
|
- storage:/var/www/html/storage/app
|
||||||
|
depends_on:
|
||||||
|
pgsql:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
pgsql:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: trypost-pgsql
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: trypost
|
||||||
|
POSTGRES_USER: trypost
|
||||||
|
POSTGRES_PASSWORD: trypost-password # <- must match DB_PASSWORD above
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'pg_isready -U trypost -d trypost']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: trypost-redis
|
||||||
|
restart: unless-stopped
|
||||||
|
command: redis-server --appendonly yes
|
||||||
|
volumes:
|
||||||
|
- redisdata:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'redis-cli', 'ping']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
|
redisdata:
|
||||||
|
storage:
|
||||||
Loading…
Reference in a new issue