Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
54994a7
tests: isolate test database to laravel_test
davidharting Apr 24, 2026
c42364a
trust all proxies for PaaS deployment
davidharting Apr 24, 2026
7bebd91
tests: hardcode DB_USERNAME/PASSWORD in phpunit.xml
davidharting Apr 24, 2026
240074d
exclude /healthz from session/cookie middleware
davidharting Apr 24, 2026
e803f5c
bind Caddy to \$PORT so it works behind Render ingress
davidharting Apr 24, 2026
8c3c3e3
dockerfile: prep image for Render build + deploy
davidharting Apr 24, 2026
a507b8b
remove docker-compose.yml
davidharting Apr 24, 2026
07f03e2
ci: remove GHCR image build workflow
davidharting Apr 24, 2026
6be7b58
ci: run tests on push to main + align test DB name
davidharting Apr 24, 2026
c361158
docs: rewrite CLAUDE.md architecture section for Render
davidharting Apr 24, 2026
590ee7d
docs: add render-migration project runbook
davidharting Apr 24, 2026
e6f2204
add render.yaml blueprint
davidharting Apr 24, 2026
953f113
bump PHP memory_limit to 256M
davidharting Apr 24, 2026
b46bd74
document memory_limit sizing rule in Dockerfile
davidharting Apr 24, 2026
8936ff6
docs: add local Dockerfile smoke-test section to runbook
davidharting Apr 24, 2026
406ac22
render.yaml: wrap services in project + prod environment
davidharting Apr 24, 2026
6d90d27
render.yaml: downgrade Postgres to basic-256mb
davidharting Apr 24, 2026
ba696cb
docs: tick off completed migration phases in runbook
davidharting Apr 24, 2026
03ddf73
fix: run preDeployCommand through a shell script
davidharting Apr 24, 2026
08220d1
fix: strip file capabilities from frankenphp binary for Render compat…
davidharting Apr 24, 2026
17ce78b
render.yaml: set APP_URL to generated onrender.com hostname
davidharting Apr 24, 2026
38a7440
docs: flesh out data migration plan and tick off Phase 3
davidharting Apr 24, 2026
b99612b
render.yaml: add davidharting.com custom domain to web service
davidharting Apr 27, 2026
c80980d
render.yaml: update APP_URL to davidharting.com after DNS cutover
davidharting Apr 27, 2026
838fdbd
feat: replace always-on scheduler worker with Render native cron jobs
davidharting Apr 27, 2026
53317a6
Disable Render subdomain in blueprint
davidharting Apr 27, 2026
d45b6d2
format
davidharting May 8, 2026
0f14d94
docs: remove render-migration project doc
davidharting May 8, 2026
1488884
test: condense health check coverage
davidharting May 8, 2026
71ab035
fix: harden trusted proxy headers on Render
davidharting May 8, 2026
3debb11
docs: simplify Render migration notes
davidharting May 8, 2026
e64fd8f
chore: gate Render autodeploys on CI
davidharting May 8, 2026
0af79eb
chore: load Render secret file at runtime
davidharting May 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ fly.toml
public/hot
docker-compose.yml
docker-compose.dev.yml
render.yaml


# 3. Copy over top-level .gitignore
Expand Down
39 changes: 0 additions & 39 deletions .github/workflows/docker-image.yml

This file was deleted.

10 changes: 8 additions & 2 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
name: Pull Request CI
name: CI

on:
pull_request:
branches:
- main
push:
branches:
- main
workflow_dispatch: # Allows you to run this workflow manually from the Actions tab

jobs:
Expand Down Expand Up @@ -51,6 +54,9 @@ jobs:
run: php artisan key:generate

- name: Execute tests
env:
DB_USERNAME: root
DB_PASSWORD: password
run: php artisan test --compact --do-not-cache-result

services:
Expand All @@ -59,7 +65,7 @@ jobs:
env:
POSTGRES_USER: root
POSTGRES_PASSWORD: password
POSTGRES_DB: laravel
POSTGRES_DB: laravel_test
ports:
- 5432:5432
options: >-
Expand Down
30 changes: 20 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,29 @@ Quick check: if `php artisan test` fails with autoload errors, run the setup.

## Architecture overview

- Cloudflare DNS. Orange-checkmark reverse proxy to my Digital Ocean droplet
- Digital Ocean droplet is running Ubuntu with Docker installed
- I use `docker-compose up` to run the containers for my site. See docker-compose.yml
- I run the Laravel web server Octane and Caddy. See Caddyfile.
The site runs on Render.com from `render.yaml`.

So Cloudflare -> Digital Ocean droplet -> Caddy -> Laravel -> Postgres
```
Internet
-> Render ingress
-> web service: FrankenPHP Octane on $PORT
-> Render Postgres on the private network
```

The containers are:
Supporting services:

- laravel web server
- laravel queue worker
- laravel scheduler
- postgres database
- `davidhartingdotcom-worker`: runs `php artisan queue:work`.
- `davidhartingdotcom-backup-run`: Render cron job that runs database backups.
- `davidhartingdotcom-backup-clean`: Render cron job that prunes old backups.
- Cloudflare DNS points `davidharting.com` at Render. Cloudflare R2 stores public and private objects.

