Oops, something went wrong! Site admins have been notified.

In Part 7: Cron + Automation we wired up the scheduled work that keeps the site healthy — cron, feed imports, off-site backups. Everything up to now has run on your laptop at localhost:8081. This post is about the last mile: taking it live so the rest of the world can reach it.

This is the part people are most nervous about, and it's worth saying up front: hosting a Drupal site is a solved problem. You have real choices, the trade-offs are clear, and the way we do it is reproducible enough that a stack delete plus redeploy rebuilds the whole site from git. This post stays conceptual — it's a map of the territory, not a DevOps course.

Three ways to host a Drupal site

Before our specific setup, here's the lay of the land. There are three realistic paths, and the right one depends on your budget, your team, and how much you want to manage yourself.

  • Managed Drupal (Pantheon, Acquia, Platform.sh) — you hand them your code and they run everything: servers, backups, scaling, security. Roughly $50–500/month. Lowest skill needed, highest cost. Best for agencies and compliance-heavy clients who want someone else on the hook.
  • VPS + Docker (a $6–50/month box at DigitalOcean, Hetzner, etc. running a Compose stack) — you rent one server and run the containers yourself. Medium skill, low cost. Best for small teams and cost-sensitive projects under ~100 daily visitors.
  • Self-managed Docker Swarm (a cluster behind Portainer + Traefik) — you run your own multi-site infrastructure. Higher skill, moderate cost. Best when you're hosting several sibling sites and want them to share one set of tools.

(There's a fourth path — old-school shared hosting with SFTP — but tight memory, lagging PHP versions, and no command-line access make it fragile for a real Drupal site. We skip it.)

Rule of thumb: under 100 daily visitors with low compliance needs, a VPS is plenty. Compliance plus a team, go managed. Several related sites, Swarm. We host more than one site, so we run Swarm.

How we deploy: Portainer + Docker Swarm

Chattanooga.Digital runs on Docker Swarm, managed through Portainer — a web UI for containers. Swarm is "Docker, but across a cluster," and Portainer is the dashboard you click instead of memorizing Swarm commands.

The local compose.yaml from Part 1 still describes the same three core services (database, PHP, web server), but production uses a different file — compose.production.yaml — with a few deliberate changes:

  • Named volumes instead of bind mounts. Bind mounts only work if the files happen to live on the exact node a container lands on; named volumes travel with the service across the cluster.
  • An init container. On every boot it clones the repo and populates the volumes, so the running site always reflects the latest main.
  • Traefik labels. A reverse proxy that routes public traffic to the right container and issues the HTTPS certificate automatically (more on that below).
  • Swarm secrets instead of .env values. Passwords like the database password live as encrypted Swarm secrets mounted at /run/secrets/ — never in a committed file or a log.

Deploying is a Portainer flow: Stacks → Add stack → Repository, point it at the GitHub repo and compose.production.yaml, paste in the environment variables, click Deploy the stack. A couple of minutes later the init container has populated the volumes and the site is up.

The magic part: SSL with no certbot

You never run certbot, never copy certificate files, never set a renewal reminder. Traefik watches for a container labeled with a hostname, asks Let's Encrypt for a certificate, Let's Encrypt validates over HTTP, and the cert is issued and stored — then renewed automatically before it expires. The whole chain is one router label in the compose file:

traefik.http.routers.chattanooga-prod.rule=Host(`www.chattanooga.digital`) || Host(`chattanooga.digital`)

That single line is the difference between "the certificate dance is a recurring chore" and "I never think about HTTPS again."

fix-settings: the service that patches every deploy

Here's the piece that makes a recipe-driven Drupal site deployable on these Wodby container images. The image rewrites settings.php on every boot from a template — handy, except it clobbers our database password and trusted-host config each time. Rather than fight that, we add a one-shot service called fix-settings that runs after the site comes up and reconciles everything that needs to be true after a fresh boot.

It runs the same canonical build sequence that make build runs locally, so dev and production stay in lockstep:

  1. composer update — pull security patches
  2. drush updatedb — apply any database schema updates
  3. drush recipe byte — the upstream base theme
  4. drush recipe chattanooga_digital — our content types, pages, calendar, forms
  5. drush php:script post_install.php — the cleanup that sets our homepage as the front page

On top of that build, fix-settings patches the database password back into settings.php, appends the trusted host patterns, configures SMTP so email actually sends, sets the site name and slogan from environment variables, restores our theme files (the Wodby image overwrites those too), and clears the cache. The whole thing is idempotent — running it again applies only what's missing and leaves existing content untouched.

That idempotence is the whole point. It's why the structure lives in recipes and not in a database dump: every deploy re-applies the recipe, so the live site can never silently drift from what's in git. (The flip side: never patch a live production site through the admin UI — those edits vanish on the next redeploy. Structural changes belong in the recipe.)

The orchestrator that runs all this lives in the repo as a set of small phase scripts under scripts/fix-settings/ — one per concern (settings, composer, recipe, SMTP, theme restore, and so on), sourced in order. Same scripts, both environments.

Staging → production, with a preview first

We run two separate Swarm clusters: staging (a disposable verification target that rebuilds 100% fresh from the recipe on every redeploy) and production (the real site). Every change lands on staging first; staging proves the recipe builds cleanly from scratch.

The cutover to production is two-phase, and the clever bit is the preview hostname. The production compose parameterizes its Traefik host rule through an environment variable, so we stand the production stack up on a temporary, already-resolving preview URL before the real DNS is pointed at it:

TRAEFIK_HOST_RULE=Host(`chattanooga-prod-preview.staging.chattanooga.digital`)

That gets us a real, Traefik-resolvable URL with a real Let's Encrypt cert to test against — the full test and accessibility suites run against it, the client reviews it, a contact form gets submitted to confirm email delivery. Only once everything's green do we point DNS at the production node and flip the env var back to its default:

TRAEFIK_HOST_RULE=Host(`www.chattanooga.digital`) || Host(`chattanooga.digital`)

Traefik picks up the new rule within seconds, Let's Encrypt issues the production cert on the first matching request, and the site is live at its real address. No blind cutover, no certificate scramble, no window where the public URL serves a half-built site. The full ordered checklist lives in docs/PRODUCTION_CHECKLIST.md.

What survives a redeploy

The reassuring part of all this discipline: deleting and recreating the stack is safe. The structure comes back from the recipe in git, the theme comes back from the init container's clone, and the content comes back from the newest weekly backup on GitHub Releases (with the committed snapshot as a fallback). The only way to lose content is to make database changes and never snapshot them — which is exactly why Part 7's backup cron exists.

What's next

The site is live, on HTTPS, rebuildable from git. In Part 9 we cover keeping it that way — the day-to-day of running a live Drupal site: updates, monitoring, backups you can actually restore, and the maintenance rhythm that keeps small problems from becoming outages.

← Part 7: Cron + Automation  ·  Part 9: Maintain It →

More insights

Want updates from Chattanooga.Digital?

Pre-join the co-op to receive new posts, workshop schedules, and member updates.