Categories
Web Site Management

Building BlueElysium: Part 1 – The Web Server Foundation

A journey from concept to production: Building a self-hosted WordPress website with Docker


Introduction

When I decided to build my own self-hosted website and email infrastructure, I knew I wanted complete control over my data, flexibility to customize everything, and a learning experience that would deepen my understanding of modern web technologies. This is the story of building BlueElysium – a fully containerized, production-ready web and mail server.

In this first part, I’ll walk you through building the web server foundation: WordPress, MySQL, Nginx reverse proxy, and automated SSL certificate management.

The Vision

Goal: Create a self-hosted WordPress website with:

  • Automated SSL certificate management
  • Production-grade security
  • Easy maintenance and updates
  • Scalable architecture for future expansion
  • Complete data ownership

Technology Stack:

  • Docker & Docker Compose for containerization
  • WordPress with PHP 8.2-FPM
  • MySQL 8.0 for database
  • Nginx as reverse proxy and web server
  • Let’s Encrypt for SSL/TLS certificates via Certbot

Part 1: The Infrastructure Design

Why Docker?

I chose Docker for several compelling reasons:

  1. Isolation: Each service runs in its own container with defined resources
  2. Reproducibility: The entire stack can be rebuilt from docker-compose.yml
  3. Portability: Easy to migrate to another server if needed
  4. Updates: Update individual services without affecting others
  5. Scalability: Add new services (like email) without disrupting existing ones

Architecture Overview

Internet → Port 80 443 → Nginx (webserver) → WordPress (PHP-FPM) → MySQL
                            ↓
                     Let's Encrypt (certbot)

The architecture consists of four core containers:

  1. MySQL Database – Backend storage for WordPress
  2. WordPress – The CMS with PHP-FPM
  3. Nginx (webserver) – Reverse proxy with SSL termination
  4. Certbot – Automated SSL certificate management

Part 2: Setting Up the Foundation

Step 1: Project Structure

I organized the project with persistent data separated from container definitions:

BlueElysium/
├── docker-compose.yml          # Container orchestration
├── .env                        # Sensitive configuration
├── docker-data/                # Persistent data volumes
│   ├── wordpress/
│   │   └── html/              # WordPress files
│   ├── mysql/                 # Database files
│   └── certbot/               # SSL certificates
└── nginx-conf/                # Nginx configuration
    └── default.conf           # Reverse proxy config

Step 2: The Database Container

MySQL is the heart of the WordPress installation. Here’s what I learned about configuring it:

Key Decisions:

  • Used MySQL 8.0 for better performance and security features
  • Created separate databases for WordPress and future mail server
  • Configured for UTF-8mb4 to support all Unicode characters
  • Set up health checks to ensure database is ready before WordPress starts

Important Configuration:

db:
    image: mysql:8.0
    container_name: db
    restart: unless-stopped
    env_file: .env
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - MYSQL_DATABASE=${MYSQL_WPBE_DATABASE}
      - MYSQL_USER=${MYSQL_WPBE_USER}
      - MYSQL_PASSWORD=${MYSQL_WPBE_PASSWORD}
    volumes:
      - dbdata:/var/lib/mysql
      - ./docker/provision/mysql/init:/docker-entrypoint-initdb.d
    command: '--default-authentication-plugin=mysql_native_password'
    #command: '--default-authentication-plugin=caching_sha256_password'
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      timeout: 20s
      retries: 4
    networks:
      - app-network

Lesson Learned: Always use health checks! This prevents WordPress from trying to connect before MySQL is ready, eliminating those frustrating “Error establishing database connection” messages during startup.

Step 3: WordPress Container

WordPress runs as a separate container with PHP-FPM, communicating with Nginx via FastCGI.

Key Configurations:

wordpress:
    depends_on:
      db:
        condition: service_healthy
    image: wordpress:6.4.3-fpm-alpine
    container_name: wordpress
    restart: unless-stopped
    env_file: .env
    environment:
      - WORDPRESS_DB_HOST=db:3306
      - WORDPRESS_DB_USER=${MYSQL_WPBE_USER}
      - WORDPRESS_DB_PASSWORD=${MYSQL_WPBE_PASSWORD}
      - WORDPRESS_DB_NAME=${MYSQL_WPBE_DATABASE}
    volumes:
      - wordpress:/var/www/html
    healthcheck:
      test: ["CMD-SHELL", "php-fpm -t || exit 1"]
      timeout: 10s
      retries: 4
      interval: 20s
    networks:
      - app-network

Why PHP-FPM?

  • Better performance than Apache mod_php
  • Separates web server (Nginx) from PHP processing
  • More granular resource control
  • Industry standard for production WordPress

Challenge: Getting WordPress and Nginx to communicate properly took some trial and error. The key was ensuring they shared the WordPress files volume and using the correct FastCGI parameters.