Operational notes:

- Render terminates TLS; the container listens on plain HTTP at `$PORT`.
- `render.yaml` owns service, database, worker, and cron definitions.
- Secrets are managed in Render, not committed to git. Prefer an IaC-friendly path, such as Render secret files, over long-term dashboard-only configuration.
- The web service `preDeployCommand` runs migrations and Telegram webhook registration before new web instances receive traffic.
- Historical note: this previously ran on a Digital Ocean droplet with Docker Compose. See `docs/projects/render-migration.md` for migration history.

## Commands

Expand Down
2 changes: 1 addition & 1 deletion Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
}
}

davidharting.com {
:{$PORT:80} {
log {
level {$CADDY_SERVER_LOG_LEVEL}

Expand Down
56 changes: 41 additions & 15 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ RUN apt-get update \
&& echo "deb [signed-by=/usr/share/keyrings/postgresql-keyring.gpg] https://apt.postgresql.org/pub/repos/apt jammy-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
&& apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y unzip libnss3-tools procps postgresql-client-17 nodejs \
&& rm -rf /var/lib/apt/lists/*
&& apt-get install -y unzip libnss3-tools procps postgresql-client-17 nodejs libcap2-bin \
&& rm -rf /var/lib/apt/lists/* \
&& setcap -r /usr/local/bin/frankenphp

RUN install-php-extensions \
intl \
Expand All @@ -18,25 +19,50 @@ RUN install-php-extensions \
pdo_pgsql \
zip

RUN echo "upload_max_filesize = 25M\npost_max_size = 27M" \
> /usr/local/etc/php/conf.d/uploads.ini


COPY docker-entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# PHP runtime config.
#
# memory_limit: the dunglas/frankenphp base image does not override php.ini, so
# by default we'd inherit PHP's compile-time default of 128M. Bumped to 256M
# to give Octane worker threads headroom for Filament exports and import
# actions.
#
# Sizing rule from FrankenPHP's performance docs:
# num_threads × memory_limit < available_memory
# FrankenPHP spawns 2 × CPU cores worker threads by default. On the Render
# starter plan (0.5 CPU, 512 MB) that's ~1 thread, so 256M fits comfortably
# alongside Caddy + Go runtime overhead. If we move to a larger Render plan,
# revisit this — on standard (1 CPU, 2 GB) the default 2 threads × 256M = 512M
# is still fine, but on pro (2 CPU, 4 GB) with 4 threads we'd use 1 GB for PHP.
#
# The worker / scheduler services run a single PHP process (queue:work /
# schedule:work) so the thread multiplier doesn't apply — 256M is strictly
# safe there.
RUN printf "memory_limit = 256M\nupload_max_filesize = 25M\npost_max_size = 27M\n" \
> /usr/local/etc/php/conf.d/php.ini

RUN mkdir -p /app/public/build

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
COPY . /app

# spatie/laravel-backup expects this to be defined in the post-install script
ENV MAIL_FROM_ADDRESS=hello@davidharting.com
WORKDIR /app

RUN composer install --optimize-autoloader && php artisan optimize:clear
# Dependency layers kept ahead of COPY . so source edits don't bust the cache.
COPY composer.json composer.lock /app/
RUN composer install --no-scripts --no-autoloader --no-dev --prefer-dist

COPY package.json package-lock.json /app/
RUN npm ci --no-audit
RUN npm run build

ENTRYPOINT ["bash", "/entrypoint.sh"]
CMD ["php", "artisan", "octane:frankenphp", "--caddyfile", "Caddyfile", "--https", "--http-redirect"]
# spatie/laravel-backup expects MAIL_FROM_ADDRESS at composer post-install time.
ENV MAIL_FROM_ADDRESS=hello@davidharting.com
# Baked into the JS bundle at Vite build time.
ENV VITE_APP_NAME=davidharting.com

COPY . /app

RUN composer dump-autoload --optimize \
&& composer run-script post-autoload-dump \
&& npm run build \
&& php artisan optimize:clear

CMD ["php", "artisan", "octane:frankenphp", "--caddyfile", "Caddyfile"]
11 changes: 7 additions & 4 deletions app/Http/Middleware/TrustProxies.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ class TrustProxies extends Middleware
/**
* The trusted proxies for this application.
*
* Render forwards public traffic through its ingress before it reaches the
* container port, so trusting the ingress path is expected on this PaaS.
* Do not trust X-Forwarded-Host; Render should preserve the real Host
* header, and accepting a forwarded host is unnecessary spoofing surface.
*
* @var array<int, string>|string|null
*/
protected $proxies;
protected $proxies = '*';
Comment thread
davidharting marked this conversation as resolved.

/**
* The headers that should be used to detect proxies.
Expand All @@ -21,8 +26,6 @@ class TrustProxies extends Middleware
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
Request::HEADER_X_FORWARDED_PROTO;
}
Loading
Loading