-
Notifications
You must be signed in to change notification settings - Fork 1
feat: implement AWS Lightsail deployment stack and add API health che… #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| node_modules | ||
| apps/*/node_modules | ||
| apps/*/dist | ||
| apps/*/coverage | ||
| coverage | ||
| .git | ||
| .gitignore | ||
| .dockerignore | ||
| *.log | ||
| .env | ||
| .env.* | ||
| !.env.example |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| FROM node:20-alpine AS build | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| COPY package.json package-lock.json ./ | ||
| COPY apps/api/package.json apps/api/package.json | ||
| COPY apps/web/package.json apps/web/package.json | ||
|
|
||
| RUN npm ci | ||
|
|
||
| COPY apps/api apps/api | ||
|
|
||
| RUN npm run build:api | ||
| RUN npm prune --omit=dev | ||
|
|
||
| FROM node:20-alpine AS runtime | ||
|
|
||
| ENV NODE_ENV=production | ||
| ENV PORT=8080 | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| COPY --from=build --chown=node:node /app/package.json /app/package-lock.json ./ | ||
| COPY --from=build --chown=node:node /app/node_modules ./node_modules | ||
| COPY --from=build --chown=node:node /app/apps/api/package.json ./apps/api/package.json | ||
| COPY --from=build --chown=node:node /app/apps/api/dist ./apps/api/dist | ||
|
|
||
| RUN chown -R node:node /app | ||
|
|
||
| USER node | ||
|
|
||
| EXPOSE 8080 | ||
|
|
||
| CMD ["node", "apps/api/dist/server.js"] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { getDatabaseSyncOptions, syncDatabaseForStartup } from "../database-startup"; | ||
|
|
||
| describe("database startup sync behavior", () => { | ||
| it("skips Sequelize sync in production by default", async () => { | ||
| const sequelize = { sync: jest.fn() }; | ||
|
|
||
| await syncDatabaseForStartup(sequelize, { | ||
| NODE_ENV: "production", | ||
| }); | ||
|
|
||
| expect(sequelize.sync).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("keeps non-production startup sync enabled by default", async () => { | ||
| const sequelize = { sync: jest.fn().mockResolvedValue(undefined) }; | ||
|
|
||
| await syncDatabaseForStartup(sequelize, { | ||
| NODE_ENV: "development", | ||
| }); | ||
|
|
||
| expect(sequelize.sync).toHaveBeenCalledWith(); | ||
| }); | ||
|
|
||
| it("allows explicit production sync when requested", () => { | ||
| expect(getDatabaseSyncOptions({ | ||
| NODE_ENV: "production", | ||
| DB_SYNC: "true", | ||
| })).toEqual({}); | ||
| }); | ||
|
|
||
| it("preserves explicit alter sync mode", () => { | ||
| expect(getDatabaseSyncOptions({ | ||
| NODE_ENV: "development", | ||
| DB_SYNC_ALTER: "true", | ||
| })).toEqual({ alter: true }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import type { Sequelize } from "sequelize"; | ||
|
|
||
| type SyncOptions = Parameters<Sequelize["sync"]>[0]; | ||
| type StartupEnv = Partial<Pick<NodeJS.ProcessEnv, "NODE_ENV" | "DB_SYNC" | "DB_SYNC_ALTER">>; | ||
|
|
||
| export function getDatabaseSyncOptions(env: StartupEnv = process.env): SyncOptions | undefined | null { | ||
| if (env.DB_SYNC_ALTER === "true") { | ||
| return { alter: true }; | ||
| } | ||
|
|
||
| if (env.DB_SYNC === "true") { | ||
| return {}; | ||
| } | ||
|
|
||
| if (env.NODE_ENV === "production" && env.DB_SYNC !== "true") { | ||
| return null; | ||
| } | ||
|
|
||
| return undefined; | ||
| } | ||
|
|
||
| export async function syncDatabaseForStartup( | ||
| sequelize: Pick<Sequelize, "sync">, | ||
| env: StartupEnv = process.env | ||
| ) { | ||
| const syncOptions = getDatabaseSyncOptions(env); | ||
|
|
||
| if (syncOptions === null) { | ||
| return; | ||
| } | ||
|
|
||
| if (syncOptions === undefined) { | ||
| await sequelize.sync(); | ||
| return; | ||
| } | ||
|
|
||
| await sequelize.sync(syncOptions); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import express from "express"; | ||
| import request from "supertest"; | ||
|
|
||
| import healthRoutes from "../health.routes"; | ||
|
|
||
| describe("Health Routes", () => { | ||
| const app = express(); | ||
|
|
||
| beforeAll(() => { | ||
| app.use(healthRoutes); | ||
| }); | ||
|
|
||
| it("returns a simple healthy response for load balancers", async () => { | ||
| const response = await request(app).get("/healthz"); | ||
|
|
||
| expect(response.status).toBe(200); | ||
| expect(response.body).toEqual({ status: "ok" }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { Router } from "express"; | ||
|
|
||
| const router = Router(); | ||
|
|
||
| router.get("/healthz", (_req, res) => { | ||
| res.status(200).json({ status: "ok" }); | ||
| }); | ||
|
|
||
| export default router; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| APP_DOMAIN=example.com | ||
| WEB_ROOT=/srv/nail-star/web | ||
| API_IMAGE=nail-star-api:latest |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| {$APP_DOMAIN} { | ||
| encode zstd gzip | ||
|
|
||
| handle /api/* { | ||
| uri strip_prefix /api | ||
| reverse_proxy api:8080 | ||
| } | ||
|
|
||
| handle /healthz { | ||
| reverse_proxy api:8080 | ||
| } | ||
|
|
||
| root * /srv/nail-star/web | ||
| try_files {path} /index.html | ||
| file_server | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| DB_HOST=your-lightsail-postgres-endpoint | ||
| DB_PORT=5432 | ||
| DB_USER=postgres | ||
| DB_PASSWORD=replace-me | ||
| DB_NAME=nail_star | ||
| JWT_SECRET=replace-me-with-a-long-random-secret | ||
| CORS_ORIGINS=https://example.com | ||
| COOKIE_SECURE=true | ||
| COOKIE_SAME_SITE=lax | ||
| MAIL_USER=your-ses-smtp-user | ||
| MAIL_PASS=your-ses-smtp-password |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| services: | ||
| api: | ||
| build: | ||
| context: ../.. | ||
| dockerfile: apps/api/Dockerfile | ||
| image: ${API_IMAGE:-nail-star-api:latest} | ||
| restart: unless-stopped | ||
| env_file: | ||
| - ./api.env | ||
| environment: | ||
| NODE_ENV: production | ||
| PORT: 8080 | ||
| expose: | ||
| - "8080" | ||
| healthcheck: | ||
| test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/healthz >/dev/null 2>&1 || exit 1"] | ||
| interval: 30s | ||
| timeout: 5s | ||
| retries: 3 | ||
| start_period: 20s | ||
|
|
||
| caddy: | ||
| image: caddy:2-alpine | ||
| restart: unless-stopped | ||
| depends_on: | ||
| api: | ||
| condition: service_healthy | ||
| ports: | ||
| - "80:80" | ||
| - "443:443" | ||
| environment: | ||
| APP_DOMAIN: ${APP_DOMAIN} | ||
| volumes: | ||
| - ./Caddyfile:/etc/caddy/Caddyfile:ro | ||
| - ${WEB_ROOT:-/srv/nail-star/web}:/srv/nail-star/web:ro | ||
| - caddy_data:/data | ||
| - caddy_config:/config | ||
|
|
||
| volumes: | ||
| caddy_data: | ||
| caddy_config: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| # Lightsail Deployment | ||
|
|
||
| This deployment keeps monthly cost low while separating the database from the app server. | ||
|
|
||
| ## AWS Shape | ||
|
|
||
| - Lightsail Linux instance for Caddy, static web files, and the API container. | ||
| - Lightsail managed PostgreSQL for application data. | ||
| - Optional S3 or Lightsail object storage for off-instance database dumps. | ||
|
|
||
| Expected baseline cost is roughly $30-$35/month before optional snapshot and backup storage growth. | ||
|
|
||
| ## DNS | ||
|
|
||
| Point the production domain at the Lightsail instance static IP. Caddy handles TLS automatically. | ||
|
|
||
| ## First Deploy | ||
|
|
||
| Create a Lightsail PostgreSQL database, then create the deployment env files on the server: | ||
|
|
||
| ```bash | ||
| cp deploy/lightsail/.env.example deploy/lightsail/.env | ||
| cp deploy/lightsail/api.env.example deploy/lightsail/api.env | ||
| ``` | ||
|
|
||
| Update both files with the production domain, database endpoint, credentials, JWT secret, and SMTP credentials. | ||
|
|
||
| Build and publish the frontend files: | ||
|
|
||
| ```bash | ||
| npm ci | ||
| VITE_BACKEND_URL=https://example.com/api npm run build:web | ||
| sudo mkdir -p /srv/nail-star/web | ||
| sudo rsync -a --delete apps/web/dist/ /srv/nail-star/web/ | ||
| ``` | ||
|
|
||
| Start the API and reverse proxy: | ||
|
|
||
| ```bash | ||
| docker compose -f deploy/lightsail/compose.yml --env-file deploy/lightsail/.env up -d --build | ||
| ``` | ||
|
|
||
| Verify: | ||
|
|
||
| ```bash | ||
| curl https://example.com/healthz | ||
| curl https://example.com/api/healthz | ||
| ``` | ||
|
|
||
| Both should return: | ||
|
|
||
| ```json | ||
| {"status":"ok"} | ||
| ``` | ||
|
|
||
| ## Updating | ||
|
|
||
| Pull the new code and repeat: | ||
|
|
||
| ```bash | ||
| VITE_BACKEND_URL=https://example.com/api npm run build:web | ||
| sudo rsync -a --delete apps/web/dist/ /srv/nail-star/web/ | ||
| docker compose -f deploy/lightsail/compose.yml --env-file deploy/lightsail/.env up -d --build | ||
| ``` | ||
|
|
||
| ## Database Startup | ||
|
|
||
| The API does not run Sequelize sync automatically in production. Run schema migrations or controlled setup before restarting production services. | ||
|
|
||
| For an explicit one-time production sync, set this in `deploy/lightsail/api.env` before starting the API: | ||
|
|
||
| ```bash | ||
| DB_SYNC=true | ||
| ``` | ||
|
|
||
| Remove it after the sync has run. Use `DB_SYNC_ALTER=true` only for controlled maintenance, not normal production startup. | ||
|
|
||
| ## Backup Minimum | ||
|
|
||
| Keep managed database backups enabled. Also schedule an off-instance dump, for example: | ||
|
|
||
| ```bash | ||
| PGPASSWORD="$DB_PASSWORD" pg_dump -h "$DB_HOST" -U "$DB_USER" -d "$DB_NAME" -Fc > nail-star-$(date +%F).dump | ||
| ``` | ||
|
|
||
| Store dumps outside the app server, such as S3, Lightsail object storage, or another backup target. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.