A powerful, self-hosted blogging platform with advanced features including, comments, backups, and Progressive Web App capabilities.
- ✅ Multi-Image Upload (up to 12 images per post)
- ✅ File Attachments (PDF, DOC, etc.)
- ✅ Markdown & HTML Editor with toolbar
- ✅ Emoji Picker (44+ emojis)
- ✅ Sticky Posts (pin important posts)
- ✅ Trash System (soft delete with restore)
- ✅ Admin Dashboard with statistics
- ✅ Post Management (filter, search)
- ✅ Media Manager (images & files)
- ✅ Settings Panel (blog configuration)
- ✅ Multi-Language (10 languages: DE, EN, ES, FR, NL, SK, ZH, BS, CZ, RU)
- ✅ Responsive Design (mobile-friendly)
- ✅ Comment System (nested comments with moderation)
- ✅ Inline Admin Editor (edit posts with live preview)
- ✅ Notifications (email alerts, in-app notifications)
- 🔄 Multi-User Support (roles: Super Admin, Admin, Editor, Viewer) (maybe)
- 🔄 Advanced Search (fulltext, filters, saved searches)
- 🔄 Calendar View (posts per day with color coding)
- 🔄 Export/Import (JSON, CSV, ZIP backup)
- 🔄 Automated Backups (scheduled backups)
- 🔄 Security Features (2FA, IP whitelist, brute-force protection)
- 🔄 Audit Log (track all changes)
- 🔄 Theme Editor (customize colors & CSS)
- PHP 8.1+ with extensions:
- pdo_mysql, mbstring, json, curl, intl, gd (optional for image ops), openssl, zip
- Web server: Nginx or Apache
- MySQL/MariaDB 10.5+ (or compatible)
- Composer (optional; if you plan to manage dependencies)
- docker compose
- Posts, comments, sticky posts, trash management
- Backups (export), media manager
- Security (CSRF tokens, sanitized theme selection, optional email notifications)
- PWA-ready front-end
- Simple config via
config.ini
Optional for email:
- msmtp or a local MTA if using SMTP
- App-password for Gmail or a dedicated SMTP account
The installation scripts are not executable by default. On macOS/Linux, make them executable first:
chmod +x install.sh docker-install.sh bare-metal-install.shIf your environment requires ownership changes (to avoid using 777), run the script with sudo so it can set safe ownership and permissions:
# Local install (writes to data/ and uploads/ and data/config.ini)
sudo ./install.sh
# Docker quick install (also adjusts config.ini in repo root)
sudo ./docker-install.sh
# Bare-metal comfort setup (auto-detect web user, ACL option, webserver reload)
sudo ./bare-metal-install.shThese scripts will try to set secure permissions (dirs 775, files 664) and adjust ownership on:
- Non-Docker:
data/,uploads/,data/config.ini,static/styles/,static/styles/custom1.css - Docker quick install:
data/,uploads/,config.ini
If you are not using the default web user (e.g., www-data on Debian/Ubuntu or apache on CentOS/Fedora), set ownership manually:
# Replace www-data with your web server user (e.g., apache, nginx, etc.)
sudo chown -R www-data:www-data data/ uploads/
sudo chown www-data:www-data data/config.ini # for non-Docker
sudo chown www-data:www-data config.ini # for Docker quick install
# Optional (Theme Editor writes custom CSS)
sudo chown -R www-data:www-data static/styles
sudo chown www-data:www-data static/styles/custom1.cssWindows users: Run scripts via Git Bash or WSL, or execute docker compose commands directly if you prefer not to use the scripts.
This is the fastest way to get the blog running.
- Clone the repository
git clone https://github.com/el-choco/blog-advanced.git
cd blog-advanced- Make the installation scripts executable
chmod +x install.sh docker-install.sh- Run the Docker installation script
./docker-install.sh
# Or with sudo if you need to set ownership on config files and writable directories:
# sudo ./docker-install.shThe script will:
- Create necessary directories (data/, uploads/, data/backups/)
- Set proper ownership (www-data:www-data)
- Set proper permissions (0775)
- Create .env file from .env.example
- Start Docker containers
- Import database schema
- Access your blog
- Frontend: http://localhost:3333
- Admin Panel: http://localhost:3333/admin/
- phpMyAdmin: http://localhost:3334
Default credentials:
- Username: admin
- Password: admin123
⚠️ IMPORTANT: Change the default password immediately!
If you prefer to set up manually or customize the installation:
- Clone the repository
git clone https://github.com/el-choco/blog-advanced.git
cd blog-advanced- Prepare environment files
- Copy example env files (if present) or create your own
.env:
cp .env.example .env 2>/dev/null || true
# Edit .env with your DB credentials and app settings- Create necessary directories and set permissions
# Make scripts executable
chmod +x install.sh docker-install.sh
# Run the install script to create directories
./install.sh
# Or with sudo if ownership changes are required:
# sudo ./install.sh
# Or manually create and set permissions
mkdir -p data data/backups uploads
chown -R www-data:www-data data uploads
chmod -R 0775 data uploads- Use existing docker-compose.yml or customize The repository includes pre-configured docker-compose files:
docker-compose.yml- Default MySQL setupdocker-compose.sqlite.yml- SQLite setupdocker-compose.postgres.yml- PostgreSQL setup
Or create a custom docker-compose.yml (choose MySQL, Postgres or SQLite):
version: "3.9"
services:
web:
image: php:8.2-apache
container_name: blog-advanced-web
volumes:
- ./:/var/www/html
ports:
- "8080:80"
environment:
- PHP_OPCACHE_VALIDATE_TIMESTAMPS=1
depends_on:
- db
db:
image: mariadb:11
container_name: blog-advanced-db
restart: unless-stopped
environment:
- MYSQL_ROOT_PASSWORD=changeme-root
- MYSQL_DATABASE=blog
- MYSQL_USER=bloguser
- MYSQL_PASSWORD=changeme
volumes:
- db_data:/var/lib/mysql
volumes:
db_data:- Start containers
docker compose up -d- Configure PHP and Apache inside the container (optional)
- Enable required extensions:
docker exec -it blog-advanced-web bash -lc "apt-get update && apt-get install -y libicu-dev libzip-dev libjpeg62-turbo-dev libpng-dev && docker-php-ext-install intl zip"Note: official image ships with many basics. Adjust steps as needed.
- Access the app
- Open http://localhost:3333
- Go to http://localhost:3333/admin to configure settings, language, timezone, and theme.
- Database configuration
- Edit
config.ini(see “Configuration” section) with the DB hostdband your credentials from docker-compose:- mysql_host = db
- mysql_port = 3306
- mysql_user = bloguser
- mysql_pass = changeme
- db_name = blog
- Import database schema (if not using automated script)
# Wait for database to be ready
sleep 30
# Import schema
docker exec -i blog-advanced-db mysql -u bloguser -pblogpass123 blog < app/db/mysql/01_schema.sql- Logs and backups
- Use the admin “Backups” page to create and download archives. Store externally.
- Configure file paths in
config.inias needed.
- Clone the repository into your web root
cd /var/www
git clone https://github.com/el-choco/blog-advanced.git
cd blog-advanced- Make the installation script executable
chmod +x install.sh- Run the installation script
# Run as root or with sudo to set proper ownership
sudo ./install.shThe script will:
- Create necessary directories (data/, uploads/, data/backups/, logs/, sessions/)
- Set ownership to www-data:www-data
- Set permissions to 0775
- Create .gitkeep files
- Check for PHP and required extensions
Or manually create directories and set permissions (if not using the install script):
mkdir -p data/{posts,images,files,users,backups,cache} uploads/{images,files} logs sessions
chown -R www-data:www-data data uploads
chmod -R 0775 data uploads data/backups- Configure your web server
- Apache (VirtualHost example):
<VirtualHost *:80>
ServerName blog.local
DocumentRoot /var/www/blog-advanced
<Directory /var/www/blog-advanced>
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/blog-advanced-error.log
CustomLog ${APACHE_LOG_DIR}/blog-advanced-access.log combined
</VirtualHost>
Enable site and reload:
a2enmod rewrite
a2ensite blog-advanced.conf
systemctl reload apache2- Nginx (server block example):
server {
listen 80;
server_name blog.local;
root /var/www/blog-advanced;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
location ~* \.(css|js|png|jpg|jpeg|gif|svg|webp|ico)$ {
expires 30d;
access_log off;
}
}
Reload:
nginx -t && systemctl reload nginx- PHP extensions
- Ensure required extensions are enabled:
php -m | grep -E 'pdo_mysql|mbstring|intl|zip|gd|curl|openssl|json'Install missing ones via package manager (Debian/Ubuntu example):
apt-get install -y php8.2-{mysql,mbstring,intl,zip,gd,curl}
systemctl reload php8.2-fpm || systemctl restart apache2- Database
- Create a database and user:
CREATE DATABASE blog CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'bloguser'@'%' IDENTIFIED BY 'changeme';
GRANT ALL PRIVILEGES ON blog.* TO 'bloguser'@'%';
FLUSH PRIVILEGES;- Import the schema for your database type:
- MySQL/MariaDB:
mysql -u bloguser -p blog < app/db/mysql/01_schema.sql - PostgreSQL:
psql -U bloguser -W -d blog -f app/db/postgres/01_schema.sql - SQLite: Schema is automatically loaded on first run
- MySQL/MariaDB:
The schema files include all required tables: posts, categories, comments, images, and users.
- Configuration
- Create or edit
config.iniin the project root: - chose DB Type by removing the Semikolon ( ; ).
;[database]
;db_connection = sqlite
;sqlite_db = data/sqlite.db
;[database]
;db_connection = "mysql"
;mysql_host = "db"
;mysql_port = "3306"
;mysql_user = "bloguser"
;mysql_pass = "blogpass123"
;db_name = "blog"
;[database]
;db_connection = postgres
;postgres_socket = /tmp/postgres.sock
;postgres_host = db
;postgres_port = 5432
;postgres_user = bloguser
;postgres_pass = blogpass123
;db_name = blog
[profile]
title = "Blog"
name = "your name"
pic_small = "static/images/profile.jpg"
pic_big = "static/images/profile_big.jpg"
cover = "static/images/cover.jpg"
[language]
lang = "de"
[components]
highlight = "1"
[custom]
theme = "theme02"
[admin]
force_login = "1"
nick = "admin"
pass = "admin"
[friends]
[directories]
images_path = "data/i/"
thumbnails_path = "data/t/"
logs_path = "data/logs/"
backup_dir = "data/backups/"
[proxy]
[system]
timezone = "Europe/Berlin"
version = "1.42"
debug = "0"
logs = "1"
SOFT_DELETE = "0"
HARD_DELETE_FILES = "1"
AUTO_CLEANUP_IMAGES = "1"
[visitor]
enabled = "1"
title = "Blog"
name = "your name"
subtitle = ""
lang = "en"
timezone = "Europe/Berlin"
[email]
notifications_enabled = "1"
admin_email = "your@mail.com"
notify_admin_new_comment = "1"
notify_user_approved = "1"
from_email = "noreply@pacos-blog.dynv6.net"
from_name = "your-Tech-Blog"
title = "Blog"
name = "your name"
subtitle = ""
lang = "en"
timezone = "Europe/Berlin"
theme = "theme02"
Note: Do not commit secrets to the repo. Add sensitive files to .gitignore (see below).
- Permissions
- If you skipped running
./install.shearlier, ensure proper permissions:
# Create additional directories if needed
mkdir -p data/i data/t data/logs static/images
# Set ownership and permissions
chown -R www-data:www-data data uploads
chmod -R 0775 data uploads data/backupsNote: The install.sh script already handles most permissions. This step is only needed if you're setting up manually.
- Access
- Open http://blog.local (adjust hosts/DNS) and http://blog.local/admin
- If you use msmtp:
- Place
~/.msmtprcor/etc/msmtprcwith chmod 600. - Configure PHP
sendmail_path:sendmail_path = "/usr/bin/msmtp -t" - In the app, enable email notifications in Admin → Email (set admin_email, from_email, from_name).
- Place
- Alternative: Use native PHP mail or a transactional service (SMTP credentials).
Security: Never commit credentials. Rotate app passwords if exposed.
- Backups: Use Admin → Backups to create and download archives. Store externally.
- Logs: Keep
data/logs/readable and rotated. - Theme: Switch under Admin → Appearance. Theme setting is sanitized to prevent invalid names.
- Trash: Admin → Trash for restore/permanent delete.
- Comments: Admin → Comments for moderation (email notifications optional).
The backup system supports multiple export formats:
- SQL Backup - Traditional mysqldump backup (database only)
- JSON Export - Structured export of posts, categories, and comments
- CSV Export - Spreadsheet-compatible export as ZIP archive
- Full Backup - Complete backup including database and media files
Backup Directory:
- Default location:
data/backups/ - Configurable via
config.iniunder[directories]section:backup_dir = "data/backups/"
Import (Replace Mode):
- Importing a backup will replace existing data (truncate tables first)
- Import order: categories → posts → posts_categories → comments
- For MySQL: Foreign key checks are temporarily disabled during import
- Supported formats:
.json(JSON export) and.zip(Full backup)
MySQL Foreign Key Handling: When restoring data on MySQL, the import process:
- Executes
SET FOREIGN_KEY_CHECKS = 0before import - Truncates tables and inserts data
- Executes
SET FOREIGN_KEY_CHECKS = 1after import
This ensures proper restoration even with complex table relationships.
Standard Update: (Bind mount or code in the image doesn't matter):
Get the latest changes:
git fetch origin
git pull origin main
Rebuild and start the container:
docker compose down
docker compose build
docker compose up -d
Quick version (combined build on startup):
git pull origin main
docker compose up -d --build
If your Compose version doesn't support flags like --build:
git pull origin main
docker compose down
docker compose build
docker compose up -d
Optional (if old code is stubbornly running):
docker compose exec web grep -n "tab-language" /var/www/html/admin/settings.php
docker compose restart
If necessary, remove old images and rebuild:
docker images
docker rmi <image_id_of_web_image>
docker compose build && docker compose up -d
After the Check for updates:
git log -1 --oneline (shows the last commit)
docker compose ps (is the container running?)
Hardly reload the browser (Ctrl+F5)
Verify specific functionality (e.g., language tab):
grep -n "tab-language" admin/settings.php or in the container as above.
Shortcut for everyday use:
git pull origin main
docker compose up -d --build
If there are errors or conflicts during the pull:
git stash push -u -m "before update"
git pull origin main
- 403/permissions on uploads:
- Check ownership/permissions for
data/andstatic/images/.
- Check ownership/permissions for
- Missing PHP extensions:
- Install and reload PHP-FPM/Apache.
- Blank page:
- Check web server error logs and PHP logs.
- Email not sending:
- Verify msmtp logs and credentials, enable app passwords if using Gmail.
See repository for license information.
Maintained by el-choco. Contributions welcome via issues and pull requests.
If you deploy without Docker, you can choose between a minimal installer and a comfort helper script:
-
install.sh(minimal):- Creates required directories (
data/,uploads/,logs/,sessions/) - Sets safe ownership (default
www-data:www-data) and permissions (dirs0775, files0664) - Ensures Theme Editor paths exist and are writable (
static/styles/custom1.css)
- Creates required directories (
-
bare-metal-install.sh(comfort):- Auto-detects the web user (
www-data,nginx, orapache) with fallback to your current user - Optional ACLs (
--apply-acl) to grant write access without changing ownership (usessetfaclif available) - Guarantees
static/styles/custom1.cssexists and is writable - Optional web server reload (
--reload) fornginx,apache2/httpdviasystemctl - Customizable user/group via flags:
--web-user,--web-group - Can skip the base installer:
--skip-install
- Auto-detects the web user (
# Run with auto-detection and base setup
./bare-metal-install.sh
# Explicit web user/group and reload the web server
./bare-metal-install.sh --web-user www-data --web-group www-data --reload
# Use ACLs to grant write access without changing ownership
./bare-metal-install.sh --apply-acl
# If install.sh already ran and you want only the comfort steps
./bare-metal-install.sh --skip-install- Writable by the web server:
data/,uploads/,logs/,sessions/→ ownerWEB_USER:WEB_GROUP, dirs0775, files0664static/styles/andstatic/styles/custom1.css(Theme Editor) → same as above
- Read-only for the web server:
app/,static/scripts/, other static assets → dirs0755, files0644
- Avoid
0777entirely.
Note: On systems without setfacl, ACL mode automatically falls back to chown/chmod.