docker shit
This commit is contained in:
35
.dockerignore
Normal file
35
.dockerignore
Normal file
@@ -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
|
||||
47
Dockerfile
47
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"]
|
||||
78
README.md
78
README.md
@@ -4,6 +4,7 @@
|
||||
[](https://tailwindcss.com/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://www.electronjs.org/)
|
||||
[](https://www.docker.com/)
|
||||
[](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/)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
17
next.config.js
Normal file
17
next.config.js
Normal file
@@ -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
|
||||
19
package-lock.json
generated
19
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1 +1,72 @@
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<Post[]> {
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user