Skip to content

Commit 706a412

Browse files
authored
Merge pull request #52 from Ardenexal/feat/deploy-demo-server
feat: add deployment workflow and documentation for demo app
2 parents c343074 + a5a599d commit 706a412

3 files changed

Lines changed: 197 additions & 0 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: Deploy Demo
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
paths:
7+
- 'demo/**'
8+
- '.github/workflows/deploy.yml'
9+
# Note: tags and paths are both required when used together.
10+
# Tag pushes will only trigger if the tagged commit includes demo/ changes.
11+
# If you create a tag on a commit that doesn't touch demo/, push an empty
12+
# commit to demo/ (e.g. touch demo/.deploy) and retag.
13+
tags: [ '*.*' ]
14+
15+
jobs:
16+
build-and-deploy:
17+
runs-on: ubuntu-latest
18+
permissions:
19+
contents: read
20+
packages: write
21+
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- name: Login to GHCR
26+
uses: docker/login-action@v3
27+
with:
28+
registry: ghcr.io
29+
username: ${{ github.actor }}
30+
password: ${{ secrets.GITHUB_TOKEN }}
31+
32+
- name: Docker metadata
33+
id: meta
34+
uses: docker/metadata-action@v5
35+
with:
36+
images: ghcr.io/ardenexal/php-fhir-tools
37+
tags: |
38+
type=raw,value=latest
39+
type=semver,pattern={{version}}
40+
41+
- name: Build and push Docker image
42+
uses: docker/build-push-action@v6
43+
with:
44+
context: .
45+
file: demo/Dockerfile
46+
push: true
47+
tags: ${{ steps.meta.outputs.tags }}
48+
labels: ${{ steps.meta.outputs.labels }}
49+
50+
- name: Join Tailscale network
51+
uses: tailscale/github-action@v2
52+
with:
53+
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
54+
oauth-secret: ${{ secrets.TS_OAUTH_CLIENT_SECRET }}
55+
tags: tag:ci
56+
57+
- name: Deploy to server
58+
uses: appleboy/ssh-action@v1
59+
with:
60+
host: ${{ secrets.SERVER_TAILSCALE_IP }}
61+
username: ${{ secrets.SERVER_USER }}
62+
key: ${{ secrets.SERVER_SSH_KEY }}
63+
script: |
64+
echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
65+
docker compose -f ${{ secrets.DEPLOY_COMPOSE_PATH }} pull
66+
docker compose -f ${{ secrets.DEPLOY_COMPOSE_PATH }} up -d
67+
docker image prune -f

