:bulb: How to make a Linux server behind an iptime router accessible from the outside via HTTPS using a DuckDNS domain. Environment: iptime router + Linux server (Ubuntu) + Docker

[00] Overall Architecture

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
External PC
  │
  │  https://mydomain.duckdns.org:443
  ▼
DuckDNS DNS server
  │  (domain → router external IP)
  ▼
iptime router (external IP)
  │  Port forwarding (80, 443 → internal server)
  ▼
Linux server
  │
  ├── Nginx (reverse proxy + SSL termination)
  │     │
  │     ├── service_a container
  │     ├── service_b container
  │     └── service_c container
  │
  └── Certbot (Let's Encrypt cert issue/renew)

[01] Why DuckDNS Instead of iptime DDNS?

The iptime.org domain has CAA (Certification Authority Authorization) records that block Let’s Encrypt from issuing certs:

1
2
0 issuewild ;
0 issue ;

DuckDNS allows CAA, so Let’s Encrypt cert issuance works. DuckDNS is also free and has been running stably since 2013.

:memo: Note: You can use iptime DDNS and DuckDNS together. External HTTPS → mydomain.duckdns.org, internal LAN → mydomain.iptime.org.

[02] Register a DuckDNS Domain

Location: external PC (browser)

  1. Visit https://www.duckdns.org and log in with Google/GitHub
  2. Under add domain, enter your desired name (e.g., mydomain)
  3. Your router’s current external IP is auto-registered
  4. Copy the token at the top of the page and save it

[03] DuckDNS Auto-IP-Update

Home internet typically has a dynamic IP, so the router’s external IP can change. You must auto-update DuckDNS so your domain always points to the current router IP.

:warning: iptime model differences: Some iptime models don’t support the User DDNS (external DDNS) menu. In that case, the Linux server itself must handle the update.

Method A — Router-Side DDNS (Supported Models)

Location: iptime admin page (192.168.0.1)

Under Advanced SettingsSpecial FeaturesDDNS, if User DDNS is available:

Field Value
DDNS Service User DDNS (or Custom)
URL https://www.duckdns.org/update?domains=mydomain&token=YOUR_TOKEN&ip=
Update interval 5 minutes

This is ideal — even when the server is off, the router updates the IP.

Method B — Server-Side Update (Unsupported Models)

Location: Linux server

On models without external DDNS, the server must update DuckDNS itself. Set up crontab (periodic) + systemd (immediate on boot) together.

B-1. Update Script

1
2
3
4
5
6
7
mkdir ~/duckdns

cat > ~/duckdns/duck.sh << 'EOF'
echo url="https://www.duckdns.org/update?domains=mydomain&token=YOUR_TOKEN&ip=" | curl -k -o ~/duckdns/duck.log -K -
EOF

chmod +x ~/duckdns/duck.sh

B-2. Register in crontab (every 5 minutes)

1
2
3
crontab -e
# Add:
# */5 * * * * ~/duckdns/duck.sh >/dev/null 2>&1

B-3. Update Immediately on Boot (systemd)

If you use WOL (Wake on LAN), the ISP may change the IP while the server is off. Register a systemd service that updates DuckDNS right after boot.

1
sudo nano /etc/systemd/system/duckdns.service
1
2
3
4
5
6
7
8
9
10
11
[Unit]
Description=DuckDNS IP update on boot
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/bin/bash /home/USERNAME/duckdns/duck.sh

[Install]
WantedBy=multi-user.target
1
2
3
4
5
sudo systemctl enable duckdns.service
sudo systemctl start duckdns.service

# Verify
cat ~/duckdns/duck.log   # "OK" means success

:memo: Method B summary: crontab updates every 5 minutes, systemd updates immediately on boot. As long as the server is up, the DuckDNS domain always points to the latest IP.

[04] iptime Router Port Forwarding

Location: iptime admin page (192.168.0.1)

Advanced SettingsNAT/Router ManagementPort Forwarding

Add these rules:

Rule External port Internal IP Internal port Purpose
certbot 80 server internal IP 80 Cert issuance
https 443 server internal IP 443 HTTPS service

:memo: Server internal IP: check on server with ip addr or hostname -I (usually 192.168.0.xxx)

:memo: Port 80: needed for cert renewal (every 90 days). Leaving it open and redirecting to 443 in Nginx has no real security impact.

[05] Linux Server — Docker Compose

Location: Linux server

Directory Structure

1
2
3
4
5
6
7
8
~/myserver/
├── docker-compose.yml
├── nginx/
│   └── nginx.conf
└── data/
    └── certbot/
        ├── conf/    ← cert storage
        └── www/     ← certbot webroot

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
services:
  nginx:
    image: nginx:alpine
    container_name: nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./data/certbot/conf:/etc/letsencrypt:ro
      - ./data/certbot/www:/var/www/certbot:ro

  certbot:
    image: certbot/certbot
    container_name: certbot
    restart: unless-stopped
    volumes:
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

  service_a:
    image: your-image-a
    container_name: service_a
    # No external ports needed — nginx connects internally

  service_b:
    image: your-image-b
    container_name: service_b

  service_c:
    image: your-image-c
    container_name: service_c

nginx/nginx.conf

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
events {}

http {
  # HTTPS redirect + certbot challenge
  server {
    listen 80;
    server_name mydomain.duckdns.org;

    location /.well-known/acme-challenge/ {
      root /var/www/certbot;
    }

    location / {
      return 301 https://$host$request_uri;
    }
  }

  # service_a
  server {
    listen 443 ssl;
    server_name mydomain.duckdns.org;

    ssl_certificate /etc/letsencrypt/live/mydomain.duckdns.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mydomain.duckdns.org/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
      proxy_pass http://service_a:3000;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
    }
  }

  # service_b (subdomain)
  server {
    listen 443 ssl;
    server_name b.mydomain.duckdns.org;

    ssl_certificate /etc/letsencrypt/live/mydomain.duckdns.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mydomain.duckdns.org/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
      proxy_pass http://service_b:4000;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
    }
  }
}

