Skip to content

Commit ac85b84

Browse files
committed
Initial structure
0 parents  commit ac85b84

59 files changed

Lines changed: 17341 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.test

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Underlay
2+
NODE_ENV=development
3+
DATABASE_URL=postgresql://underlay:underlay@localhost:5432/underlay
4+
SESSION_SECRET=dev-secret-change-me
5+
APP_URL=http://localhost:4321
6+
API_PORT=3000
7+
8+
# S3 (MinIO in dev)
9+
S3_BUCKET=underlay
10+
S3_REGION=us-east-1
11+
S3_ENDPOINT=http://localhost:9000
12+
S3_ACCESS_KEY=minioadmin
13+
S3_SECRET_KEY=minioadmin
14+
15+
# Backup
16+
BACKUP_S3_PREFIX=backups/

.github/workflows/deploy.yml

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
name: Deploy
2+
3+
concurrency:
4+
group: deploy-${{ github.ref }}
5+
cancel-in-progress: true
6+
7+
on:
8+
push:
9+
branches: [ main ]
10+
workflow_dispatch:
11+
12+
env:
13+
REGISTRY: ghcr.io
14+
IMAGE_NAME: ${{ github.repository }}
15+
16+
permissions:
17+
contents: read
18+
packages: write
19+
20+
jobs:
21+
build:
22+
name: Build & Push Image
23+
runs-on: ubuntu-latest
24+
outputs:
25+
image_tag: ${{ steps.meta.outputs.version }}
26+
steps:
27+
- uses: actions/checkout@v4
28+
29+
- uses: docker/setup-buildx-action@v3
30+
31+
- uses: docker/login-action@v3
32+
with:
33+
registry: ${{ env.REGISTRY }}
34+
username: ${{ github.actor }}
35+
password: ${{ secrets.GITHUB_TOKEN }}
36+
37+
- id: meta
38+
uses: docker/metadata-action@v5
39+
with:
40+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
41+
tags: |
42+
type=sha,prefix=
43+
type=raw,value=latest,enable={{is_default_branch}}
44+
45+
- uses: docker/build-push-action@v5
46+
with:
47+
context: .
48+
target: production
49+
push: true
50+
tags: ${{ steps.meta.outputs.tags }}
51+
labels: ${{ steps.meta.outputs.labels }}
52+
cache-from: type=gha
53+
cache-to: type=gha,mode=max
54+
55+
deploy:
56+
name: Deploy
57+
needs: build
58+
runs-on: ubuntu-latest
59+
steps:
60+
- name: Start SSH agent
61+
uses: webfactory/ssh-agent@v0.9.0
62+
with:
63+
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
64+
65+
- name: Add known hosts
66+
run: |
67+
mkdir -p ~/.ssh
68+
ssh-keyscan -H "${{ secrets.SSH_HOST }}" >> ~/.ssh/known_hosts
69+
70+
- name: Deploy over SSH
71+
env:
72+
SSH_USER: ${{ secrets.SSH_USER }}
73+
SSH_HOST: ${{ secrets.SSH_HOST }}
74+
REPO: ${{ github.repository }}
75+
BRANCH: ${{ github.ref_name }}
76+
GHCR_USER: ${{ secrets.GHCR_USER }}
77+
GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }}
78+
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
79+
IMAGE_TAG: ${{ needs.build.outputs.image_tag }}
80+
ENV_FILE: ${{ vars.ENV_FILE || '.env.enc' }}
81+
DEPLOY_PATH: ${{ vars.DEPLOY_PATH || '/opt/underlay' }}
82+
run: |
83+
ssh "${SSH_USER}@${SSH_HOST}" \
84+
"env GHCR_USER='${GHCR_USER}' GHCR_TOKEN='${GHCR_TOKEN}' IMAGE='${IMAGE}' IMAGE_TAG='${IMAGE_TAG}' ENV_FILE='${ENV_FILE}' bash -s -- '${REPO}' '${BRANCH}' '${DEPLOY_PATH}'" <<'EOS'
85+
set -euo pipefail
86+
87+
REPO="${1:?missing repo}"
88+
BRANCH="${2:-main}"
89+
APP_DIR="${3:-/opt/underlay}"
90+
91+
: "${IMAGE:?missing IMAGE}"
92+
: "${IMAGE_TAG:?missing IMAGE_TAG}"
93+
: "${GHCR_USER:?missing GHCR_USER}"
94+
: "${GHCR_TOKEN:?missing GHCR_TOKEN}"
95+
96+
REPO_SSH="git@github.com:${REPO}.git"
97+
98+
ssh-keyscan -H github.com >> ~/.ssh/known_hosts 2>/dev/null
99+
chmod 600 ~/.ssh/known_hosts
100+
101+
if [[ ! -d "${APP_DIR}/.git" ]]; then
102+
sudo mkdir -p "${APP_DIR}"
103+
sudo chown -R "$USER:$USER" "${APP_DIR}"
104+
git clone --branch "${BRANCH}" "${REPO_SSH}" "${APP_DIR}"
105+
fi
106+
107+
cd "${APP_DIR}"
108+
git fetch --prune origin
109+
git checkout "${BRANCH}"
110+
git pull origin "${BRANCH}"
111+
112+
: "${ENV_FILE:?missing ENV_FILE}"
113+
sops -d --input-type dotenv --output-type dotenv "$ENV_FILE" > .env
114+
115+
echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GHCR_USER" --password-stdin
116+
117+
export IMAGE IMAGE_TAG
118+
docker compose pull
119+
docker compose up -d --remove-orphans
120+
121+
wait_healthy() {
122+
local timeout="${1:-120}"
123+
local end=$((SECONDS + timeout))
124+
while (( SECONDS < end )); do
125+
if ! docker compose ps | grep -qiE 'starting|unhealthy|restarting'; then
126+
echo "All services running"
127+
docker compose ps
128+
return 0
129+
fi
130+
echo "Waiting for services..."
131+
sleep 5
132+
done
133+
echo "Rollout timeout"
134+
docker compose ps
135+
return 1
136+
}
137+
wait_healthy 120
138+
139+
docker image prune -a --filter "until=72h" -f
140+
141+
echo "Deployed to $(hostname)"
142+
EOS

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
node_modules/
2+
dist/
3+
.env
4+
.env.dev
5+
data/
6+
planning/
7+
.astro/
8+
*.db
9+
*.db-journal
10+
*.db-wal

