Docker services that accept HTTP requests usually listen on an internal container port: for example, 3000, 8080, 5000, or 8000. This port is used inside the Docker network, while external access to the service is better organized through a domain, HTTPS, and a reverse proxy.

In a production environment, the user opens the service at:

https://app.example.com

The domain points to the public IP address of the VPS, Traefik accepts the request and forwards it to the internal Docker service.

The scheme looks like this:

User → https://app.example.com → Traefik → Docker service

1. Point the Domain to the VPS

First, create a DNS record for the domain or subdomain.

Example:

Type: A
Name: app
Value: SERVER_IP

If the domain is example.com, the service will be available at:

app.example.com

You can check DNS with the command:

dig app.example.com +short

Expected result:

SERVER_IP

2. Prepare the Docker Service

Assume the application runs inside the container on port 3000.

Example of a basic service:

services:
 app:
   image: your-app-image:latest
   container_name: app
   restart: unless-stopped
   expose:
     - "3000"

Here, expose is used instead of ports because the application does not need to be exposed directly to the internet. It will be available only inside the Docker network for Traefik.

Bad:

ports:
  - "3000:3000"

Better:

expose:
  - "3000"

Important
Do not publish the internal application port to the outside if access should go only through HTTPS and a reverse proxy.

3. Add Traefik as a Reverse Proxy

Below is an example of a docker-compose.yml file where Traefik publishes the application through HTTPS.

Replace:

app.example.com
admin@example.com
your-app-image:latest

with your own values.

services:
 traefik:
   image: traefik:v3.0
   container_name: traefik
   restart: unless-stopped
   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"

     - "--certificatesresolvers.letsencrypt.acme.email=admin@example.com"
     - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
     - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
     - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
   ports:
     - "80:80"
     - "443:443"
   volumes:
     - "/var/run/docker.sock:/var/run/docker.sock:ro"
     - "./letsencrypt:/letsencrypt"
   networks:
     - proxy

 app:
   image: your-app-image:latest
   container_name: app
   restart: unless-stopped
   expose:
     - "3000"
   labels:
     - "traefik.enable=true"

     - "traefik.http.routers.app.rule=Host(`app.example.com`)"
     - "traefik.http.routers.app.entrypoints=websecure"
     - "traefik.http.routers.app.tls=true"
     - "traefik.http.routers.app.tls.certresolver=letsencrypt"

     - "traefik.http.services.app.loadbalancer.server.port=3000"
   networks:
     - proxy

networks:
 proxy:
   name: proxy

Traefik uses routers, services, and entrypoints to process HTTP requests. In a Docker scenario, these parameters are usually set through container labels.

4. What This Configuration Does

In this example, Traefik:

  • listens on ports 80 and 443;
  • gets information about containers through the Docker socket;
  • does not publish all containers automatically;
  • looks only for containers with traefik.enable=true;
  • accepts requests for app.example.com;
  • issues an HTTPS certificate through Let’s Encrypt;
  • proxies requests to the app:3000 container.

Key block for the application:

labels:
 - "traefik.enable=true"
 - "traefik.http.routers.app.rule=Host(`app.example.com`)"
 - "traefik.http.routers.app.entrypoints=websecure"
 - "traefik.http.routers.app.tls=true"
 - "traefik.http.routers.app.tls.certresolver=letsencrypt"
  - "traefik.http.services.app.loadbalancer.server.port=3000"

What this means:

traefik.enable=true

Traefik should process this container.

Host(`app.example.com`)

Requests to this domain are routed to the container.

entrypoints=websecure

The service works through the HTTPS entrypoint, that is, through port 443.

tls.certresolver=letsencrypt

A Let’s Encrypt certificate should be issued for this domain.

loadbalancer.server.port=3000

The internal application port inside the container is 3000.

Traefik supports ACME/Let’s Encrypt for automatic certificate issuance. In the official Traefik Docker Compose example with Let’s Encrypt, the certificate is issued through an HTTP challenge for a service published through Traefik.

5. Start the Services

Create a folder for certificates:

mkdir -p letsencrypt

Start the containers:

docker compose up -d

Check the status:

docker ps

The following containers should be running:

traefik
app

Check Traefik logs:

docker logs traefik

If DNS is configured correctly and ports 80/443 are available, Traefik will be able to issue an HTTPS certificate.

6. Open Ports in the Firewall

The VPS must have these ports open:

80/tcp
443/tcp

If UFW is used:

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status

It is better to allow SSH only from a trusted IP:

sudo ufw allow from YOUR_TRUSTED_IP to any port 22 proto tcp

7. Check HTTPS

Open in the browser:

https://app.example.com

You can also check it with curl:

curl -I https://app.example.com

Expected result:

HTTP/2 200

or another successful response from the application.

Если открыть:

http://app.example.com

Traefik should redirect the request to HTTPS because the configuration includes a redirect from web to websecure:

- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"

Conclusion

To publish a Docker service through HTTPS on a VPS, you can use Traefik as a reverse proxy.

In a simple setup, the scheme looks like this:

Domain → VPS → Traefik → Docker service

The application does not need to be exposed directly through:

SERVER_IP:PORT

It is enough to keep it inside the Docker network and publish only ports 80 and 443 through Traefik.

Traefik is convenient for Docker services because routing is described directly in docker-compose.yml through labels. This is especially useful if you need to publish several services on one VPS:

app.example.com → app:3000
api.example.com → api:8080
panel.example.com → panel:9000

This approach is safer and more convenient for a production setup: the user opens the service through a domain with HTTPS, while the internal application port remains hidden.