Featured image of post Create your own Wordpress website for free using Traefik and Docker

Create your own Wordpress website for free using Traefik and Docker

I’ve switched from using Traefik and WordPress for my website. Now, I use an internal Nginx proxy within Docker and my site is built with Hugo. To learn more about this change, you can read my post: Migrating from WordPress to Hugo


This first post will guide you on how to establish your own self-hosted Wordpress website using Docker, mirroring the setup of this very site. For this example, I’m using a physical Ubuntu machine connected to a Unifi Dream Machine Pro. You have the flexibility to employ a VPS or any bare-metal operating system that supports Docker. While this tutorial focuses on Ubuntu-specific tools, adjustments can be made for other environments if you’re comfortable with Docker.

Disclosure: As an Amazon Associate, I earn from qualifying purchases. Your support through these links helps maintain this blog, but rest assured, the pricing remains the same for you whether you use my links or not. Thanks for your support!

This guide presumes you have Ubuntu installed, access to a user account with root privileges, Docker and Docker-Compose set up, a domain name under your ownership (consider Google Domains or Hover if you need one), the ability to manage your domain’s DNS records (CloudFlare is recommended), and the capability to forward ports on your network (unless using a VPS or externally hosted OS).

Ubuntu’s environment file system is particularly useful for securely storing sensitive variables. This simplifies sharing docker-compose files without exposing confidential information on platforms like GitHub. While there might be other methods for managing secrets within Docker, they won’t be covered in this tutorial. Lastly, prioritize security when hosting a website on your network. Additionally, if you’re running multiple externally accessible web servers, you might find this post on reverse proxies helpful: https://nexus-security.github.io/running-a-reverse-proxy-using-docker-made-simple/.

Prepare Your Domain

For your domain (e.g., whitematter.tech), add DNS records that direct it to your server’s public IP address. For home networks, this is the public IP provided by your ISP, which can be found at whatismyip.com. Personally, I prefer dynamic DNS solutions like duckdns.org, allowing automatic updates to my public IP in the absence of a static IP from my ISP. For a better understanding of how to integrate duckdns, refer to this video. The following image illustrates how CloudFlare is configured for my domain, pointing to my duckdns address (without duckdns, “Content” would be your public IP, such as 177.99.88.10).

Set Environment Variables

In Ubuntu, access the Environment file with the command:

1
sudo nano /etc/environment

Below the PATH variable, paste the following, replacing the generic values in red with your specific information. Do not modify the PATH variable’s content.

For your time zone (TZ), refer to this list.

Replace “username” in the USERDIR with your Ubuntu username. It’s strongly recommended to use a password manager like 1Password to generate and securely store passwords for MySQL and your WordPress databases.

1
2
3
4
5
6
7
8
9
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games"

TZ=America/New_York
USERDIR=/home/username/docker
MYSQL_ROOT_PASSWORD=dbrootpassword
DATABASE_NAME=dbname
DATABASE_USER=dbuser
DATABASE_PASSWORD=dbpassword
DOMAIN=yourdomain.com

Save the file (“Ctrl” + “o” followed by “Enter”) and exit (“Ctrl” + “x”).

A logout/login or a system reboot is needed for the changes to take effect:

1
sudo reboot

Preparing Directories and Permissions

Execute the following commands. You can run them all at once or individually. ${USERDIR} will be populated from the environment file.

1
2
3
4
5
6
7
8
sudo mkdir ${USERDIR}
sudo mkdir ${USERDIR}/apps
sudo mkdir ${USERDIR}/wordpress
sudo mkdir ${USERDIR}/wordpress/wp-data
sudo mkdir ${USERDIR}/data
sudo mkdir ${USERDIR}/data/configurations
sudo touch ${USERDIR}/data/acme.json
sudo chmod 600 ${USERDIR}/data/acme.json

Create Traefik static configuration file.

Use the following command to create the Traefik static configuration file (${USERDIR} will be taken from your environment file):

1
sudo nano ${USERDIR}/data/traefik.yml

Paste the following content into the file, ensuring you replace the placeholder email address in red with your own.

Two caServer addresses are provided at the end of the file. The one containing “staging” is intended for testing purposes. While using the staging server certificates is beneficial during setup, be aware that browsers may display security warnings. The LetsEncrypt server has limitations on the number of certificates issued within a specific timeframe. To disable an address, simply add “#” at the beginning of the line – Ubuntu will ignore lines prefixed with “#.”

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
api:
  dashboard: true

entryPoints:
  http:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: https
  https:
    address: ":443"
    http:
      middlewares:
        - secureHeaders@file
        - page-ratelimit@file
      tls:
        certResolver: letsencrypt

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
  file:
    filename: /configurations/dynamic.yml
