DEV Community

gu1lh3rm3_x
gu1lh3rm3_x

Posted on

Using Docker + Traefik + WordPress on Hostinger VPS

Recently, a friend of mine came to me with an idea: he wanted a WordPress site where he could “upload” old console games (SNES, Game Boy, etc.) so people could play them directly from their browser—even on mobile.

He already knew what to do on the WordPress side, but first he needed the right infrastructure to host everything.

I told him:

“Hey, I actually have a VPS at Hostinger. If you want, we can split the cost and I’ll set up WordPress there for you.”

He agreed—and that’s where the fun began.

Step 1: Running Multiple Projects on One VPS

I knew I wanted the flexibility to run multiple projects on this VPS, not just WordPress. That meant I needed a way to isolate apps and keep them easy to manage.

The answer: Docker.

The idea was straightforward:

  • Run WordPress in a container.
  • Use MariaDB as the database.
  • Store data in persistent Docker volumes.

Here’s my first docker-compose.yml:

version: '3.9'

services:
  db:
    image: mariadb:10.11
    container_name: wordpress_db
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: somepass
      MYSQL_DATABASE: somedatabase
      MYSQL_USER: someuser
      MYSQL_PASSWORD: somepassword
    volumes:
      - db_data:/var/lib/mysql

  wordpress:
    image: wordpress:latest
    container_name: wordpress_app
    depends_on:
      - db
    ports:
      - "8080:80"
    restart: always
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: someuser
      WORDPRESS_DB_PASSWORD: somepass
      WORDPRESS_DB_NAME: somedatabase
    volumes:
      - wordpress_data:/var/www/html

volumes:
  db_data:
  wordpress_data:

Enter fullscreen mode Exit fullscreen mode

It worked fine, but I didn’t want credentials hardcoded in YAML. So, I moved them into a .env file and updated the configuration.

Step 2: Making It Production-Ready with Traefik

At this point, we had a functional WordPress setup—but it was only accessible via:
http://VPS_IP:8080

That’s not practical. We needed:

  1. HTTPS support with certificates.
  2. A reverse proxy to route requests to different containers (since we planned multiple projects).
  3. Proper DNS mapping.

Enter Traefik—an HTTP reverse proxy and ingress controller.

The setup looked like this:
architecture idea with Traefik

  • SSL certificates via Let’s Encrypt
  • Traefik as reverse proxy → routes traffic to WordPress
  • MariaDB as the database
  • Everything connected on the same Docker network

Here’s the updated docker-compose.yml:

version: '3.9'

services:
  traefik:
    image: traefik:v3.0
    container_name: traefik
    restart: always
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      - "--api.dashboard=true"
      - "--api.insecure=false"
      - "--certificatesresolvers.le.acme.email=youremail@example.com"
      - "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.le.acme.tlschallenge=true"

    ports:
      - "80:80"
      - "443:443"
      - "8080:8080" # Dashboard (optional)
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./letsencrypt:/letsencrypt"
      - "./traefik_logs:/var/log/traefik"
    networks:
      - wpnet

  db:
    image: mariadb:10.11
    container_name: wordpress_db
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - wpnet

  wordpress:
    image: wordpress:latest
    container_name: wordpress_app
    depends_on:
      - db
    restart: always
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: ${WORDPRESS_DB_USER}
      WORDPRESS_DB_PASSWORD: ${WORDPRESS_DB_PASSWORD}
      WORDPRESS_DB_NAME: ${WORDPRESS_DB_NAME}
      WORDPRESS_CONFIG_EXTRA: |
        define('WP_HOME','https://HOST_DNS.com');
        define('WP_SITEURL','https://HOST_DNS.com');
    volumes:
      - wordpress_data:/var/www/html
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.wordpress.rule=Host(`HOST_DNS.com`) || Host(`www.HOST_DNS.com`)"
      - "traefik.http.routers.wordpress.entrypoints=websecure"
      - "traefik.http.routers.wordpress.tls.certresolver=le"
    networks:
      - wpnet

volumes:
  db_data:
  wordpress_data:

networks:
  wpnet:
    driver: bridge
Enter fullscreen mode Exit fullscreen mode

Now the site could be accessed via:
https://HOST_DNS.com

Step 3: Backups & Recovery

A working site is great—but what about backups?

I wrote a simple backup script with a cronjob that:

  1. Dumps the database
  2. Archives WordPress files
  3. Copies Traefik certificates
  4. Deletes backups older than 7 days

I also created a restore script, which let me choose which parts to restore (DB, WordPress files, or certificates).

This way, if anything breaks, I can recover quickly.

Step 4: Managing Secrets

Right now, secrets live in a .env file, which isn’t ideal for production. The next step is to move them into a proper secrets manager (e.g., Docker secrets, HashiCorp Vault, or a cloud provider’s secret manager).

That will make the setup more secure.

Final Thoughts

This little project started as “let’s host WordPress on a VPS” and turned into a practical exercise in:

  • Containerization
  • Reverse proxying with Traefik
  • SSL automation
  • Backups & recovery
  • Secrets management

It was a great hands-on way to think about infrastructure from scratch.

If you found this helpful, drop a like, leave a comment, or share it with a friend who might enjoy it. 🚀

Top comments (0)