Merge pull request #65 from trypostit/feat/docker-distribution

feat(docker): publish multi-arch image to GHCR + self-host compose
This commit is contained in:
Paulo Castellano 2026-05-27 16:02:32 -03:00 committed by GitHub
commit 5de993bc6c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 324 additions and 5 deletions

130
.github/workflows/release-docker.yml vendored Normal file
View file

@ -0,0 +1,130 @@
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 }}
build-args: |
VITE_REVERB_APP_KEY=trypost-reverb-key
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 }}

6
Caddyfile Normal file
View file

@ -0,0 +1,6 @@
# Caddy serves TryPost on your domain with automatic HTTPS (Let's Encrypt).
# APP_DOMAIN is provided by the caddy service in compose.prod.yaml.
# reverse_proxy transparently passes through WebSocket upgrades (Reverb on /app).
{$APP_DOMAIN} {
reverse_proxy app:80
}

176
compose.prod.yaml Normal file
View file

@ -0,0 +1,176 @@
# 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) =====
# The published image bakes the client at localhost:8080. For a custom
# domain, rebuild the image with --build-arg VITE_REVERB_HOST=<domain>
# VITE_REVERB_PORT=443 VITE_REVERB_SCHEME=https.
REVERB_APP_ID: "1001"
REVERB_APP_KEY: trypost-reverb-key # must match the key baked into the published image
REVERB_APP_SECRET: change-me-reverb-secret # <- change me
REVERB_HOST: localhost
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
# Optional reverse proxy with automatic HTTPS (Let's Encrypt).
# To serve on a domain:
# 1. Point the domain's DNS at this host.
# 2. Set APP_DOMAIN below and APP_URL above to https://<that domain>.
# 3. Start with the proxy profile:
# docker compose -f compose.prod.yaml --profile proxy up -d
# Without the profile, the app is served directly on http://localhost:8000.
caddy:
image: caddy:2-alpine
container_name: trypost-caddy
restart: unless-stopped
profiles: [proxy]
environment:
APP_DOMAIN: post.example.com # <- your domain
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
depends_on:
- app
volumes:
pgdata:
redisdata:
storage:
caddy-data:
caddy-config:

View file

@ -1,7 +1,7 @@
import '../css/app.css';
import './echo';
import { createInertiaApp, router } from '@inertiajs/vue3';
import { configureEcho } from '@laravel/echo-vue';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { i18nVue } from 'laravel-vue-i18n';
import type { DefineComponent } from 'vue';
@ -12,10 +12,6 @@ import dayjs from './dayjs';
import { capturePageview, initializePostHog, syncPostHogContext } from './posthog';
import type { Auth } from './types';
configureEcho({
broadcaster: 'reverb',
});
const appName = import.meta.env.VITE_APP_NAME || 'TryPost.it';
createInertiaApp({

11
resources/js/echo.ts Normal file
View file

@ -0,0 +1,11 @@
import { configureEcho } from '@laravel/echo-vue';
configureEcho({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});