Category: Docker

  • [Troubleshooting] Error: could not find system library ‘openssl’ required by the ‘openssl-sys’ crate

    When using reqwest crate and building docker images with Dockerfile mentioned in this article, you will encounter openssl issues due to missing packages.

    Error: could not find system library 'openssl' required by the 'openssl-sys' crate

    It turns out that below line is the culprit:

    FROM docker.io/rust:1-slim-bookworm AS build
    
    #############################################

    rust:1-slim-bookworm misses lots of necessary package to run the application. It is suggested in official Rust docker image document that we should avoid using slim image.

    Simple but ugly solution

    Well, the simplest approach is using a minimal Dockerfile which will always work:

    FROM rust:bookworm
    
    COPY . .
    
    RUN cargo build --release
    
    EXPOSE 8080
    
    CMD ./target/release/your_package

    However, it will generate a pretty large image which can take gigabytes of storage.

    The better one

    We can still use multi stage build to generate an optimal image with a little tweak (using default rust image version rust:bookworm):

    FROM docker.io/rust:bookworm AS build
    
    ## cargo package name: customize here or provide via --build-arg
    ARG pkg=hello-world
    
    WORKDIR /build
    
    COPY . .
    
    RUN --mount=type=cache,target=/build/target \
        --mount=type=cache,target=/usr/local/cargo/registry \
        --mount=type=cache,target=/usr/local/cargo/git \
        set -eux; \
        cargo build --release; \
        objcopy --compress-debug-sections target/release/$pkg ./main
    
    ################################################################################
    
    FROM docker.io/debian:bookworm-slim
    
    WORKDIR /app
    
    ## copy the main binary
    ## add more files below if needed
    COPY --from=build /build/main ./
    
    EXPOSE 8080
    
    CMD ./main

    Now it builds fine, but you will get a runtime error when running reqwest:

    ./main: error while loading shared libraries: libssl.so.3: cannot open shared object file: No such file or directory

    Furthermore, after installing correct packages either vendored or automatic, you will still see error when sending https requests:

    hyper_util::client::legacy::Error(Connect, Ssl(Error { code: ErrorCode(1), cause: Some(Ssl(ErrorStack([Error { code: 167772294, library: "SSL routines", function: "tls_post_process_server_certificate", reason: "certificate verify failed", file: "ssl/statem/statem_clnt.c", line: 2092 }]))) }, X509VerifyResult { code: 20, error: "unable to get local issuer certificate" }))

    To solve these issues, I separately wrote an article for detailed explanation and solutions.

  • [Troubleshooting] libssl.so.3 and certificate Error Running Rust/Reqwest Under Debian Image

    If you see below errors while running Rust app using Docker, this article is right for you:

    error while loading shared libraries: libssl.so.3: cannot open shared object file: No such file or directory
    hyper_util::client::legacy::Error(Connect, Ssl(Error { code: ErrorCode(1), cause: Some(Ssl(ErrorStack([Error { code: 167772294, library: "SSL routines", function: "tls_post_process_server_certificate", reason: "certificate verify failed", file: "ssl/statem/statem_clnt.c", line: 2092 }]))) }, X509VerifyResult { code: 20, error: "unable to get local issuer certificate" }))

    Anatomy

    Reqwest document mentioned about requirements when running under Linux OS, where you need OpenSSL installed.

    To supply OpenSSL, Rust official document provided 2 ways:

    Vendored OpenSSL

    Add below dependency to Cargo.toml.

    [dependencies]
    openssl = { version = "0.10", features = ["vendored"] }

    Automatic OpenSSL

    Add below line to Dockerfile at stage of running app.

    RUN apt-get update && apt-get install -y pkg-config libssl-dev

    Either approach works fine and you can run the app successfully. However, when sending https requests, you will get below error:

    unable to get local issuer certificate

    A Debian image issue

    This thread gives a good idea of what happens here. Essentially, the debian official image does not have ca certification package installed. To solve the issue, simply install the package in Dockerfile:

    RUN apt-get update && apt-get install -y pkg-config libssl-dev && apt install -y ca-certificates
    # or below if using vendored OpenSSL
    # RUN apt-get update && apt install -y ca-certificates

    Final Dockerfile

    To build rust + actix-web + reqwest, below is what works for me:

    [package]
    name = "hello-world"
    version = "0.1.0"
    edition = "2021"
    
    [dependencies]
    actix-web = "4"
    reqwest = "0.12"
    serde = { version = "1.0", features = ["derive"] }
    openssl = { version = "0.10", features = ["vendored"] }
    FROM rust:bookworm AS build
    
    ## cargo package name: customize here or provide via --build-arg
    ARG pkg=hello-world
    
    WORKDIR /build
    
    COPY . .
    
    RUN --mount=type=cache,target=/build/target \
        --mount=type=cache,target=/usr/local/cargo/registry \
        --mount=type=cache,target=/usr/local/cargo/git \
        set -eux; \
        cargo build --release; \
        objcopy --compress-debug-sections target/release/$pkg ./main
    
    ################################################################################
    
    FROM docker.io/debian:bookworm-slim
    
    WORKDIR /app
    
    ## copy the main binary
    COPY --from=build /build/main ./
    
    EXPOSE 8080
    
    RUN apt-get update && apt install -y ca-certificates
    
    CMD ./main
  • Containerize Actix Web App Using Docker In A Cleaner Way

    Recently came across this task to try different Rust web frameworks and Actix Web is one of the most popular frameworks. Its documentation is clean and easy to bootstrap, however, when I was trying to deploy the app using Docker, I could not find a clean Dockerfile example that contains minimal dependencies.

    Luckily, when I was trying Rocket, it has a pretty clean documentation about containerize the app. After tweaking it a bit, it just worked for Actix Web app as well.

    Personally, I feel more confident to follow official documents for long term maintainability. Even tho the image size coming from the build is not as small as ~60MB like in this guide, but ~145MB image is also acceptable here consider other factors.

    Initiate Actix Web Sample App

    Create a new Rust app:

    cargo new hello-world
    cd hello-world

    Now add Actix Web dependency into Cargo.toml file:

    [dependencies]
    actix-web = "4"

    Then replace the contents of src/main.rs with the following:

    use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};
    
    #[get("/")]
    async fn hello() -> impl Responder {
        HttpResponse::Ok().body("Hello world!")
    }
    
    #[post("/echo")]
    async fn echo(req_body: String) -> impl Responder {
        HttpResponse::Ok().body(req_body)
    }
    
    async fn manual_hello() -> impl Responder {
        HttpResponse::Ok().body("Hey there!")
    }
    
    #[actix_web::main]
    async fn main() -> std::io::Result<()> {
        HttpServer::new(|| {
            App::new()
                .service(hello)
                .service(echo)
                .route("/hey", web::get().to(manual_hello))
        })
        .bind(("0.0.0.0", 8080))?
        .run()
        .await
    }

    Compile and run the program:

    cargo run

    Test app using http://0.0.0.0:8080

    Add Dockerfile and build Docker image

    The original Dockerfile from Rocket document has some Rocket specific settings, simply removing those env variables would just work:

    FROM docker.io/rust:1-slim-bookworm AS build
    
    ## cargo package name: customize here or provide via --build-arg
    ARG pkg=hello-world
    
    WORKDIR /build
    
    COPY . .
    
    RUN --mount=type=cache,target=/build/target \
        --mount=type=cache,target=/usr/local/cargo/registry \
        --mount=type=cache,target=/usr/local/cargo/git \
        set -eux; \
        cargo build --release; \
        objcopy --compress-debug-sections target/release/$pkg ./main
    
    ################################################################################
    
    FROM docker.io/debian:bookworm-slim
    
    WORKDIR /app
    
    ## copy the main binary
    ## add more files below if needed
    COPY --from=build /build/main ./
    
    EXPOSE 8080
    
    CMD ./main

    Note that, pkg ARG need to be the same as package name in Cargo.toml.

    Now let’s build the image:

    docker build -t hello-world-image .

    Check the image size (~145MB):

    Finally, run the image:

    docker run -d -p 0.0.0.0:8080:8080 hello-world-image

    Test the app using http://0.0.0.0:8080

  • Develop and Deploy Rust Rocket App Using Docker Compose (Traefik/Docker Hub)

    This article will talk about how to use Docker for quick Rocket web app deployment. This architecture can be scaled with more complicated CI/CD framework and Kubernetes clusters for large scale application and zero downtime deployment.

    Prerequisite

    • Linux server
    • Local dev machine with Rust installed
    • Docker with Docker Compose
    • Follow this guide for setting up Traefik.
    • Domain name

    Getting started

    Update rust

    Install rustup by following the instructions on its website. Once rustup is installed, ensure the latest toolchain is installed by running the command:

    rustup default stable

    Initiate Rocket sample app

    cargo new hello-rocket --bin
    cd hello-rocket

    Now, add Rocket as a dependency in your Cargo.toml:

    [dependencies]
    rocket = "0.5.1"

    Modify src/main.rs so that it contains the code for the Rocket Hello, world! program, reproduced below:

    #[macro_use] extern crate rocket;
    
    #[get("/")]
    fn index() -> &'static str {
        "Hello, world!"
    }
    
    #[launch]
    fn rocket() -> _ {
        rocket::build().mount("/", routes![index])
    }

    Finally, we can run below command to test our first app:

    cargo run

    Build docker image and upload

    Add Dockerfile

    There are many tutorials out there but it is always a good practice to follow official documents: https://rocket.rs/guide/v0.5/deploying/#containerization

    Note that, in order to test Docker image in local, EXPOSE is needed:

    FROM docker.io/rust:1-slim-bookworm AS build
    
    ## cargo package name: customize here or provide via --build-arg
    ARG pkg=hello-rocket
    
    WORKDIR /build
    
    COPY . .
    
    RUN --mount=type=cache,target=/build/target \
        --mount=type=cache,target=/usr/local/cargo/registry \
        --mount=type=cache,target=/usr/local/cargo/git \
        set -eux; \
        cargo build --release; \
        objcopy --compress-debug-sections target/release/$pkg ./main
    
    ################################################################################
    
    FROM docker.io/debian:bookworm-slim
    
    WORKDIR /app
    
    ## copy the main binary
    COPY --from=build /build/main ./
    
    ## copy runtime assets which may or may not exist
    COPY --from=build /build/Rocket.tom[l] ./static
    COPY --from=build /build/stati[c] ./static
    COPY --from=build /build/template[s] ./templates
    
    ## ensure the container listens globally on port 8000
    ENV ROCKET_ADDRESS=0.0.0.0
    ENV ROCKET_PORT=8000
    
    ## uncomment below to test in local
    ## EXPOSE 8000
    
    CMD ./main

    Make sure the pkg set to same value as package name in Cargo.toml

    Build Docker image

    docker build -t your_username/my-rocket-image .

    Upload Docker image to Docker Hub

    First make sure you have Docker Hub account and then login to Docker:

    Docker login

    Then upload Docker image to Docker Hub:

    docker push your_username/my-rocket-image

    Deploy docker image to cloud instance with Docker Compose and Traefik

    Assume you already followed this guide and Traefik reverse proxy is up running in your server.

    Run uploaded Rocket app Docker image

    First let’s add a folder:

    mkdir ~/rocket-docker && cd ~/rocket-docker

    Then add docker-compose.yml:

    vi docker-compose.yml
    networks:
      traefik:
        external: true
     
    services:
      app:
        image: your_username/my-rocket-image:latest
        networks:
          - traefik
        labels:
          - "traefik.enable=true"
          - "traefik.http.routers.rocket-docker.rule=Host(`your-domain.com`)"
          - "traefik.http.routers.rocket-docker.service=rocket-docker"
          - "traefik.http.routers.rocket-docker.entrypoints=websecure"
          - "traefik.http.services.rocket-docker.loadbalancer.server.port=8000"
          - "traefik.http.services.rocket-docker.loadbalancer.passhostheader=true"
          - "traefik.http.routers.rocket-docker.tls=true"
          - "traefik.http.routers.rocket-docker.tls.certresolver=letsencrypt"
          - "traefik.http.routers.rocket-docker.middlewares=compresstraefik"
          - "traefik.http.middlewares.compresstraefik.compress=true"
          - "traefik.docker.network=traefik"
        restart: unless-stopped

    Make sure to update your-domain.com

    Run below to start service:

    docker compose up -d

    Verify by visiting www.your-domain.com see if everything works properly.

    Develop and update service with latest change

    Try change something in your local Next.js app, and run upload script again:

    docker build -t your_username/my-rocket-image . && docker push your_username/my-rocket-image

    Then go to your cloud instance and run below:

    docker pull your_username/my-rocket-image:latest && docker compose -f ~/rocket-docker/docker-compose.yml up -d

    Lastly, verify the change by visiting www.your-domain.com.

  • Setup Traefik Reverse Proxy and Add Routing Using Docker Compose

    Reverse proxy is essential for any service being accessed publicly. Traefik is a popular reverse proxy and load balancer designed for microservices and containerized applications. Please make sure Docker is installed with Docker Compose.

    Setup Traefik with built-in dashboard app

    Let’s first create a folder for Traefik reverse proxy.

    mkdir ~/traefik && cd ~/traefik

    Next we need to create network for Traefik to communicate with other containers, and it should be exposed externally.

    docker create network traefik

    Now, let’s create docker-compose.yml

    vi docker-compose.yml
    networks:
      traefik:
        external: true
    
    volumes:
      traefik-certificates:
    
    services:
      traefik:
        image: "traefik:latest"
        command:
          - "--log.level=DEBUG"
          - "--accesslog=true"
          - "--api.dashboard=true"
          - "--api.insecure=true"
          - "--ping=true"
          - "--ping.entrypoint=ping"
          - "--entryPoints.ping.address=:8082"
          - "--entryPoints.web.address=:80"
          - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
          - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
          - "--entryPoints.websecure.address=:443"
          - "--providers.docker=true"
          - "--providers.docker.endpoint=unix:///var/run/docker.sock"
          - "--providers.docker.exposedByDefault=false"
          - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
          # For requesting dev cert (if prod cert has issue during development)
          # - "--certificatesresolvers.myhttpchallenge.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
          - "--certificatesresolvers.letsencrypt.acme.email=admin@bill-min.com"
          - "--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json"
        volumes:
          - /var/run/docker.sock:/var/run/docker.sock
          - traefik-certificates:/etc/traefik/acme
        networks:
          - traefik
        ports:
          - "80:80"
          - "443:443"
        healthcheck:
          test: ["CMD", "wget", "http://localhost:8082/ping","--spider"]
          interval: 10s
          timeout: 5s
          retries: 3
          start_period: 5s
        labels:
          - "traefik.enable=true"
          - "traefik.http.routers.dashboard.rule=Host(`traefik.your-domain.com`)"
          - "traefik.http.routers.dashboard.service=api@internal"
          - "traefik.http.routers.dashboard.entrypoints=websecure"
          - "traefik.http.routers.dashboard.tls=true"
          - "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
          - "traefik.http.routers.dashboard.middlewares=authtraefik"
          - "traefik.http.middlewares.authtraefik.basicauth.users=your_username:{SHA}your_hash"
        restart: unless-stopped

    Above config also adds Traefik Dashboard and https with letsencrypt.

    Replace your-domain with your actual domain name and setup correct DNS.

    Replace your_username and {SHA}your_hash with ones generated from https://hostingcanada.org/htpasswd-generator/

    Finally, lets start Traefik container.

    docker compose up -d

    Verify the container is running successfully

    docker ps

    You can also check Traefik dashboard by visiting https://traefik.your-domain.com. (you will need to enter the username and password used when generating htpasswd)

    Add routing to other containerized apps

    After setting up Traefik, adding routes is simple. In most cases, there are 2 things get involved:

    • Communicating with Traffic through network that can be accessed externally, which is traefik from above setting.
    • Running the container from Docker image behind Traefik load balancer at exposed port.

    Traefik will config properly based on labels and below is an example of running Next.js app through Docker Compose.

    networks:
      traefik:
        external: true
    
    services:
      app:
        image: your_username/nextjs-docker:latest
        ports:
          - 3000:3000
        networks:
          - traefik
        labels:
          - "traefik.enable=true"
          - "traefik.http.routers.nextjs.rule=Host(`www.your-domain.com`)"
          - "traefik.http.routers.nextjs.service=nextjs"
          - "traefik.http.routers.nextjs.entrypoints=websecure"
          - "traefik.http.services.nextjs.loadbalancer.server.port=3000"
          - "traefik.http.services.nextjs.loadbalancer.passhostheader=true"
          - "traefik.http.routers.nextjs.tls=true"
          - "traefik.http.routers.nextjs.tls.certresolver=letsencrypt"
          - "traefik.http.routers.nextjs.middlewares=compresstraefik"
          - "traefik.http.middlewares.compresstraefik.compress=true"
          - "traefik.docker.network=traefik"
        restart: unless-stopped

    For running different apps, the settings are mostly the same except the router name and service name.

  • Develop and Deploy Next.js App Using Docker Compose (Traefik/Docker Hub)

    Before Docker, the way to deploy a node app usually requires uploading/downloading latest files using git or file transfer protocol then re-run the app in cloud. With Docker we can build docker image from local development then upload to Docker Hub. With Docker Compose setup in cloud, we can easily fetch latest image and re-run the container. This architecture can be scaled with more complicated CI/CD framework and Kubernetes clusters for large scale application and zero downtime deployment.

    Install Docker Desktop

    Simply download and install Docker Desktop from: https://www.docker.com/products/docker-desktop/

    Register account (if not already)

    A Docker Hub account is required in order to upload images.

    Login to Docker user CLI

    Run below command to login in to Docker for using docker push command.

    Docker login

    Initiate sample Next.js app

    Official Next.js doc has sample app with docker:

    npx create-next-app --example with-docker nextjs-docker

    After going through prompts, Dockerfile should be already inside project folder.

    Add build&upload script inside package.json

    Official Next.js doc has instructions for building and running Next.js with Docker, however, let’s do it more intuitively. Add below script to package.json

    {
      ...
      "scripts": {
        "upload:docker": "docker build -t your_username/nextjs-docker . && docker push your_username/nextjs-docker"
      }
      ...
    }

    Replace nextjs-docker with your preferred app’s name.

    Build docker image and upload

    Simply run below command:

    npm run upload:docker

    This will build docker image then upload to Docker Hub with latest tag.

    Github and Github Action

    In case github is being used as source control, above step can be integrated with Github Action and trigger on commit or PR merge.

    Deploy docker image to cloud instance with Docker Compose and Traefik

    Prerequisite

    • Linux server
    • Docker Engine installed with Docker Compose plugin

    Setup Traefic (skip if already done)

    Let’s make a folder for Traefic:

    mkdir ~/traefik && cd ~/traefik

    Next we need to create network for Traefik to communicate with other containers, and it should be exposed externally.

    docker create network traefik

    Now, let’s create docker-compose.yml

    vi docker-compose.yml
    networks:
      traefik:
        external: true
    
    volumes:
      traefik-certificates:
    
    services:
      traefik:
        image: "traefik:latest"
        command:
          - "--log.level=DEBUG"
          - "--accesslog=true"
          - "--api.dashboard=true"
          - "--api.insecure=true"
          - "--ping=true"
          - "--ping.entrypoint=ping"
          - "--entryPoints.ping.address=:8082"
          - "--entryPoints.web.address=:80"
          - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
          - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
          - "--entryPoints.websecure.address=:443"
          - "--providers.docker=true"
          - "--providers.docker.endpoint=unix:///var/run/docker.sock"
          - "--providers.docker.exposedByDefault=false"
          - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
          # For requesting dev cert (if prod cert has issue during development)
          # - "--certificatesresolvers.myhttpchallenge.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
          - "--certificatesresolvers.letsencrypt.acme.email=admin@bill-min.com"
          - "--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json"
        volumes:
          - /var/run/docker.sock:/var/run/docker.sock
          - traefik-certificates:/etc/traefik/acme
        networks:
          - traefik
        ports:
          - "80:80"
          - "443:443"
        healthcheck:
          test: ["CMD", "wget", "http://localhost:8082/ping","--spider"]
          interval: 10s
          timeout: 5s
          retries: 3
          start_period: 5s
        labels:
          - "traefik.enable=true"
          - "traefik.http.routers.dashboard.rule=Host(`traefik.your-domain.com`)"
          - "traefik.http.routers.dashboard.service=api@internal"
          - "traefik.http.routers.dashboard.entrypoints=websecure"
          - "traefik.http.routers.dashboard.tls=true"
          - "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
          - "traefik.http.routers.dashboard.middlewares=authtraefik"
          - "traefik.http.middlewares.authtraefik.basicauth.users=your_username:{SHA}your_hash"
        restart: unless-stopped

    Above config also adds Traefik Dashboard and https with letsencrypt.

    Replace your-domain with your actual domain name and setup correct DNS.

    Replace your_username and {SHA}your_hash with ones generated from https://hostingcanada.org/htpasswd-generator/

    Finally, lets start Traefik container.

    docker compose up -d

    Verify the container is running successfully

    docker ps

    You can also check Traefik dashboard by visiting https://traefik.your-domain.com. (you will need to enter the username and password used when generating htpasswd)

    Run uploaded Next.js docker image

    First let’s add a folder:

    mkdir ~/nextjs-docker && cd ~/nextjs-docker

    Then add docker-compose.yml:

    networks:
      traefik:
        external: true
    
    services:
      app:
        image: your_username/nextjs-docker:latest
        ports:
          - 3000:3000
        networks:
          - traefik
        labels:
          - "traefik.enable=true"
          - "traefik.http.routers.nextjs.rule=Host(`www.your-domain.com`)"
          - "traefik.http.routers.nextjs.service=nextjs"
          - "traefik.http.routers.nextjs.entrypoints=websecure"
          - "traefik.http.services.nextjs.loadbalancer.server.port=3000"
          - "traefik.http.services.nextjs.loadbalancer.passhostheader=true"
          - "traefik.http.routers.nextjs.tls=true"
          - "traefik.http.routers.nextjs.tls.certresolver=letsencrypt"
          - "traefik.http.routers.nextjs.middlewares=compresstraefik"
          - "traefik.http.middlewares.compresstraefik.compress=true"
          - "traefik.docker.network=traefik"
        restart: unless-stopped

    Note that, the Next.js app need to run inside same network as Traefic. Also replace your-domain with real domain.

    Run below to start service:

    docker compose up -d

    Verify by visiting www.your-domain.com see if everything works properly.

    Develop and update service with latest change

    Try change something in your local Next.js app, and run upload script again (or using Github Action):

    npm run upload:docker

    Then go to your cloud instance and run below:

    docker pull your_username/nextjs-docker:latest && docker compose -f ~/nextjs-docker/docker-compose.yml up -d

    Lastly, verify the change by visiting www.your-domain.com.

  • Pull and Run Latest Docker Image With One Line Using Docker Compose

    In case the docker image with latest tag has been updated, we can use below one-line script to pull the updated version and recreate container.

    docker pull docker-image:latest && docker compose -f ~/apps/docker-compose.yml up -d

    After that, we can use below script for cleaning up dangling images.

    docker image prune
  • Setup Multiple WordPress Sites Using Docker Compose (Traefik + MariaDB)

    Prerequisite

    Run below CLI to check if everything is installed properly.

    docker compose version

    Setup Traefik with Docker Compose

    Let’s first create a folder for Traefik reverse proxy.

    mkdir ~/traefik && cd ~/traefik

    Next we need to create network for Traefik to communicate with other containers, and it should be exposed externally.

    docker create network traefik

    Now, let’s create docker-compose.yml

    vi docker-compose.yml
    networks:
      traefik:
        external: true
    
    volumes:
      traefik-certificates:
    
    services:
      traefik:
        image: "traefik:latest"
        command:
          - "--log.level=DEBUG"
          - "--accesslog=true"
          - "--api.dashboard=true"
          - "--api.insecure=true"
          - "--ping=true"
          - "--ping.entrypoint=ping"
          - "--entryPoints.ping.address=:8082"
          - "--entryPoints.web.address=:80"
          - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
          - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
          - "--entryPoints.websecure.address=:443"
          - "--providers.docker=true"
          - "--providers.docker.endpoint=unix:///var/run/docker.sock"
          - "--providers.docker.exposedByDefault=false"
          - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
          # For requesting dev cert (if prod cert has issue during development)
          # - "--certificatesresolvers.myhttpchallenge.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
          - "--certificatesresolvers.letsencrypt.acme.email=admin@bill-min.com"
          - "--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json"
        volumes:
          - /var/run/docker.sock:/var/run/docker.sock
          - traefik-certificates:/etc/traefik/acme
        networks:
          - traefik
        ports:
          - "80:80"
          - "443:443"
        healthcheck:
          test: ["CMD", "wget", "http://localhost:8082/ping","--spider"]
          interval: 10s
          timeout: 5s
          retries: 3
          start_period: 5s
        labels:
          - "traefik.enable=true"
          - "traefik.http.routers.dashboard.rule=Host(`traefik.your-domain.com`)"
          - "traefik.http.routers.dashboard.service=api@internal"
          - "traefik.http.routers.dashboard.entrypoints=websecure"
          - "traefik.http.routers.dashboard.tls=true"
          - "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
          - "traefik.http.routers.dashboard.middlewares=authtraefik"
          - "traefik.http.middlewares.authtraefik.basicauth.users=your_username:{SHA}your_hash"
        restart: unless-stopped

    Above config also adds Traefik Dashboard and https with letsencrypt.

    Replace your-domain with your actual domain name and setup correct DNS.

    Replace your_username and {SHA}your_hash with ones generated from https://hostingcanada.org/htpasswd-generator/

    Finally, lets start Traefik container.

    docker compose up -d

    Verify the container is running successfully

    docker ps

    You can also check Traefik dashboard by visiting https://traefik.your-domain.com. (you will need to enter the username and password used when generating htpasswd)

    Setup first wordpress site

    First, we need to create an internal network for communicating between wordpress and db (MariaDB in this tutorial).

    docker create network my-wp-1

    Then lets create new folder for each site we are building.

    mkdir ~/my-wp-1 && cd ~/my-wp-1

    Create docker-compose.yml.

    networks:
      my-wp-1:
      traefik:
        external: true
     
    volumes:
      mariadb-data:
     
    services:
      mariadb:
        image: mariadb:latest
        volumes:
          - mariadb-data:/var/lib/mysql
        environment:
          MARIADB_DATABASE: ${WORDPRESS_DB_NAME}
          MARIADB_USER: ${WORDPRESS_DB_USER}
          MARIADB_PASSWORD: ${WORDPRESS_DB_PASSWORD}
          MARIADB_RANDOM_ROOT_PASSWORD: "1"
        networks:
          - my-wp-1
        healthcheck:
          test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
          interval: 10s
          timeout: 5s
          retries: 3
          start_period: 60s
        restart: unless-stopped
     
      wordpress:
        image: wordpress:latest
        volumes:
          - ./wordpress-files:/var/www/html
        environment:
          WORDPRESS_DB_HOST: mariadb:3306
          WORDPRESS_DB_USER: ${WORDPRESS_DB_USER}
          WORDPRESS_DB_PASSWORD: ${WORDPRESS_DB_PASSWORD}
          WORDPRESS_DB_NAME: ${WORDPRESS_DB_NAME}
        networks:
          - my-wp-1
          - traefik
        healthcheck:
          test: timeout 10s bash -c ':> /dev/tcp/127.0.0.1/80' || exit 1
          interval: 10s
          timeout: 5s
          retries: 3
          start_period: 90s
        labels:
          - "traefik.enable=true"
          - "traefik.http.routers.my-wp-1.rule=Host(`${WORDPRESS_HOSTNAME}`)"
          - "traefik.http.routers.my-wp-1.service=my-wp-1"
          - "traefik.http.routers.my-wp-1.entrypoints=websecure"
          - "traefik.http.services.my-wp-1.loadbalancer.server.port=80"
          - "traefik.http.services.my-wp-1.loadbalancer.passhostheader=true"
          - "traefik.http.routers.my-wp-1.tls=true"
          - "traefik.http.routers.my-wp-1.tls.certresolver=letsencrypt"
          - "traefik.http.routers.my-wp-1.middlewares=compresstraefik"
          - "traefik.http.middlewares.compresstraefik.compress=true"
          - "traefik.docker.network=traefik"
        restart: unless-stopped
        depends_on:
          mariadb:
            condition: service_healthy

    Also we need to create .env file for setting up some variables. (Replace values with proper ones)

    WORDPRESS_HOSTNAME=www.your-domain.com
    WORDPRESS_DB_NAME=wordpressdb
    WORDPRESS_DB_USER=myuser
    WORDPRESS_DB_PASSWORD=mypassword

    Then we should be able to start wordpress.

    docker compose up -d

    Now we should be able to visit https://www.your-domain.com to start using WordPress.

    Create more wordpress sites

    Simply repeat the steps we used to create our first site to create more sites, just need to change my-wp-1 to different value in docker-compose.yml and change values in .env file then run docker compose up -d.