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:
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:
- HTTPS support with certificates.
- A reverse proxy to route requests to different containers (since we planned multiple projects).
- Proper DNS mapping.
Enter Traefik—an HTTP reverse proxy and ingress controller.
- 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
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:
- Dumps the database
- Archives WordPress files
- Copies Traefik certificates
- 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)