certificatesResolvers:
  letsencrypt:
    acme:
      email: you@yourdomain.com
      storage: acme.json
      keyType: EC384
      httpChallenge:
        entryPoint: http
      #caServer: https://acme-staging-v02.api.letsencrypt.org/directory
      caServer: https://acme-v02.api.letsencrypt.org/directory

Save the file (“Ctrl” + “o” and “Enter”) and exit (“Ctrl” + “x”).

Prepare Traefik and Create Traefik Dynamic Config

For security, your Traefik password needs to be hashed using MD5, SHA1, or BCrypt. BCrypt is recommended for its stronger encryption.

Online Htpassword generators can be used for this purpose. For instance, if your username is “admin” and password is “password1234,” the BCrypt hash would resemble this: “admin:$2y$10$d0yk7WE.XqhF5bT1DdJhduRFOM5JSabTiSFCTnbC2.JgMolypHgS2.” Utilize a password manager (1Password is recommended) to generate a strong password for this step.

Once your username, password, and its hashed version are ready, proceed with the following:

Create the Traefik dynamic configuration file:

1
sudo nano ${USERDIR}/data/configurations/dynamic.yml

Paste the content below into the dynamic.yml file, replacing the placeholder username and password (in red) with your generated hashed credentials.

This configuration utilizes the page-ratelimit middleware. The provided values permit an average of 50 requests per second with a burst limit of 50. Feel free to adjust these values as needed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
http:
  middlewares:
    secureHeaders:
      headers:
        frameDeny: true
        sslRedirect: true
        browserXssFilter: true
        contentTypeNosniff: true
        forceSTSHeader: true
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 31536000
        customFrameOptionsValue: SAMEORIGIN
    user-auth:
      basicAuth:
        users:
          - "admin:$2y$10$d0yk7WE.XqhF5bT1DdJhduRFOM5JSabTiSFCTnbC2.JgMolypHgS2"
    page-ratelimit:
      rateLimit:
        average: 50
        burst: 50
tls:
  options:
    default:
      cipherSuites:
        - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
        - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
      minVersion: VersionTLS12

Save (“Ctrl” + “o” and “Enter”) and exit (“Ctrl” + “x”).

Create the Traefik Docker-Compose File

Create the Traefik docker-compose file:

1
sudo nano ${USERDIR}/docker-compose.yml

Copy the content below into the file, noting that fields enclosed in ${ } will be filled in from the environment file.

This configuration uses the latest version of Traefik (currently v2.2.6). If you prefer to stick to the latest release within the 2.2.X series, specify the image tag as “traefik:chevrotin.” Be mindful that using the “latest” tag might lead to compatibility issues if future Traefik versions (v3, etc.) introduce breaking changes. Check dockerhub for available release tags.

This setup routes ports 80 (http) and 443 (https) to the Traefik container.

You can find a copy of this docker-compose.yml file on GitHub.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
version: '3.3'
services:
  traefik:
    image: traefik:latest
    container_name: traefik
    restart: always
    security_opt:
      - no-new-privileges:true
    ports:
      - 80:80
      - 443:443
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ${USERDIR}/data/traefik.yml:/traefik.yml:ro
      - ${USERDIR}/data/configurations:/configurations
      - ${USERDIR}/data/acme.json:/acme.json
    environment:
      TZ: ${TZ}
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy"
      - "traefik.http.routers.traefik-secure.entrypoints=https"
      - "traefik.http.routers.traefik-secure.rule=Host(`traefik.${DOMAIN}`)"
      - "traefik.http.routers.traefik-secure.service=api@internal"
      - "traefik.http.routers.traefik-secure.middlewares=user-auth@file"

networks:
  proxy:
    external: true

Save (“Ctrl” + “o” and “Enter”) and exit (“Ctrl” + “x”).

Create the WordPress Docker-Compose File

Create the WordPress docker-compose file:

1
sudo nano ${USERDIR}/apps/docker-compose.yml

Copy the content below, noting that fields enclosed in ${ } will be populated from the environment file.