[06] First-Time Cert Issuance

Location: Linux server

6-1. Start Container with Temporary nginx.conf

Without a cert, Nginx will fail to load SSL config. First run with a temporary port-80-only config.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# nginx/nginx.conf (temporary)
events {}
http {
  server {
    listen 80;
    server_name mydomain.duckdns.org;

    location /.well-known/acme-challenge/ {
      root /var/www/certbot;
    }

    location / {
      return 200 'ok';
    }
  }
}
1
2
cd ~/myserver
docker compose up -d nginx

6-2. Issue Cert

1
2
3
4
5
6
7
docker compose run --rm certbot certonly \
  --webroot \
  -w /var/www/certbot \
  -d mydomain.duckdns.org \
  --email your@email.com \
  --agree-tos \
  --no-eff-email

On success, cert files appear under data/certbot/conf/live/mydomain.duckdns.org/.

6-3. Restore Final nginx.conf and Restart

After restoring the final nginx.conf from STEP 05:

1
2
docker compose down
docker compose up -d

[07] Verify Auto-Renewal

Location: Linux server

The certbot container in docker-compose.yml attempts renewal every 12 hours. Manual test:

1
docker compose run --rm certbot renew --dry-run

[08] Access Test

1
2
3
4
5
# From external PC
curl -I https://mydomain.duckdns.org

# Or in a browser
# Visit https://mydomain.duckdns.org → verify the padlock icon

[09] Troubleshooting

Nginx Won’t Start

1
2
3
docker logs nginx
# Check for SSL cert path errors
# Before cert issuance, run with the temporary config first

Certbot Issuance Fails

  • Check router port-80 forwarding is set
  • Verify http://mydomain.duckdns.org/.well-known/acme-challenge/test is reachable from outside
  • Confirm DuckDNS domain currently points to your router’s IP

DuckDNS Points to Wrong IP

1
2
3
4
5
# Check current router external IP
curl ifconfig.me

# Manually update DuckDNS
curl "https://www.duckdns.org/update?domains=mydomain&token=YOUR_TOKEN&ip=$(curl -s ifconfig.me)"

Can’t Reach DuckDNS After WOL (Method B)

1
2
3
4
5
6
7
8
9
10
# Check systemd service status
sudo systemctl status duckdns.service

# Manual update
~/duckdns/duck.sh
cat ~/duckdns/duck.log   # confirm OK

# Re-register if needed
sudo systemctl daemon-reload
sudo systemctl enable duckdns.service

[10] Summary

Step Location Task
STEP 02 External PC browser DuckDNS domain registration
STEP 03 Router or server DuckDNS auto-IP-update (Method A: router / Method B: server)
STEP 04 Router admin page Add port 80, 443 forwarding
STEP 05 Linux server Docker Compose + Nginx setup
STEP 06 Linux server Let’s Encrypt cert issuance
STEP 07 Linux server Confirm auto-renewal works