.sops.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# SOPS configuration — specifies which age public keys can decrypt.
2+
# Generate a keypair: age-keygen -o key.txt
3+
# Add your public keys below.
4+
5+
creation_rules:
6+
- path_regex: \.env(\.dev)?(\.enc)?$
7+
age: []
8+
# Add age public keys here:
9+
# - age1your_first_key_here
10+
# - age1your_second_key_here

Dockerfile

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# --- Build stage ---
2+
FROM node:24-alpine AS build
3+
RUN apk add --no-cache python3 make g++
4+
WORKDIR /app
5+
COPY package.json package-lock.json* ./
6+
RUN npm install
7+
COPY . .
8+
RUN npm run build
9+
10+
# --- Dev stage (used by docker-compose.dev.yml) ---
11+
FROM node:24-alpine AS dev
12+
RUN apk add --no-cache python3 make g++
13+
WORKDIR /app
14+
COPY package.json package-lock.json* ./
15+
RUN npm install
16+
COPY . .
17+
CMD ["sh", "-c", "npm run db:migrate && npm run db:seed && npm run dev:server"]
18+
19+
# --- Production stage ---
20+
FROM node:24-alpine AS production
21+
WORKDIR /app
22+
RUN apk add --no-cache curl python3 make g++
23+
COPY package.json package-lock.json* ./
24+
RUN npm install --omit=dev && apk del python3 make g++
25+
COPY --from=build /app/dist ./dist
26+
COPY --from=build /app/public ./public
27+
COPY --from=build /app/src/api ./src/api
28+
COPY --from=build /app/src/db ./src/db
29+
COPY --from=build /app/src/lib ./src/lib
30+
COPY --from=build /app/tools ./tools
31+
COPY --from=build /app/tsconfig.json ./tsconfig.json
32+
COPY --from=build /app/drizzle.config.ts ./drizzle.config.ts
33+
34+
ENV NODE_ENV=production
35+
ENV HOST=0.0.0.0
36+
EXPOSE 4321 3000
37+
38+
# Run migrations then start both Astro SSR and Fastify API
39+
CMD ["sh", "-c", "npx tsx src/db/migrate.ts && node ./dist/server/entry.mjs & npx tsx src/api/server.ts & wait"]

0 commit comments

Comments
 (0)