docker-compose.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
version: '3.7'
services:
  db:
    image: mariadb:latest
    container_name: wp-db
    volumes:
      - db-data:/var/lib/mysql
    networks:
      - default
    security_opt:
      - no-new-privileges:true
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${DATABASE_NAME}
      MYSQL_USER: ${DATABASE_USER}
      MYSQL_PASSWORD: ${DATABASE_PASSWORD}
      TZ: ${TZ}

  wordpress:
    depends_on:
      - db
    image: wordpress:latest
    container_name: wordpress
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_NAME: ${DATABASE_NAME}
      WORDPRESS_DB_USER: ${DATABASE_USER}
      WORDPRESS_DB_PASSWORD: ${DATABASE_PASSWORD}
      TZ: ${TZ}
    volumes:
      - ${USERDIR}/wordpress/wp-data:/var/www/html
    networks:
      - proxy
      - default
    security_opt:
      - no-new-privileges:true
    restart: unless-stopped
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy"
      - "traefik.http.routers.wordpress-secure.entrypoints=https"
      - "traefik.http.routers.wordpress-secure.rule=Host(`${DOMAIN}`)"

  portainer:
    container_name: portainer
    image: portainer/portainer:latest
    restart: unless-stopped
    command: -H unix:///var/run/docker.sock
    networks:
      - proxy
      - default
    security_opt:
      - no-new-privileges:true
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      TZ: ${TZ}
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy"
      - "traefik.http.routers.portainer-secure.entrypoints=https"
      - "traefik.http.routers.portainer-secure.rule=Host(`portainer.${DOMAIN}`)"

volumes:
  db-data:

networks:
  proxy:
    external: true

Create the Proxy Network and Start Traefik

With all files in place, create the proxy network and start Traefik.

Create the proxy network:

1
docker network create proxy

Start the Traefik container:

1
docker-compose -f ${USERDIR}/docker-compose.yml up -d

Allow a few minutes for startup, then try to access the Traefik web admin page at https://traefik.yourdomain.com (replace “yourdomain.com” with your domain, matching the one in the environment file). If successful, log in using the non-hashed username and password. You should see the following:

If you can’t access the page, you might need to forward ports 80 and 443 to your Ubuntu server (see the example screenshot for forwarding to Ubuntu at 10.100.0.15). This step is not required for VPS or externally hosted options. This post provides recommendations for securing your network. Once ports are forwarded, you can proceed.

Start the Remaining Containers

Important: Remember to log in to WordPress and Portainer immediately after starting them to set passwords and secure access.

Start the remaining containers:

1
docker-compose -f ${USERDIR}/apps/docker-compose.yml up -d

Give the containers a few minutes to start, and then try to access WordPress and Portainer through your browser.

WordPress:

Access WordPress at “https://yourdomain.com" (replace “yourdomain.com” with your domain).

You should land on the initial WordPress setup screen. Create a strong password for your WordPress user account. A password manager like 1Password can help with this.

A success message will indicate a successful WordPress installation.

Portainer:

Access Portainer at “https://portainer.yourdomain.com" (replace “yourdomain.com” with the domain from Section 1).

You’ll be directed to the Portainer setup screen. Create a strong password for your Portainer user account. Again, using a password manager like 1Password is recommended.

After setting your Portainer password, log in. In the left-hand menu, click “containers.” This view lists all running containers. From here, you can stop, start, restart, remove, and otherwise manage installed containers.

Getting Started with WordPress

It’s a good practice to increase the PHP memory limit and maximum file upload size for new WordPress installations. One approach is to modify the .htaccess file. Access this file in the terminal (${USERDIR} will be taken from the environment file; the file path below assumes you’ve kept the WordPress volume as defined in the docker-compose file).

1
sudo nano ${USERDIR}/wordpress/wp-data/.htaccess

Add the following lines to the file:

1
2
3
4
5
php_value memory_limit 256M
php_value upload_max_filesize 128M
php_value post_max_size 128M
php_value max_execution_time 300
php_value max_input_time 1000

Save (“Ctrl” + “o” and “Enter”) and exit (“Ctrl” + “x”).

Restart the WordPress container for the changes to take effect. You can either do this through Portainer or restart all containers:

1
docker restart $(docker ps -q)

WordPress Admin Page:

You can access your WordPress admin area at “https://yourdomain.com/wp-admin" (replace “yourdomain.com” with your domain). Use the credentials you set up in Section 14 to log in.

Change the WordPress Theme:

You can easily change your blog or website’s theme.

Click “Add New Theme,” search for your desired theme, hover over it, and click “Install.” After installation, click “Activate.”

Create a Blog Post:

To add a new blog post, click “Posts,” then “Add New.” This will generate a blank post. The default WordPress block editor is a user-friendly option for creating content. If you prefer, you can install page builder plugins. Here’s what the default block editor looks like:

After crafting your post, click “Publish,” and then click it again to make it live. You can view your post by navigating to the address displayed under “Post address.” Your blog post will resemble this. Customization can be achieved through settings and plugins.

Licensed under CC BY-NC-SA 4.0