{"id":1653,"date":"2025-11-10T21:51:22","date_gmt":"2025-11-10T18:51:22","guid":{"rendered":"https:\/\/www.dchost.com\/blog\/wordpress-on-docker-compose-without-the-drama-nginx-mariadb-redis-persistent-volumes-auto%e2%80%91backups-and-a-calm-update-flow\/"},"modified":"2025-11-10T21:51:22","modified_gmt":"2025-11-10T18:51:22","slug":"wordpress-on-docker-compose-without-the-drama-nginx-mariadb-redis-persistent-volumes-auto%e2%80%91backups-and-a-calm-update-flow","status":"publish","type":"post","link":"https:\/\/www.dchost.com\/blog\/en\/wordpress-on-docker-compose-without-the-drama-nginx-mariadb-redis-persistent-volumes-auto%e2%80%91backups-and-a-calm-update-flow\/","title":{"rendered":"WordPress on Docker Compose, Without the Drama: Nginx, MariaDB, Redis, Persistent Volumes, Auto\u2011Backups, and a Calm Update Flow"},"content":{"rendered":"<div class=\"dchost-blog-content-wrapper\"><p>So there I was, staring at yet another WordPress site that had quietly grown from a simple blog into a real business. Traffic was good. Sales were happening. The owner was excited. And then, predictably, came the question: \u201cCan you make it run like a proper system? I don\u2019t want surprises.\u201d I smiled because I\u2019ve been there\u2014sites stitched together with a mix of shared hosting, plugin updates at 2 a.m., and backups that <em>might<\/em> work if we\u2019re lucky. Here\u2019s the thing: it doesn\u2019t have to be that way.<\/p>\n<p>If you\u2019ve ever wanted WordPress to feel solid\u2014like a little service running in your own private cloud\u2014Docker Compose is your friend. It lets you spin up Nginx, PHP\u2011FPM, MariaDB, and Redis as small, predictable pieces that fit together cleanly. Add persistent volumes so nothing gets lost, schedule backups, and use a sane update flow, and suddenly WordPress stops being a stress magnet.<\/p>\n<p>In this guide, we\u2019ll build a friendly, production\u2011tinted stack: Nginx in front, WordPress (PHP\u2011FPM) in the middle, MariaDB for data, Redis for speed, volumes for persistence, auto\u2011backups that actually run, and an update flow that won\u2019t make your palms sweat. I\u2019ll show you code, but more importantly I\u2019ll share the little choices that keep this simple and calm.<\/p>\n<div id=\"toc_container\" class=\"toc_transparent no_bullets\"><p class=\"toc_title\">\u0130&ccedil;indekiler<\/p><ul class=\"toc_list\"><li><a href=\"#Why_Docker_Compose_for_WordPress_just_works\"><span class=\"toc_number toc_depth_1\">1<\/span> Why Docker Compose for WordPress just\u2026 works<\/a><\/li><li><a href=\"#The_simple_mental_model_whats_persistent_and_whats_ephemeral\"><span class=\"toc_number toc_depth_1\">2<\/span> The simple mental model: what\u2019s persistent and what\u2019s ephemeral<\/a><\/li><li><a href=\"#Lets_write_docker-composeyml_friendly_but_productionaware\"><span class=\"toc_number toc_depth_1\">3<\/span> Let\u2019s write docker-compose.yml (friendly, but production\u2011aware)<\/a><\/li><li><a href=\"#Nginx_that_plays_nice_with_PHPFPM_and_doesnt_get_in_the_way\"><span class=\"toc_number toc_depth_1\">4<\/span> Nginx that plays nice with PHP\u2011FPM (and doesn\u2019t get in the way)<\/a><\/li><li><a href=\"#MariaDB_and_Redis_fast_where_it_matters_durable_where_it_must\"><span class=\"toc_number toc_depth_1\">5<\/span> MariaDB and Redis: fast where it matters, durable where it must<\/a><\/li><li><a href=\"#Persistent_volumes_explained_like_were_on_a_whiteboard\"><span class=\"toc_number toc_depth_1\">6<\/span> Persistent volumes, explained like we\u2019re on a whiteboard<\/a><\/li><li><a href=\"#Autobackups_you_can_actually_trust\"><span class=\"toc_number toc_depth_1\">7<\/span> Auto\u2011backups you can actually trust<\/a><\/li><li><a href=\"#A_friendly_lowrisk_update_flow\"><span class=\"toc_number toc_depth_1\">8<\/span> A friendly, low\u2011risk update flow<\/a><\/li><li><a href=\"#What_can_go_wrong_and_how_I_recover_without_drama\"><span class=\"toc_number toc_depth_1\">9<\/span> What can go wrong (and how I recover without drama)<\/a><\/li><li><a href=\"#Security_and_stability_touches_that_matter\"><span class=\"toc_number toc_depth_1\">10<\/span> Security and stability touches that matter<\/a><\/li><li><a href=\"#A_tiny_performance_sprinkle_only_when_it_pays\"><span class=\"toc_number toc_depth_1\">11<\/span> A tiny performance sprinkle, only when it pays<\/a><\/li><li><a href=\"#WPCLI_quick_wins_youll_use_all_the_time\"><span class=\"toc_number toc_depth_1\">12<\/span> WP\u2011CLI quick wins you\u2019ll use all the time<\/a><\/li><li><a href=\"#Putting_it_all_together_and_where_to_go_next\"><span class=\"toc_number toc_depth_1\">13<\/span> Putting it all together (and where to go next)<\/a><\/li><li><a href=\"#Stepbystep_your_first_deployment_checklist\"><span class=\"toc_number toc_depth_1\">14<\/span> Step\u2011by\u2011step: your first deployment checklist<\/a><\/li><li><a href=\"#Wrapup_the_calm_repeatable_way_to_run_WordPress\"><span class=\"toc_number toc_depth_1\">15<\/span> Wrap\u2011up: the calm, repeatable way to run WordPress<\/a><\/li><\/ul><\/div>\n<h2 id=\"section-1\"><span id=\"Why_Docker_Compose_for_WordPress_just_works\">Why Docker Compose for WordPress just\u2026 works<\/span><\/h2>\n<p>I used to run WordPress directly on the host. It\u2019s fine\u2014until it isn\u2019t. One day you\u2019re upgrading PHP and breaking extensions, the next day you\u2019re untangling permissions. With Docker Compose, you describe your world declaratively and press \u201cgo.\u201d Think of it like packing cubes in a suitcase: one for the web server, one for PHP, one for the database, one for caching. If something breaks, you replace the cube, not the suitcase.<\/p>\n<p>What I love about Compose is that it mirrors how you think. You say \u201cNginx talks to PHP\u2011FPM here,\u201d \u201cMariaDB stores data here,\u201d \u201cRedis caches things over there,\u201d \u201cthis folder holds uploads forever,\u201d and Compose wires it up consistently every time. You get three crucial things: a repeatable environment, clear boundaries, and a small, obvious surface for automation\u2014like scheduled backups.<\/p>\n<p>If you\u2019ve never touched it, the <a href=\"https:\/\/docs.docker.com\/compose\/\" rel=\"nofollow noopener\" target=\"_blank\">Docker Compose quickstart<\/a> is a nice primer. We\u2019ll go deeper, but that page helps you recognize the moving parts.<\/p>\n<h2 id=\"section-2\"><span id=\"The_simple_mental_model_whats_persistent_and_whats_ephemeral\">The simple mental model: what\u2019s persistent and what\u2019s ephemeral<\/span><\/h2>\n<p>Before we open any editor, let\u2019s get one decision right: what needs to persist?<\/p>\n<p>The durable stuff is easy to name. Your MariaDB data\u2014posts, users, settings\u2014must persist. Your WordPress uploads\u2014images, PDFs, media\u2014must persist. Your plugins and themes should persist too, because site owners update them from the dashboard. Everything else can be ephemeral. Nginx containers can come and go. PHP\u2011FPM containers can be replaced on updates. Redis can be either; for WordPress object caching I often treat Redis as expendable (it repopulates), but I\u2019ll show you a persistent option if you want AOF or RDB for resilience.<\/p>\n<p>So the plan is: persistent volumes for MariaDB and wp-content. Optional persistence for Redis. Read\u2011only configuration mounts for Nginx and PHP ini files. And a backup volume to hold compressed archives while we sync them off\u2011box.<\/p>\n<h2 id=\"section-3\"><span id=\"Lets_write_docker-composeyml_friendly_but_productionaware\">Let\u2019s write docker-compose.yml (friendly, but production\u2011aware)<\/span><\/h2>\n<p>Here\u2019s a Compose file I\u2019ve used many times. It\u2019s trim, but it has the little things that make life easier: healthchecks, a stable network, named volumes, and clear mounts for configuration and content.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">version: &quot;3.9&quot;\n\nservices:\n  nginx:\n    image: nginx:1.25-alpine\n    depends_on:\n      wordpress:\n        condition: service_healthy\n    ports:\n      - &quot;80:80&quot;\n      # In production, you\u2019ll terminate TLS on 443 here\n      # - &quot;443:443&quot;\n    volumes:\n      - .\/nginx\/conf.d:\/etc\/nginx\/conf.d:ro\n      - .\/nginx\/nginx.conf:\/etc\/nginx\/nginx.conf:ro\n      - wp_content:\/var\/www\/html\/wp-content:ro\n      # Optional: if you manage full core as a volume, mount .\/html too\n      # - .\/html:\/var\/www\/html\n    networks:\n      - wp\n    healthcheck:\n      test: [&quot;CMD&quot;, &quot;wget&quot;, &quot;-qO-&quot;, &quot;http:\/\/localhost&quot;]\n      interval: 30s\n      timeout: 5s\n      retries: 3\n\n  wordpress:\n    image: wordpress:php8.2-fpm\n    environment:\n      WORDPRESS_DB_HOST: db:3306\n      WORDPRESS_DB_USER: wpuser\n      WORDPRESS_DB_PASSWORD: supersecret\n      WORDPRESS_DB_NAME: wpdb\n      # Optional hardening\n      PHP_MEMORY_LIMIT: 256M\n    volumes:\n      - wp_content:\/var\/www\/html\/wp-content\n      - .\/php\/uploads.ini:\/usr\/local\/etc\/php\/conf.d\/uploads.ini:ro\n    depends_on:\n      db:\n        condition: service_healthy\n      redis:\n        condition: service_started\n    networks:\n      - wp\n    healthcheck:\n      test: [&quot;CMD-SHELL&quot;, &quot;php-fpm-healthcheck || exit 1&quot;]\n      interval: 30s\n      timeout: 5s\n      retries: 5\n\n  db:\n    image: mariadb:10.11\n    environment:\n      MARIADB_DATABASE: wpdb\n      MARIADB_USER: wpuser\n      MARIADB_PASSWORD: supersecret\n      MARIADB_ROOT_PASSWORD: rootsecret\n    command: [&quot;--character-set-server=utf8mb4&quot;, &quot;--collation-server=utf8mb4_unicode_ci&quot;]\n    volumes:\n      - db_data:\/var\/lib\/mysql\n    networks:\n      - wp\n    healthcheck:\n      test: [&quot;CMD&quot;, &quot;mysqladmin&quot;, &quot;ping&quot;, &quot;-h&quot;, &quot;localhost&quot;]\n      interval: 10s\n      timeout: 5s\n      retries: 10\n\n  redis:\n    image: redis:7-alpine\n    command: [&quot;redis-server&quot;, &quot;--appendonly&quot;, &quot;yes&quot;]\n    # If you prefer Redis as ephemeral cache only, drop the volume and AOF\n    volumes:\n      - redis_data:\/data\n    networks:\n      - wp\n\n  backup:\n    image: alpine:3.20\n    depends_on:\n      db:\n        condition: service_healthy\n    volumes:\n      - db_data:\/var\/lib\/mysql:ro\n      - wp_content:\/data\/wp-content:ro\n      - backups:\/backups\n      - .\/backup\/run.sh:\/backup\/run.sh:ro\n      - .\/backup\/crontab:\/etc\/crontabs\/root:ro\n    environment:\n      DB_HOST: db\n      DB_NAME: wpdb\n      DB_USER: wpuser\n      DB_PASSWORD: supersecret\n      RETENTION_DAYS: 14\n    command: [&quot;\/bin\/sh&quot;, &quot;-c&quot;, &quot;apk add --no-cache mariadb-client tzdata &gt; \/dev\/null &amp;&amp; crond -f -l 8&quot;]\n    networks:\n      - wp\n\nnetworks:\n  wp:\n    driver: bridge\n\nvolumes:\n  db_data:\n  wp_content:\n  redis_data:\n  backups:\n<\/code><\/pre>\n<p>A couple of quick notes. First, I\u2019m using the <a href=\"https:\/\/hub.docker.com\/_\/wordpress\" rel=\"nofollow noopener\" target=\"_blank\">official WordPress Docker image<\/a> in the FPM flavor. That means Nginx will talk to PHP\u2011FPM over port 9000, and WordPress files are in \/var\/www\/html. Second, by mounting only wp-content, we let the container provide core files while still keeping plugins, themes, and uploads persistent. It\u2019s a sweet spot: you get clean updates with minimal risk of drift. Third, the backup service is just a tiny Alpine container running crond, which makes it dead simple to schedule daily dumps and tarballs.<\/p>\n<h2 id=\"section-4\"><span id=\"Nginx_that_plays_nice_with_PHPFPM_and_doesnt_get_in_the_way\">Nginx that plays nice with PHP\u2011FPM (and doesn\u2019t get in the way)<\/span><\/h2>\n<p>I try to keep the Nginx config boring. If a future version of Nginx changes something, boring configs don\u2019t break. This one is compact and friendly, with the usual WordPress rewrites and a well\u2011behaved fastcgi handler.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># .\/nginx\/nginx.conf\nuser  nginx;\nworker_processes  auto;\nerror_log  \/var\/log\/nginx\/error.log warn;\npid        \/var\/run\/nginx.pid;\n\nevents {\n  worker_connections  1024;\n}\n\nhttp {\n  include       \/etc\/nginx\/mime.types;\n  default_type  application\/octet-stream;\n  sendfile      on;\n  keepalive_timeout  65;\n  gzip on;\n  gzip_types text\/plain text\/css application\/json application\/javascript text\/xml application\/xml application\/xml+rss text\/javascript;\n\n  include \/etc\/nginx\/conf.d\/*.conf;\n}\n<\/code><\/pre>\n<pre class=\"language-nginx line-numbers\"><code class=\"language-nginx\"># .\/nginx\/conf.d\/site.conf\nserver {\n  listen 80;\n  server_name _;\n\n  root \/var\/www\/html;\n  index index.php index.html index.htm;\n\n  location \/ {\n    try_files $uri $uri\/ \/index.php?$args;\n  }\n\n  location ~ .(png|jpg|jpeg|gif|ico|svg|css|js|webp|avif)$ {\n    access_log off;\n    expires 30d;\n    add_header Cache-Control &quot;public, immutable&quot;;\n  }\n\n  location ~ .php$ {\n    try_files $uri =404;\n    fastcgi_pass wordpress:9000;\n    fastcgi_index index.php;\n    include fastcgi_params;\n    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n  }\n\n  client_max_body_size 32m; # adjust for media uploads\n}\n<\/code><\/pre>\n<p>In production you\u2019ll likely terminate TLS in this same container or behind a load balancer. I keep certs out of the container image and mount them as secrets or files. If you need a refresher on lean HTTPS choices, I wrote a long, friendly guide to certificate types and when to use them, but we\u2019ll stay focused on Compose here.<\/p>\n<h2 id=\"section-5\"><span id=\"MariaDB_and_Redis_fast_where_it_matters_durable_where_it_must\">MariaDB and Redis: fast where it matters, durable where it must<\/span><\/h2>\n<p>Let\u2019s talk data shape. MariaDB is your crown jewels\u2014posts, users, settings\u2014the irreplaceable bits. Redis is your assistant\u2014fast, helpful, but replaceable in a pinch. That mental model makes a lot of decisions easier.<\/p>\n<p>MariaDB lives on a named volume called db_data. That\u2019s the disk you back up every day. In the Compose file above, I\u2019m setting utf8mb4 out of the gate to keep emojis and odd characters happy. It costs nothing and saves some pain later. The healthcheck nudges Compose to start services in a sane order.<\/p>\n<p>Redis is there to handle WordPress object caching. The trick to making Redis useful in WordPress is the plugin that wires things up. I usually install the <a href=\"https:\/\/wordpress.org\/plugins\/redis-cache\/\" rel=\"nofollow noopener\" target=\"_blank\">Redis Object Cache plugin<\/a>, enable it, and point it at redis:6379. In many stacks I leave Redis completely ephemeral and let it repopulate; simple is strong. If you run an unusually heavy admin backend or expensive queries, turning on AOF (as shown) plus a redis_data volume gives you a little more continuity through restarts.<\/p>\n<p>Inside WordPress, you can drop this into wp-config.php to enable object cache cleanly:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">define('WP_REDIS_HOST', 'redis');\ndefine('WP_REDIS_PORT', 6379);\n\/\/ Optional: separate cache prefixes per environment\n\/\/ define('WP_CACHE_KEY_SALT', 'mysite:');\n\/\/ Turn on WordPress-level caching\ndefine('WP_CACHE', true);\n<\/code><\/pre>\n<p>In my experience, this setup keeps page rendering snappy without getting fancy. If you want to push further, microcaching at Nginx can be magical for anonymous visitors, but that\u2019s a story for another day.<\/p>\n<h2 id=\"section-6\"><span id=\"Persistent_volumes_explained_like_were_on_a_whiteboard\">Persistent volumes, explained like we\u2019re on a whiteboard<\/span><\/h2>\n<p>I get asked a lot: \u201cDo I need to mount the whole \/var\/www\/html or just wp-content?\u201d My rule of thumb is simple. If you manage WordPress core via Docker images (which you do here), mounting only wp-content is the cleanest path. Your core files come from a known image, your plugins\/themes\/uploads persist, and you can update without dragging a pile of drift along with you.<\/p>\n<p>That\u2019s what the wp_content named volume is doing. Docker stores it wherever your engine keeps volumes, and Compose attaches it to both Nginx (read\u2011only) and WordPress (read\u2011write). You could switch to a bind mount (.\/data\/wp-content:\/var\/www\/html\/wp-content) if you prefer to see files on the host. I\u2019ve done both, but named volumes are wonderfully boring\u2014you don\u2019t trip over host permissions as much.<\/p>\n<p>For MariaDB, don\u2019t overthink it. A named volume mapped to \/var\/lib\/mysql keeps the data on disk across container recreations. If you ever restore from backup, you\u2019ll stop containers, wipe that directory (carefully), and import your dump into a fresh database.<\/p>\n<p>Redis is optional for persistence. If you expect restarts to be rare and don\u2019t mind a warmup period, skip the volume. If you like belt\u2011and\u2011suspenders reliability, keep AOF on and store \/data on redis_data.<\/p>\n<h2 id=\"section-7\"><span id=\"Autobackups_you_can_actually_trust\">Auto\u2011backups you can actually trust<\/span><\/h2>\n<p>This is the part that used to keep me up at night. A backup that exists is one thing. A backup you\u2019ve restored from is the only thing that counts. The nice thing about Compose is you can turn backups into a tiny sidecar container with a cron schedule, and it just hums along.<\/p>\n<p>Let\u2019s wire a small script and crontab into that backup service.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># .\/backup\/run.sh\n#!\/bin\/sh\nset -eu\n\nSTAMP=$(date +&quot;%Y%m%d-%H%M%S&quot;)\nBACKUP_DIR=\/backups\/$STAMP\nmkdir -p &quot;$BACKUP_DIR&quot;\n\n# Database dump\nmysqldump -h &quot;$DB_HOST&quot; -u &quot;$DB_USER&quot; -p&quot;$DB_PASSWORD&quot; --single-transaction --routines --events &quot;$DB_NAME&quot; \n  | gzip -9 &gt; &quot;$BACKUP_DIR\/db.sql.gz&quot;\n\n# wp-content archive (plugins, themes, uploads)\nmkdir -p \/tmp\/backup\ncd \/data\n tar -czf &quot;$BACKUP_DIR\/wp-content.tgz&quot; wp-content\n\n# Optional: keep a small manifest for sanity checks\ncat &lt;&lt;EOF &gt; &quot;$BACKUP_DIR\/manifest.txt&quot;\nDATE=$STAMP\nDB_NAME=$DB_NAME\nFILES=wp-content.tgz\nEOF\n\n# Retention policy\nfind \/backups -maxdepth 1 -type d -mtime +&quot;${RETENTION_DAYS:-14}&quot; -exec rm -rf {} + 2&gt;\/dev\/null || true\n\necho &quot;Backup completed: $BACKUP_DIR&quot;\n<\/code><\/pre>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># .\/backup\/crontab\n# Run at 03:17 every day\n17 3 * * * \/bin\/sh \/backup\/run.sh &gt;\/proc\/1\/fd\/1 2&gt;&amp;1\n<\/code><\/pre>\n<p>The first time I switched to a cron container like this, I felt silly for not doing it sooner. No dependencies on the host. No mystery systemd timers. Everything is versioned with your stack. If you want offsite copies (please do), you can layer in a sync step\u2014rclone or your S3 CLI of choice\u2014right after the files are written. Keep credentials in environment variables or Docker secrets, not in the repo.<\/p>\n<p>Equally important: test restore once. Stop the stack, start only MariaDB and a throwaway container, import db.sql.gz into a fresh database, and untar wp-content into a temp directory. You\u2019re not aiming for a full cutover here; you just want the confidence that the files are valid and the dump opens.<\/p>\n<p>If you want a bigger picture on recovery planning, I wrote about <a href=\"https:\/\/www.dchost.com\/blog\/en\/felaket-kurtarma-plani-nasil-yazilir-rto-rpoyu-kafada-netlestirip-yedek-testleri-ve-runbooklari-gercekten-calisir-hale-getirmek\/\">my calm, no\u2011drama approach to a DR plan with real backup tests<\/a>. It pairs nicely with an automatic backup like this.<\/p>\n<h2 id=\"section-8\"><span id=\"A_friendly_lowrisk_update_flow\">A friendly, low\u2011risk update flow<\/span><\/h2>\n<p>Updates are where many WordPress deployments wobble. There are two kinds to think about: container updates (images for Nginx, WordPress FPM, MariaDB, Redis) and in\u2011app updates (plugins, themes, WordPress core from inside the dashboard). I like to keep container updates under version control and plugin\/theme updates inside WordPress, because that\u2019s where owners expect them to live.<\/p>\n<p>Here\u2019s a safe, repeatable rhythm I use:<\/p>\n<p>First, pull and roll the containers. On a maintenance window\u2014or honestly, any calm afternoon\u2014run:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">docker compose pull nginx wordpress db redis\ndocker compose up -d\n<\/code><\/pre>\n<p>Because we\u2019re not storing irreplaceable data in the stateless services, this is painless. The tricky one is MariaDB; I only upgrade DB versions after reading release notes and planning a short maintenance window. For minor updates within the same major series, it\u2019s usually smooth.<\/p>\n<p>Second, lock in application updates with WP\u2011CLI. I try to use WP\u2011CLI from inside the WordPress container so paths and PHP match exactly. This command checks what\u2019s available:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">docker compose exec wordpress wp core check-update\n<\/code><\/pre>\n<p>And these will nudge everything forward:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Minor core updates only (safe):\ndocker compose exec wordpress wp core update --minor\n\n# Plugin updates:\ndocker compose exec wordpress wp plugin update --all\n\n# Theme updates:\ndocker compose exec wordpress wp theme update --all\n<\/code><\/pre>\n<p>Before doing any of the above, I like to take an on\u2011demand backup with our backup container, just in case today is the day a plugin decides to be dramatic:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">docker compose run --rm backup \/bin\/sh \/backup\/run.sh\n<\/code><\/pre>\n<p>If you want even more safety, do a quick blue\/green dance using Compose\u2019s project flag. Spin a staging copy on a different project name (and a different host port), test there, then promote:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Clone your repo into a parallel directory and switch to it\n# then run a short-lived staging project\n\n# In the staging directory:\ndocker compose -p wpstage up -d\n\n# Point your hosts file to test, or visit http:\/\/&lt;server-ip&gt;:&lt;staging-port&gt;\n# When you're happy, destroy staging and update prod images\n\ndocker compose -p wpstage down\n# Back in prod directory:\ndocker compose pull\ndocker compose up -d\n<\/code><\/pre>\n<p>It\u2019s nothing fancy, but it gives you room to breathe. And when something goes sideways, you\u2019re a single compose down\/up away from a clean state.<\/p>\n<h2 id=\"section-9\"><span id=\"What_can_go_wrong_and_how_I_recover_without_drama\">What can go wrong (and how I recover without drama)<\/span><\/h2>\n<p>One of my clients once called me from the road: \u201cWe updated a plugin and now every page is white.\u201d Classic. We\u2019d done our homework though, so it was a five\u2011minute fix. First we took an on\u2011demand backup (never hurts). Then we disabled the plugin from WP\u2011CLI:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">docker compose exec wordpress wp plugin deactivate the-bad-plugin\n<\/code><\/pre>\n<p>Pages came back, we rolled the plugin back later. The point isn\u2019t magic; it\u2019s having a predictable toolbox.<\/p>\n<p>If a database update misbehaves, I\u2019ll restore from the last known\u2011good dump. The flow looks like this:<\/p>\n<pre class=\"language-python line-numbers\"><code class=\"language-python\"># Stop the stack\ndocker compose down\n\n# Start only DB in the background\ndocker compose up -d db\n\n# Create a temporary import container\ncat backups\/&lt;STAMP&gt;\/db.sql.gz | gunzip | \n  docker compose exec -T db mysql -u wpuser -p&quot;supersecret&quot; wpdb\n\n# Bring the rest back up\ndocker compose up -d\n<\/code><\/pre>\n<p>If uploads or plugin files were damaged\u2014and this is rare\u2014I\u2019d replace wp-content entirely from the wp-content.tgz archive. It\u2019s a blunt instrument, but it works.<\/p>\n<p>The other failure I see is \u201cIt worked yesterday, but now we get random 502s.\u201d That\u2019s usually a container died and restarted. Healthchecks help here, but logs tell you the truth. I try Nginx first:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">docker compose logs --tail=200 nginx\n<\/code><\/pre>\n<p>If it\u2019s clean, PHP\u2011FPM next:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">docker compose logs --tail=200 wordpress\n<\/code><\/pre>\n<p>Nine times out of ten, the fix is plain\u2014bad plugin update, missing PHP extension, or memory limit too low. Bump PHP memory to 256M or 512M, recycle the container, and life goes on.<\/p>\n<h2 id=\"section-10\"><span id=\"Security_and_stability_touches_that_matter\">Security and stability touches that matter<\/span><\/h2>\n<p>I won\u2019t turn this into a hardening manifesto, but a few gentle touches go a long way. First, don\u2019t bake secrets into your docker-compose.yml. Use environment files or Docker secrets, especially for DB passwords and offsite backup credentials. Second, keep Nginx and WordPress images moving; stale images often mean stale security patches. Third, permissions: let the container write to wp-content and nowhere else. Mount everything else read\u2011only when possible. Fourth, rate limit login endpoints at Nginx if your site gets hammered; it\u2019s a calm way to push back on brute force without exhausting PHP.<\/p>\n<p>For teams that like stronger walls around their admin panels, client certificates (mTLS) on a separate admin subdomain can be a nice upgrade, but that\u2019s a separate pattern. The core idea is the same: keep simple things simple, and make secure things boring so everyone trusts them.<\/p>\n<h2 id=\"section-11\"><span id=\"A_tiny_performance_sprinkle_only_when_it_pays\">A tiny performance sprinkle, only when it pays<\/span><\/h2>\n<p>WordPress runs surprisingly well on this stack without extra tricks. Redis object caching gives you a nice lift. Nginx handles static assets efficiently. FPM can breathe with a reasonable pm.max_children. I don\u2019t rush into full page caching or CDN setups until traffic warrants it. The day you see high anonymous traffic and the DB getting too chatty, you can introduce microcaching at Nginx, or use a CDN for images and static files. But don\u2019t let perf tuning distract you from backups and updates\u2014that\u2019s the part that bites when neglected.<\/p>\n<h2 id=\"section-12\"><span id=\"WPCLI_quick_wins_youll_use_all_the_time\">WP\u2011CLI quick wins you\u2019ll use all the time<\/span><\/h2>\n<p>When you get comfortable with WP\u2011CLI inside the container, it becomes your Swiss Army knife.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Clear Redis object cache if needed\ndocker compose exec wordpress wp redis flush\n\n# Create a new admin user quickly\ndocker compose exec wordpress wp user create jane jane@example.com --role=administrator --user_pass='TempPass123!'\n\n# Search and replace after a domain change (test with --dry-run first)\ndocker compose exec wordpress wp search-replace 'http:\/\/old.example.com' 'https:\/\/new.example.com' --all-tables\n<\/code><\/pre>\n<p>Little commands like these shrink big problems. And they make \u201cI\u2019ll just fix it\u201d a five\u2011minute reality instead of a night of guessing.<\/p>\n<h2 id=\"section-13\"><span id=\"Putting_it_all_together_and_where_to_go_next\">Putting it all together (and where to go next)<\/span><\/h2>\n<p>We\u2019ve covered a lot, but the flow is wonderfully straightforward when you live with it for a week. Docker Compose lays out a clean map of your world: Nginx in front, PHP\u2011FPM serving WordPress, MariaDB holding the state, Redis speeding up the chatter. Persistent volumes protect your uploads and database. A small cron container takes reliable backups. Updates happen with a single pull and a few WP\u2011CLI commands. And when you need to roll back, you can\u2014quickly, calmly, without drama.<\/p>\n<p>If you want to explore more Docker\u2011centric WordPress patterns and the quiet little habits that keep a <a href=\"https:\/\/www.dchost.com\/vps\">VPS<\/a> happy, I\u2019ve written a broader playbook that complements this stack nicely. It leans into the same ideas\u2014predictability, boring automation, and repeatable updates\u2014and shows how to grow the setup without tripping over it.<\/p>\n<h2 id=\"section-14\"><span id=\"Stepbystep_your_first_deployment_checklist\">Step\u2011by\u2011step: your first deployment checklist<\/span><\/h2>\n<p>Here\u2019s the flow I give friends when they\u2019re spinning up their first Compose\u2011based WordPress. It\u2019s not a formal checklist\u2014more like a friendly trombone of steps you can hum through.<\/p>\n<p>First, clone your repo to the server, create the directories (nginx, php, backup), and drop the configs we used above into place. Second, bring up the stack with docker compose up -d and watch logs until the site responds. Third, visit the WordPress installer, complete the setup, and immediately install and activate the Redis Object Cache plugin. Fourth, run your first backup manually to prime the directory and be sure it succeeds. Fifth, schedule your offsite sync and test restore once, even if it\u2019s just on a throwaway VM. Sixth, set a monthly reminder to run updates, or better yet, do them on a quiet Friday morning while your coffee is still hot.<\/p>\n<p>When you hit a speed bump\u2014and you will at some point\u2014remember the mental model: database and uploads are the gold; everything else is replaceable. Start with logs, use WP\u2011CLI for surgical fixes, and keep your changes small. You\u2019ll develop a feel for this that no guide can give you, and that\u2019s when it really becomes fun.<\/p>\n<h2 id=\"section-15\"><span id=\"Wrapup_the_calm_repeatable_way_to_run_WordPress\">Wrap\u2011up: the calm, repeatable way to run WordPress<\/span><\/h2>\n<p>There\u2019s a moment I love when someone logs into their site after we switch to this stack. They click around and say, \u201cHuh\u2014it just feels solid.\u201d That\u2019s Docker Compose doing its quiet work. Separate services, clear boundaries, persistent volumes where it matters, and little scripts that run like clockwork. Pair that with Redis for zip, a cron container for backups, and a friendly update routine, and WordPress stops being a moving target and starts being a stable part of your week.<\/p>\n<p>My advice if you\u2019re just getting into this: start small and boring. Use the official images. Keep configuration tidy and versioned. Test your backups once. Learn two or three WP\u2011CLI commands by heart. And don\u2019t be afraid to nudge things forward in little steps rather than big leaps. You\u2019ll keep your uptime, your sleep, and your sanity.<\/p>\n<p>Hope this was helpful. If you got stuck on a specific piece, take a breath, skim the logs, and keep the mental model close by. You\u2019ve got this\u2014and your site will feel better for it.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>So there I was, staring at yet another WordPress site that had quietly grown from a simple blog into a real business. Traffic was good. Sales were happening. The owner was excited. And then, predictably, came the question: \u201cCan you make it run like a proper system? I don\u2019t want surprises.\u201d I smiled because I\u2019ve [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1654,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26],"tags":[],"class_list":["post-1653","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-teknoloji"],"_links":{"self":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/1653","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/comments?post=1653"}],"version-history":[{"count":0,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/1653\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media\/1654"}],"wp:attachment":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media?parent=1653"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/categories?post=1653"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/tags?post=1653"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}