Step 4: Nginx Reverse Proxy

Nginx handles incoming HTTP/HTTPS requests and routes them to WordPress.

Configuration Highlights:

server {
        listen 80 default_server;
        listen [::]:80 default_server;

        server_name blueelysium.net www.blueelysium.net;

        location ~ /.well-known/acme-challenge {
                allow all;
                root /var/www/html;
        }
...

Critical Lessons:

  1. Shared Volumes: Nginx needs access to WordPress files for static assets
  2. FastCGI Path: Must use correct SCRIPT_FILENAME path
  3. Security Headers: Always include HSTS and other security headers
  4. HTTP/2: Significantly improves performance for modern browsers

Step 5: SSL Certificate Automation

Let’s Encrypt provides free SSL certificates, but they expire every 90 days. Automation is essential.

Certbot Container:

certbot:
  image: certbot/certbot:latest
  volumes:
    - ./docker-data/certbot/conf:/etc/letsencrypt
    - ./docker-data/certbot/www:/var/www/certbot
  entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

Initial Certificate Request:

docker compose run --rm certbot certonly \
  --webroot \
  --webroot-path=/var/www/certbot \
  --email admin@blueelysium.net \
  --agree-tos \
  --no-eff-email \
  -d blueelysium.net \
  -d www.blueelysium.net

The Chicken-and-Egg Problem:

  • Nginx needs certificates to start with SSL
  • Certbot needs Nginx running to verify domain ownership

Solution:

  1. Start with HTTP-only Nginx configuration
  2. Request certificates
  3. Update Nginx config to use SSL
  4. Restart Nginx

Pro Tip: Use the webroot method instead of standalone. This allows Nginx to keep running during renewals.

Part 3: Environment Variables & Security

The .env File

Never hardcode secrets! I used environment variables for all sensitive data:

# Domain Configuration
DOMAIN=blueelysium.net
CERTBOT_EMAIL=admin@blueelysium.net

# MySQL Configuration
MYSQL_ROOT_PASSWORD=SecureRootPassword123!
MYSQL_WPBE_DATABASE=mydb
MYSQL_WPBE_USER=myuser
MYSQL_WPBE_PASSWORD=SecureWPPassword456!

Security Practices:

  1. Never commit .env to git – Add to .gitignore immediately
  2. Use strong passwords – 20+ characters, mixed case, numbers, symbols
  3. Different passwords – Each service gets unique credentials
  4. Backup .env securely – Store encrypted copy off-server

Firewall Configuration

I configured UFW (Uncomplicated Firewall) to allow only necessary ports:

sudo ufw allow 80/tcp   # HTTP
sudo ufw allow 443/tcp  # HTTPS
sudo ufw allow 22/tcp   # SSH
sudo ufw enable

Important: Configure SSH access BEFORE enabling the firewall!

Part 4: Deployment & Testing

First Deployment

# Start all services
docker compose up -d

# Check status
docker compose ps

# View logs
docker compose logs -f

What I Watched For:

  1. MySQL health check passing
  2. WordPress connecting to database
  3. Nginx starting without errors
  4. Certificates being issued successfully

Initial WordPress Setup

After containers were running, I accessed https://blueelysium.net and completed WordPress installation:

  1. Selected language
  2. Created admin account (strong password!)
  3. Set site title and tagline
  4. Configured permalink structure (Post name for SEO)

Performance Testing

I used several tools to verify everything was working optimally:

SSL/TLS Configuration:

# Test SSL
curl -I https://blueelysium.net

# Check certificate
openssl s_client -connect blueelysium.net:443 -servername blueelysium.net

Results:

  • ✅ A+ rating on SSL Labs
  • ✅ TLS 1.2 and 1.3 only
  • ✅ Strong cipher suites
  • ✅ HSTS enabled

Page Load Speed:

  • Initial: ~2.5 seconds
  • With caching plugins: ~800ms
  • With CDN consideration for future

Part 5: Common Issues & Solutions

Issue 1: “Error Establishing Database Connection”

Problem: WordPress couldn’t connect to MySQL

Root Cause: WordPress container started before MySQL was ready

Solution: Added health check to database and depends_on with condition: service_healthy

Issue 2: 502 Bad Gateway

Problem: Nginx showed 502 error when accessing WordPress

Root Cause: FastCGI configuration incorrect

Solution:

fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

Must use $document_root not hardcoded path.

Issue 3: Static Assets Not Loading

Problem: Images and CSS files returned 404

Root Cause: Nginx didn’t have access to WordPress files

Solution: Shared WordPress volume with Nginx:

webserver:
  volumes:
    - ./docker-data/wordpress/html:/var/www/html:ro

Issue 4: Certificate Renewal Failed

Problem: Let’s Encrypt couldn’t verify domain ownership

Root Cause: Nginx blocked /.well-known/acme-challenge/ path

Solution: Added specific location block for ACME challenges:

location /.well-known/acme-challenge/ {
    root /var/www/certbot;
}

Part 6: Optimization & Fine-Tuning

WordPress Optimizations

Plugins Installed:

  • WP Super Cache – Page caching for better performance
  • Wordfence Security – Security scanning and firewall
  • UpdraftPlus – Additional backup option
  • Yoast SEO – Search engine optimization

Configuration Tweaks:

// wp-config.php additions
define('WP_CACHE', true);
define('COMPRESS_CSS', true);
define('COMPRESS_SCRIPTS', true);
define('CONCATENATE_SCRIPTS', true);
define('ENFORCE_GZIP', true);

Nginx Optimizations

Added caching headers for static assets:

location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

MySQL Tuning

Adjusted MySQL configuration for better performance:

[mysqld]
innodb_buffer_pool_size = 512M
max_connections = 100
query_cache_size = 0
query_cache_type = 0

Part 7: Monitoring & Maintenance

Daily Monitoring

Quick Health Check:

# Check all containers running
docker compose ps

# Check logs for errors
docker compose logs --tail=100 | grep -i error

# Check disk space
df -h

Weekly Tasks

  1. Review WordPress updates – Check dashboard for plugin/theme updates
  2. Check SSL expiry – Certbot should auto-renew, but verify
  3. Review access logs – Check for unusual activity
  4. Database backup – Automated with weekly backup system

Monthly Tasks

  1. Update Docker images – Pull latest stable versions
  2. Security scan – Run Wordfence deep scan
  3. Performance review – Check page load times
  4. Backup verification – Test restore from backup

Lessons Learned

What Went Well

  1. Docker Compose: Made the entire stack manageable and reproducible
  2. Separation of Concerns: Each service in its own container simplified troubleshooting
  3. Environment Variables: Kept secrets out of version control
  4. Health Checks: Prevented startup race conditions
  5. Documentation: Writing everything down saved hours later

What I’d Do Differently

  1. Start with Backups: I added automated backups later; should have been day one
  2. Monitoring Earlier: Wished I’d set up monitoring before going live
  3. Staging Environment: Would have been useful for testing updates
  4. Better Logging: Should have configured centralized logging from the start

Unexpected Challenges

  1. FastCGI Configuration: Took several iterations to get right
  2. Volume Permissions: File ownership between containers required attention
  3. Certificate Initial Setup: The chicken-and-egg problem with SSL
  4. Network Between Containers: Understanding Docker networking took time

The Results

After several days of configuration, testing, and optimization:

Performance:

  • Page load time: < 1 second
  • 99.9% uptime since launch
  • A+ SSL rating

Security:

  • Strong TLS configuration
  • Firewall configured
  • Security plugins active
  • Regular updates

Maintainability:

  • Single docker-compose command to update
  • Automated SSL renewal
  • Automated backups (weekly)
  • Comprehensive documentation

What’s Next?

With the web server foundation solid, the next challenge was email. In Part 2, I’ll cover:

  • Setting up a full-featured mail server with docker-mailserver
  • Configuring SMTP, IMAP, DKIM, SPF, and DMARC
  • Integrating PostfixAdmin for email account management
  • Troubleshooting network and authentication issues
  • Dealing with Fail2Ban blocking legitimate traffic
  • The satisfying moment when the first email arrives

Resources & References

Official Documentation:

Helpful Tools:

My Repository:

  • Full docker-compose.yml and configurations available
  • Detailed documentation in docs/ directory
  • Backup and restore scripts included

Conclusion

Building a self-hosted WordPress website with Docker was both challenging and incredibly rewarding. I now have:

  • Complete control over my web presence
  • Deep understanding of how modern web infrastructure works
  • Scalable foundation for adding more services (like email!)
  • Skills transferable to professional DevOps work

The best part? Everything is reproducible. If my server dies tomorrow, I can rebuild the entire stack from my docker-compose.yml and restore from backups.

Total time invested: ~3 days for initial setup + ongoing maintenance

Worth it? Absolutely. The learning experience alone was invaluable, and having complete ownership of my data is priceless.

In the next post, I’ll dive into the real challenge: building a production-ready mail server. Email is notoriously difficult to self-host, and I learned that the hard way…


Stay tuned for Part 2: Building the Mail Server – DKIM, Dovecot, and Debugging

Questions or feedback? Feel free to reach out!


Published: November 2025
Author: Frank @ BlueElysium
Series: Building a Self-Hosted Web & Mail Server
Part: 1 of 2

Leave a Reply

Your email address will not be published. Required fields are marked *