Intro

Cloudflare tunnels allow you to publish a backend service on the public internet without having to use your own public IP address. Cloudflare tunnels support a number of protocols including SSH, RDP and HTTP. This post will focus on hosting a backend web service (written in Rust BTW) using Docker and Cloudflare tunnels to publish it on the interwebs.

Software

The following software was used in this post.

  • cloudflared - 2025.2.0
  • Docker - 27.4.1
  • Rust - 1.86.0

Architecture

This post is based on the following architecture.

  • There are 2 backend containers, a webserver and cloudflared.
  • An always on secure QUIC tunnel is established from the cloudflared container to Cloudflare over UDP port 443.
  • The webserver and cloudflared containers are attached to the app_network container network. This allows them to reach each other using DNS names.
  • The codingpackets.com domain is managed by Cloudflare and uses Cloudflare nameservers.
  • When a client browses to https://codingpackets.com, a connection is created between the client and Cloudflare. Cloudflare then creates a connection to the backend webserver over the secure tunnel via the cloudflared container. The clients request is routed from the cloudflared container to http://webserver:8001 via the webserver DNS name.

Project Structure

The following directory structure is applicable to this project.

Project Directory
.
├── bin/
├── Cargo.toml
├── config/cloudflared.yaml
├── dev/
├── docker-compose.yml
├── Dockerfile
├── src/
├── www/
└── .env

The files relevant to this post.

  • Dockerfile - Build container image for webserver.
  • docker-compose.yml - Manage container lifecycle.
  • cloudflared.yaml - Cloudflared service configuration.

Cloudflare

Cloudflare is doing alot of heavy lifting in this architecture. Cloudflare is hosting the domain and managing DNS recored, providing TLS certificates, and connecting to the backend via a Cloudflare Tunnel. Virtually all of this is free, I only pay for the cost of registering the domain which Cloudflare provides at the same cost the registrar charges.

DNS

To use Cloudflare tunnels, the nameservers for the domain needs to point to Cloudflare nameservers. For simplicity, I also have the domain registration managed by Cloudflare. This allows for automatic creation of DNS (CNAME) records that route the domain queries to the tunnel.

Create Tunnel

The process to create a tunnel via the dashboard is documented here. Be sure to create a cloudflared tunnel.

Once the tunnel is created, grab the tunnel authentication key from the 4. Run the following command step.

Tunnel Key

The tunnel key needs to be provided to the cloudflared container to allow it to connect to Cloudflare. There are a few ways to do this, For this post I have configured an environment variable: CF_TUNNEL_TOKEN in my ~/.zshrc file.

Public Hostnames

Once the tunnel is created, edit the tunnel and add a Public Hostname.

Public hostnames are used to point routes to the application backend. I have created the following records.

SubdomainDomainTypeURL
codingpackets.comHTTPwebserver:8001
wwwcodingpackets.comHTTPwebserver:8001
Important
webserver is the name for the backend service defined in the docker-compose.yml file.

When the records are creted, CNAME records will be automatically created for the domain that point to the tunnel id.

Config File

Create a config/cloudflared.yaml file with the following contents. This defines the tunnel config and will be passed to the cloudflared container on startup.

config/cloudflared.yaml
ingress:
  - hostname: codingpackets.com
    service: http://webserver:8001
  - service: http_status:404

Docker

I am using Docker to manage the containers for this project.

Dockerfile

The Dockerfile is used to build the Webservice Docker image. There are 2 stages in this Dockerfile, a builder stage and a runtime stage.

The builder stage, builds the webserver binary from the rust code. All the build dependencies are contained to this stage, reducing the size of the runtime image.

The runtime stage, builds the webserver image from which containers will be built. The compiled binary is copied to the final image from the builder stage.

Dockerfile
# Build stage
FROM rust:1.84-alpine AS builder

# Install build dependencies
RUN apk add --no-cache musl-dev

# Cache Build dependencies
WORKDIR /app
RUN mkdir ./src && echo 'fn main() { println!("Dummy!"); }' > ./src/main.rs
COPY ["Cargo.toml", "Cargo.lock",  "./"]
RUN cargo build --release

RUN rm -rf ./src
COPY ./src ./src

# The last modified attribute of main.rs needs to be updated manually,
# otherwise cargo won't rebuild it.
RUN touch -a -m ./src/main.rs

RUN cargo build --release

# Runtime stage
FROM alpine:3.19 AS runtime

ENV ENVIRONMENT="prod"
ENV WEB_PORT="8001"
ENV APP_NAME="codingpackets"
ENV APP_USER="appuser"
ENV APP_GROUP="appgroup"

RUN addgroup -S $APP_GROUP && \
  adduser -S $APP_USER -G $APP_GROUP

RUN mkdir /app && \
  chown -R $APP_USER:$APP_GROUP /app

COPY --from=builder /app/target/release/$APP_NAME /app

COPY ./www /app/www

RUN chown -R $APP_USER:$APP_GROUP /app

WORKDIR /app

USER $APP_USER:$APP_GROUP

EXPOSE $WEB_PORT

CMD "./$APP_NAME"

Environment

The .env file is used to set default environment variable values. They are passed to the docker-compose.yml file automagically.

.env
ENVIRONMENT="prod"
WEB_PORT="8001"
APP_NAME="codingpackets"
APP_USER="appuser"
APP_GROUP="appgroup"

Docker Compose

The docker-compose.yml file controls the lifecycle of the containers. The below file will manage both the backend webservice and cloudflared tunnel service.

The cloudflared service uses the previously created config/cloudflared.yaml file. Additionally, the CF_TUNNEL_TOKEN environment variable is passed in to authenticate the tunnel.

docker-compose.yml
services:
  webserver:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        - ENVIRONMENT=${ENVIRONMENT}
    image: localrepo/${APP_NAME}
    working_dir: /app
    command: ./${APP_NAME}
    environment:
      - ENVIRONMENT=${ENVIRONMENT}
    ports:
      - "${WEB_PORT}:${WEB_PORT}"
    networks:
      app_network:
    user: "${APP_USER}:${APP_GROUP}"

  cloudflared:
    image: cloudflare/cloudflared:latest
    command: --config /opt/config/cloudflared.yaml tunnel --no-autoupdate run
    environment:
      - TUNNEL_TOKEN=${CF_TUNNEL_TOKEN}
    volumes:
      - ./config/:/opt/config/
    depends_on:
      - webserver
    networks:
      app_network:

networks:
  app_network:

Build and Run

Once you have created the required files and environment variables, you can build and run the environment with docker compose up -d command.

This will build the webserver container image and download the containerd image, then, the containers will be started in the background.

Testing

  • Once the containers are started, you can browse to the webpage from the interent.
  • In the Cloudflare dashboard, the tunnel status will show healthy if everything is setup correctly.

Troubleshooting

  • Check the tunnel status in the Cloudflare dashboard.
  • Run the docker compose logs -f command to view the container logs.

Outro

In this post, I showed you how to host a backend webservice using Docker and publish it on the internet with Cloudflare tunnels. If you are exploring this architecture, I hope this post can save you some cycles.

Stay weird ✌️