Add Docker Build Support

Blog posts with 'draft: true' in the frontmatter are excluded from the production artifact
--no-cache docker builds ensure fresh database build each time. Caching isn't needed do to small size anyway
This commit is contained in:
2026-04-15 12:25:20 -07:00
parent 603687c46b
commit b96f7ed3f0
6 changed files with 165 additions and 4 deletions

15
.dockerignore Normal file
View File

@@ -0,0 +1,15 @@
node_modules
.git
.gitignore
README.md
LICENSE
.vscode
.idea
.env
.editorconfig
coverage*
dist
*.log
blog.sqlite
Dockerfile*
docker-compose*

4
.gitignore vendored
View File

@@ -1,6 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
blog.sqlite
/node_modules
/.pnp
.pnp.js
@@ -25,6 +26,7 @@ yarn-debug.log*
yarn-error.log*
# local env files
.env
.env.local
.env.development.local
.env.test.local
@@ -39,4 +41,4 @@ yarn-error.log*
**/*.tgz
**/*.log
package-lock.json
**/*.bun
**/*.bun

51
Dockerfile Normal file
View File

@@ -0,0 +1,51 @@
# Build stage
FROM oven/bun:1 AS builder
WORKDIR /usr/src/app
# Install dependencies
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
# Copy source code and content
COPY . .
# Copy database initialization script
COPY init-db.ts ./
# Initialize database using init-db.ts script
# This script handles draft-aware post verification and stability detection
ENV NODE_ENV=build
RUN bun run init-db.ts
# Final production stage
FROM oven/bun:1
WORKDIR /usr/src/app
# Copy dependencies
COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app/package.json ./
# Copy source code (for runtime transpilation)
COPY --from=builder /usr/src/app/src ./src
COPY --from=builder /usr/src/app/content ./content
COPY --from=builder /usr/src/app/bun_plugins ./bun_plugins
COPY --from=builder /usr/src/app/index.tsx .
COPY --from=builder /usr/src/app/tsconfig.json .
# Copy the initialized database for efficient layer caching
COPY --from=builder /usr/src/app/blog.sqlite ./
# Fix ownership for bun user (needed for runtime operations)
RUN chown -R bun:bun /usr/src/app
# Switch to bun user
USER bun
# Expose the default port
EXPOSE 3000/tcp
# Production environment (onStartup-post-importer won't run since DB exists)
ENV NODE_ENV=production
# Run the application directly (Bun handles TSX transpilation at runtime)
ENTRYPOINT [ "bun", "run", "index.tsx" ]

View File

@@ -14,6 +14,11 @@ import { dbConnection } from "../src/db";
const {data, content } = matter(await Bun.file(`./content/${file}`).text());
const route = `/${file.replace(/\.md$/, "")}`;
// Omit draft blog posts from database initialization in production
if (process.env.NODE_ENV === 'build' && data.draft) {
continue;
}
// Remove the title from content if it matches the frontmatter title to avoid duplicate H1s
let processedContent = content;
if (data.title) {

85
init-db.ts Normal file
View File

@@ -0,0 +1,85 @@
const { Database } = require("bun:sqlite");
async function waitForDatabase() {
const serverProcess = Bun.spawn(["bun", "run", "index.tsx"], {
env: { ...process.env, NODE_ENV: "development" },
stdout: "pipe",
stderr: "pipe"
});
// Count expected markdown files - this gives us total potential posts
const files = [];
for await (const file of new Bun.Glob("**/*.md").scan("./content")) {
files.push(file);
}
const totalMarkdownFiles = files.length;
console.log(`Found ${totalMarkdownFiles} markdown files to process`);
const maxWait = 30000; // 30 seconds total timeout
const checkInterval = 100; // Check every 100ms
let waited = 0;
let stableCount = 0;
let previousCount = -1;
while (waited < maxWait) {
if (await Bun.file("blog.sqlite").exists()) {
try {
const db = new Database("blog.sqlite", { readonly: true });
// Count only published posts (routes ending without .md)
// This matches the logic used in the actual application queries
const result = db.query("SELECT COUNT(*) as count FROM posts WHERE path NOT LIKE '%.md'").get();
const publishedPosts = result.count;
db.close();
if (publishedPosts === previousCount) {
stableCount++;
} else {
stableCount = 0;
previousCount = publishedPosts;
console.log(`Importing progress: ${publishedPosts}/${totalMarkdownFiles} published posts`);
}
// If the count has been stable for 10 consecutive checks (1 second), consider import complete
// We check against available posts, not total markdown files, since some may be drafts
if (stableCount >= 10 && publishedPosts >= 1) {
console.log(`Import complete: ${publishedPosts} published posts ready`);
break;
}
} catch (e) {
// Database might still be writing or locked
}
}
await new Promise(resolve => setTimeout(resolve, checkInterval));
waited += checkInterval;
}
// Check if we succeeded
if (!(await Bun.file("blog.sqlite").exists())) {
console.error("Database file was not created");
await serverProcess.kill();
process.exit(1);
}
// Final verification - check published posts count
const db = new Database("blog.sqlite", { readonly: true });
const result = db.query("SELECT COUNT(*) as count FROM posts WHERE path NOT LIKE '%.md'").get();
const finalPublishedPosts = result.count;
// Also get total posts for informational purposes
const totalResult = db.query("SELECT COUNT(*) as count FROM posts").get();
const totalPosts = totalResult.count;
db.close();
if (finalPublishedPosts < 1) {
console.error(`No published posts found! Total: ${totalPosts}, Published: ${finalPublishedPosts}`);
await serverProcess.kill();
process.exit(1);
}
await serverProcess.kill();
console.log(`Database initialization complete: ${totalPosts} total posts (${finalPublishedPosts} published)`);
}
waitForDatabase();

View File

@@ -2,8 +2,11 @@
"name": "portfolio",
"version": "1.0.50",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "bun run --watch ./index.tsx"
"dev": "bun run --watch ./index.tsx",
"build": "bun run docker:build",
"docker:build": "docker build --no-cache -t blog:latest .",
"docker:run": "docker run -d -p 3000:3000 --name blog blog:latest",
"docker:stop": "docker stop blog && docker rm blog"
},
"dependencies": {
"gray-matter": "^4.0.3",
@@ -15,4 +18,4 @@
"@types/react-dom": "^19.2.3"
},
"module": "src/index.js"
}
}