From 682e006e0bb81587aa644ce15cad0462ada3a468 Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Mon, 16 Jun 2025 22:20:38 +0200 Subject: [PATCH] docker shit --- .dockerignore | 35 ++++++ Dockerfile | 47 +++++-- README.md | 78 ++++++++++-- manage_container.sh | 192 +++++++++++++++++++++++++++-- next.config.js | 17 +++ package-lock.json | 19 +++ package.json | 1 + src/app/api/admin/folders/route.ts | 73 ++++++++++- src/lib/markdown.ts | 12 +- 9 files changed, 439 insertions(+), 35 deletions(-) create mode 100644 .dockerignore create mode 100644 next.config.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..84dac53 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,35 @@ +# Dependencies +node_modules +npm-debug.log +yarn-debug.log +yarn-error.log + +# Next.js build output +.next +out + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Version control +.git +.gitignore + +# IDE files +.idea +.vscode +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +# Docker files +Dockerfile +.dockerignore +manage_container.sh \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 25cf49a..f9bf1cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,52 @@ -# Use Node.js as the base image -FROM node:18 +# Build stage +FROM node:18-alpine AS builder -# Set the working directory +# Set working directory WORKDIR /app -# Copy package.json and package-lock.json (if available) +# Copy package files COPY package*.json ./ # Install dependencies -RUN npm install +RUN npm ci -# Copy the rest of the application code +# Copy source code and config files COPY . . +# Build the application +RUN npm run build + +# Production stage +FROM node:18-alpine AS runner + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install production dependencies only +RUN npm ci --only=production + +# Copy necessary files from builder +COPY --from=builder /app/.next ./.next/ +COPY --from=builder /app/public ./public/ +COPY --from=builder /app/next.config.js ./ + # Create a directory for markdown files RUN mkdir -p /markdown -# Expose the port your app runs on -EXPOSE 3000 +# Set environment variables +ENV NODE_ENV=production +ENV PORT=8080 +ENV HOSTNAME=0.0.0.0 -# Command to run the application +# Expose the port +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/ || exit 1 + +# Start the application CMD ["npm", "start"] \ No newline at end of file diff --git a/README.md b/README.md index 2225d91..906fc7b 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![Tailwind CSS](https://img.shields.io/badge/TailwindCSS-3.4-blue?style=flat-square&logo=tailwind-css&logoColor=white)](https://tailwindcss.com/) [![TypeScript](https://img.shields.io/badge/TypeScript-5-blue?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) [![Electron](https://img.shields.io/badge/Electron-28-47848F?style=flat-square&logo=electron&logoColor=white)](https://www.electronjs.org/) +[![Docker](https://img.shields.io/badge/Docker-24-2496ED?style=flat-square&logo=docker&logoColor=white)](https://www.docker.com/) [![MIT License](https://img.shields.io/badge/License-MIT-green?style=flat-square)](LICENSE) A modern, cross-platform blog system built with **Next.js**, **Markdown**, and **Electron**. Write posts in Markdown, manage content visually, and deploy to web or desktop. @@ -18,6 +19,7 @@ A modern, cross-platform blog system built with **Next.js**, **Markdown**, and * - 🖥️ **Electron desktop app** — Run your blog as a native desktop app - 📱 **Responsive UI** — Mobile-friendly and clean design - 🛠️ **Admin dashboard** — Manage posts and folders visually +- 🐳 **Docker support** — Easy deployment with Docker containers --- @@ -27,6 +29,7 @@ A modern, cross-platform blog system built with **Next.js**, **Markdown**, and * - [Node.js 18+](https://nodejs.org/) - [npm](https://www.npmjs.com/) +- [Docker](https://www.docker.com/) (for containerized deployment) ### Installation @@ -52,6 +55,70 @@ npm install --- +## 🐳 Docker Deployment + +The project includes Docker support for easy deployment. A `manage_container.sh` script is provided to simplify container management. + +### Docker Setup + +1. Make sure Docker is installed and running on your system +2. Update the `MARKDOWN_DIR` path in `manage_container.sh` to point to your local markdown directory +3. Make the script executable: + ```bash + chmod +x manage_container.sh + ``` + +### Container Management + +The `manage_container.sh` script provides several commands: + +```bash +# Build the Docker image +./manage_container.sh build + +# Start the container +./manage_container.sh start + +# Stop the container +./manage_container.sh stop + +# Restart the container +./manage_container.sh restart + +# View container logs +./manage_container.sh logs + +# Check container status +./manage_container.sh status + +# Remove the container +./manage_container.sh remove +``` + +### Container Features + +- **Health Checks**: Automatic health monitoring +- **Auto-restart**: Container restarts automatically if it crashes +- **Volume Mounting**: Your markdown files are mounted into the container +- **Port Mapping**: Access the blog at http://localhost:8080 + +### Container Status + +The status command shows: +- Container running state +- Health check status +- Access URL + +### Troubleshooting + +If the container fails to start: +1. Check the logs: `./manage_container.sh logs` +2. Verify Docker is running +3. Ensure port 8080 is available +4. Check the markdown directory path in `manage_container.sh` + +--- + ## 📝 Writing Posts Add Markdown files to the `posts/` directory. Each post should have frontmatter: @@ -79,7 +146,9 @@ markdownblog/ │ └── lib/ # Utility functions ├── electron/ # Desktop app code ├── public/ # Static assets (favicons, etc.) -└── ... +├── Dockerfile # Docker configuration +├── .dockerignore # Docker ignore rules +└── manage_container.sh # Docker management script ``` --- @@ -99,12 +168,7 @@ Next.js will automatically serve these at the root URL (e.g., `/favicon.ico`). - [Electron](https://www.electronjs.org/) - [Remark](https://remark.js.org/) (Markdown) - [date-fns](https://date-fns.org/) - ---- - -## 🐳 Docker - -A sample `Dockerfile` and `manage_container.sh` are included for containerized deployment. +- [Docker](https://www.docker.com/) --- diff --git a/manage_container.sh b/manage_container.sh index 5b49741..0140d3d 100755 --- a/manage_container.sh +++ b/manage_container.sh @@ -3,39 +3,199 @@ # Configuration IMAGE_NAME="markdown-blog" CONTAINER_NAME="markdown-blog-container" -PORT=3000 -MARKDOWN_DIR="/path/to/your/markdown" # Update this to your local markdown directory +PORT=8080 +MARKDOWN_DIR="/home/rattatwinko/Documents/shit/markdownblog" # Update this to your local markdown directory + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to check if Docker is running +check_docker() { + if ! docker info > /dev/null 2>&1; then + echo -e "${RED}Error: Docker is not running${NC}" + exit 1 + fi +} + +# Function to check if container exists +container_exists() { + docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$" +} + +# Function to check if container is running +container_running() { + docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$" +} + +# Function to check container health +check_container_health() { + local health_status + health_status=$(docker inspect --format='{{.State.Health.Status}}' $CONTAINER_NAME 2>/dev/null) + if [ "$health_status" = "healthy" ]; then + return 0 + else + return 1 + fi +} # Function to build the Docker image build_image() { - echo "Building Docker image..." - docker build -t $IMAGE_NAME . + echo -e "${YELLOW}Building Docker image...${NC}" + if ! docker build -t $IMAGE_NAME .; then + echo -e "${RED}Error: Failed to build Docker image${NC}" + exit 1 + fi + echo -e "${GREEN}Docker image built successfully${NC}" } # Function to start the container start_container() { - echo "Starting container..." - docker run -d --name $CONTAINER_NAME -p $PORT:3000 -v $MARKDOWN_DIR:/markdown $IMAGE_NAME + echo -e "${YELLOW}Starting container...${NC}" + + # Check if container already exists + if container_exists; then + if container_running; then + echo -e "${YELLOW}Container is already running${NC}" + return + else + echo -e "${YELLOW}Container exists but is not running. Starting it...${NC}" + if ! docker start $CONTAINER_NAME; then + echo -e "${RED}Error: Failed to start existing container${NC}" + exit 1 + fi + fi + else + # Create and start new container + if ! docker run -d \ + --name $CONTAINER_NAME \ + -p $PORT:8080 \ + -v "$MARKDOWN_DIR:/markdown" \ + --restart unless-stopped \ + --health-cmd="wget --no-verbose --tries=1 --spider http://localhost:8080/ || exit 1" \ + --health-interval=30s \ + --health-timeout=30s \ + --health-retries=3 \ + --health-start-period=5s \ + $IMAGE_NAME; then + echo -e "${RED}Error: Failed to create and start container${NC}" + exit 1 + fi + fi + + # Wait for container to be ready + echo -e "${YELLOW}Waiting for container to be ready...${NC}" + local max_attempts=60 + local attempt=1 + + while [ $attempt -le $max_attempts ]; do + if check_container_health; then + echo -e "${GREEN}Container is healthy and accessible at http://localhost:$PORT${NC}" + return + fi + + # Check if container is still running + if ! container_running; then + echo -e "${RED}Error: Container stopped unexpectedly${NC}" + docker logs $CONTAINER_NAME + exit 1 + fi + + echo -n "." + sleep 1 + attempt=$((attempt + 1)) + done + + echo -e "${RED}Error: Container did not become healthy in time${NC}" + docker logs $CONTAINER_NAME + exit 1 } # Function to stop the container stop_container() { - echo "Stopping container..." - docker stop $CONTAINER_NAME + echo -e "${YELLOW}Stopping container...${NC}" + if ! container_exists; then + echo -e "${YELLOW}Container does not exist${NC}" + return + fi + + if ! docker stop $CONTAINER_NAME; then + echo -e "${RED}Error: Failed to stop container${NC}" + exit 1 + fi + echo -e "${GREEN}Container stopped successfully${NC}" +} + +# Function to remove the container +remove_container() { + echo -e "${YELLOW}Removing container...${NC}" + if ! container_exists; then + echo -e "${YELLOW}Container does not exist${NC}" + return + fi + + if container_running; then + stop_container + fi + + if ! docker rm $CONTAINER_NAME; then + echo -e "${RED}Error: Failed to remove container${NC}" + exit 1 + fi + echo -e "${GREEN}Container removed successfully${NC}" } # Function to restart the container restart_container() { - echo "Restarting container..." - docker restart $CONTAINER_NAME + echo -e "${YELLOW}Restarting container...${NC}" + if ! container_exists; then + echo -e "${YELLOW}Container does not exist. Starting new container...${NC}" + start_container + return + fi + + if ! docker restart $CONTAINER_NAME; then + echo -e "${RED}Error: Failed to restart container${NC}" + exit 1 + fi + echo -e "${GREEN}Container restarted successfully${NC}" } # Function to view logs view_logs() { - echo "Viewing logs..." - docker logs $CONTAINER_NAME + echo -e "${YELLOW}Viewing logs...${NC}" + if ! container_exists; then + echo -e "${RED}Error: Container does not exist${NC}" + exit 1 + fi + + docker logs -f $CONTAINER_NAME } +# Function to show container status +show_status() { + echo -e "${YELLOW}Container Status:${NC}" + if ! container_exists; then + echo -e "${RED}Container does not exist${NC}" + return + fi + + if container_running; then + local health_status + health_status=$(docker inspect --format='{{.State.Health.Status}}' $CONTAINER_NAME 2>/dev/null) + echo -e "${GREEN}Container is running${NC}" + echo "Health status: $health_status" + echo "Access the application at: http://localhost:$PORT" + else + echo -e "${YELLOW}Container exists but is not running${NC}" + fi +} + +# Check Docker is running before proceeding +check_docker + # Main script logic case "$1" in build) @@ -53,8 +213,14 @@ case "$1" in logs) view_logs ;; + status) + show_status + ;; + remove) + remove_container + ;; *) - echo "Usage: $0 {build|start|stop|restart|logs}" + echo "Usage: $0 {build|start|stop|restart|logs|status|remove}" exit 1 ;; esac diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..fbd13e3 --- /dev/null +++ b/next.config.js @@ -0,0 +1,17 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'standalone', + reactStrictMode: true, + swcMinify: true, + // Ensure we can access the app from outside the container + experimental: { + outputFileTracingRoot: undefined, + }, + // Configure the hostname and port + server: { + hostname: '0.0.0.0', + port: 8080, + }, +} + +module.exports = nextConfig \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4add222..cdb26be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "tailwindcss": "^3.4.1" }, "devDependencies": { + "@types/chokidar": "^1.7.5", "@types/node": "^20.11.19", "@types/react": "^18.2.57", "@types/react-dom": "^18.2.19", @@ -1112,6 +1113,17 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/chokidar": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@types/chokidar/-/chokidar-1.7.5.tgz", + "integrity": "sha512-PDkSRY7KltW3M60hSBlerxI8SFPXsO3AL/aRVsO4Kh9IHRW74Ih75gUuTd/aE4LSSFqypb10UIX3QzOJwBQMGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/events": "*", + "@types/node": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -1121,6 +1133,13 @@ "@types/ms": "*" } }, + "node_modules/@types/events": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz", + "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", diff --git a/package.json b/package.json index 7c5979f..25d9be1 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "tailwindcss": "^3.4.1" }, "devDependencies": { + "@types/chokidar": "^1.7.5", "@types/node": "^20.11.19", "@types/react": "^18.2.57", "@types/react-dom": "^18.2.19", diff --git a/src/app/api/admin/folders/route.ts b/src/app/api/admin/folders/route.ts index 0519ecb..7609010 100644 --- a/src/app/api/admin/folders/route.ts +++ b/src/app/api/admin/folders/route.ts @@ -1 +1,72 @@ - \ No newline at end of file +import { NextResponse } from 'next/server'; +import fs from 'fs'; +import path from 'path'; + +const postsDirectory = path.join(process.cwd(), 'posts'); + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { name, path: folderPath } = body; + + // Create the full path for the new folder + const fullPath = path.join(postsDirectory, folderPath, name); + + // Check if folder already exists + if (fs.existsSync(fullPath)) { + return NextResponse.json( + { error: 'Folder already exists' }, + { status: 400 } + ); + } + + // Create the folder + fs.mkdirSync(fullPath, { recursive: true }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Error creating folder:', error); + return NextResponse.json( + { error: 'Error creating folder' }, + { status: 500 } + ); + } +} + +export async function DELETE(request: Request) { + try { + const body = await request.json(); + const { name, path: folderPath } = body; + + // Create the full path for the folder to delete + const fullPath = path.join(postsDirectory, folderPath, name); + + // Check if folder exists + if (!fs.existsSync(fullPath)) { + return NextResponse.json( + { error: 'Folder does not exist' }, + { status: 404 } + ); + } + + // Check if folder is empty + const files = fs.readdirSync(fullPath); + if (files.length > 0) { + return NextResponse.json( + { error: 'Cannot delete non-empty folder' }, + { status: 400 } + ); + } + + // Delete the folder + fs.rmdirSync(fullPath); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Error deleting folder:', error); + return NextResponse.json( + { error: 'Error deleting folder' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts index aaafe11..46d6b32 100644 --- a/src/lib/markdown.ts +++ b/src/lib/markdown.ts @@ -4,6 +4,7 @@ import matter from 'gray-matter'; import { remark } from 'remark'; import html from 'remark-html'; import chokidar from 'chokidar'; +import type { FSWatcher } from 'chokidar'; export interface Post { slug: string; @@ -64,12 +65,13 @@ export async function getPostsByTag(tag: string): Promise { const allPosts = await getAllPosts(); return allPosts.filter((post) => post.tags.includes(tag)); } - // File watcher setup - let watcher: chokidar.FSWatcher | null = null; - let onChangeCallback: (() => void) | null = null; - export function watchPosts(callback: () => void) { - if (watcher) { +// File watcher setup +let watcher: FSWatcher | null = null; +let onChangeCallback: (() => void) | null = null; + +export function watchPosts(callback: () => void) { + if (watcher) { watcher.close(); }