demo/DEPLOYMENT.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Demo App Deployment
2+
3+
## Architecture
4+
5+
```
6+
push to main (demo/ changes) ─┐
7+
new version tag ─┤─▶ GitHub Actions ─▶ build image ─▶ push to GHCR
8+
9+
▼ (via Tailscale)
10+
SSH into server ─▶ docker compose pull + up -d
11+
```
12+
13+
The container runs [FrankenPHP](https://frankenphp.dev/) in worker mode, serving HTTP on port 80. TLS termination is handled externally by your reverse proxy.
14+
15+
## Trigger Conditions
16+
17+
| Event | Condition | Result |
18+
|-------|-----------|--------|
19+
| Push to `main` | Files changed under `demo/` or `.github/workflows/deploy.yml` | Build + deploy `:latest` |
20+
| Version tag (e.g. `0.3`) | Tag commit must include a `demo/` file change (GitHub paths filter applies to tags too) | Build + deploy `:0.3` and `:latest` |
21+
22+
> **Tagging tip**: If your release commit doesn't touch `demo/`, create the tag on a commit that does — for example, update `demo/DEPLOYMENT.md` or bump a version comment in any demo file.
23+
24+
## Required GitHub Secrets
25+
26+
Configure these under **Settings → Secrets and variables → Actions** in the repository:
27+
28+
| Secret | Description |
29+
|--------|-------------|
30+
| `TS_OAUTH_CLIENT_ID` | Tailscale OAuth client ID for the CI node |
31+
| `TS_OAUTH_CLIENT_SECRET` | Tailscale OAuth client secret |
32+
| `SERVER_TAILSCALE_IP` | Tailscale IP address of the deploy target |
33+
| `SERVER_USER` | SSH username on the deploy target |
34+
| `SERVER_SSH_KEY` | SSH private key (PEM) for the deploy target |
35+
| `GHCR_TOKEN` | GitHub PAT with `read:packages` scope, used to pull the image on the target |
36+
| `DEPLOY_COMPOSE_PATH` | Absolute path to the `docker-compose.yml` file on the deploy target |
37+
38+
`GITHUB_TOKEN` (auto-provided by Actions) is used for pushing to GHCR from CI.
39+
40+
## Target Host Setup
41+
42+
The deploy target needs Docker installed and a `docker-compose.yml` in the path configured by `DEPLOY_COMPOSE_PATH`:
43+
44+
```yaml
45+
services:
46+
demo:
47+
image: ghcr.io/ardenexal/php-fhir-tools:latest
48+
restart: unless-stopped
49+
ports:
50+
- "8080:80"
51+
env_file:
52+
- .env
53+
```
54+
55+
The `.env` file in the same directory must set at minimum:
56+
57+
```env
58+
APP_SECRET=<random 32-char hex string>
59+
```
60+
61+
Generate a value with: `openssl rand -hex 32`
62+
63+
## Reverse Proxy
64+
65+
The container serves plain HTTP on port 80 (`SERVER_NAME="http://:80"`). Configure your reverse proxy to forward to the host port you mapped (e.g. `8080`). Example Caddy block:
66+
67+
```
68+
your-domain.example.com {
69+
reverse_proxy localhost:8080
70+
}
71+
```
72+
73+
## Tailscale ACL
74+
75+
The GitHub Actions runner joins your tailnet using an OAuth client. Before this will work:
76+
77+
1. Create an OAuth client at [tailscale.com/settings/oauth-clients](https://login.tailscale.com/admin/settings/oauthclients) with the `devices` write scope
78+
2. Ensure `tag:ci` exists in your ACL policy (Tailscale requires OAuth clients to specify an existing tag)
79+
3. Grant `tag:ci` SSH access to the deploy target in your ACL policy
80+
81+
Example ACL entry:
82+
83+
```json
84+
{
85+
"action": "accept",
86+
"src": ["tag:ci"],
87+
"dst": ["tag:server:22"]
88+
}
89+
```
90+
91+
## Building Locally
92+
93+
From the repository root:
94+
95+
```bash
96+
docker build -f demo/Dockerfile -t fhir-demo .
97+
docker run -p 8080:80 -e APP_SECRET=$(openssl rand -hex 32) fhir-demo
98+
```
99+
100+
Then open `http://localhost:8080`.

demo/Dockerfile

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
FROM dunglas/frankenphp:1-php8.5-bookworm
2+
3+
# Install extensions not already present in the base image
4+
# (dom, xml, xmlwriter, xmlreader, simplexml, mbstring, ctype, iconv are pre-installed)
5+
RUN install-php-extensions bcmath intl opcache
6+
7+
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
8+
9+
WORKDIR /app
10+
11+
# Install dependencies first for better layer caching
12+
# (invalidated only when composer.json or composer.lock change)
13+
COPY demo/composer.json demo/composer.lock ./
14+
RUN composer install --no-dev --optimize-autoloader --no-interaction --no-scripts
15+
16+
# Copy monorepo source required by ../src/* PSR-4 autoload paths in demo/composer.json
17+
COPY src/ /src/
18+
19+
# Copy rest of demo application
20+
COPY demo/ .
21+
22+
# Regenerate classmap now that all source directories are present,
23+
# then run post-install scripts (cache:clear, assets:install, importmap:install)
24+
RUN composer dump-autoload --optimize --no-dev \
25+
&& composer run-script post-install-cmd --no-interaction || true
26+
27+
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
28+
ENV SERVER_NAME="http://:80"
29+
30+
EXPOSE 80

0 commit comments

Comments
 (0)