InstallHTTPS install (Caddy)

HTTPS install (Caddy)

Install Bekci on an empty server with your own domain and an automatic Let's Encrypt certificate. Exactly which file goes where, step by step.

On this page

By the end of this guide Bekci will run on your own domain, such as https://bekci.example.com, with a valid HTTPS certificate. The Caddy web server obtains the certificate from Let's Encrypt and renews it before it expires; you don't have to do anything. Total time: about 10 minutes (plus waiting for DNS to propagate).

What you need#

  • A Linux server with a public IPv4 address (requirements).
  • A domain or subdomain, e.g. bekci.example.com, and access to its DNS settings (at your domain registrar or Cloudflare).
  • An email address for certificate notices.

The files you'll end up with#

Your server will have one folder with three files in it. You'll only edit one of them (.env):

Folder layout
/opt/bekci/
├── docker-compose.yml   ← Bekci + Caddy definition    (use as is)
├── Caddyfile            ← Caddy's configuration       (use as is)
└── .env                 ← your domain and email       (the only file you edit)

The name .env starts with a dot, so ls doesn't show it; ls -la does.

1. Add the DNS record#

In your domain's DNS settings, create a new A record:

FieldValue
TypeA
Name / Hostbekci — for bekci.example.com. For the domain itself (example.com) use @
Value / IPv4 addressYour server's public IPv4 address, e.g. 203.0.113.10
TTLAuto or 300

Your hosting provider's dashboard shows the server's public IP address; the first address printed by hostname -I on the server is usually the same. If you use IPv6 you can also add an AAAA record with the same name; if you do, make sure the server is really reachable over IPv6.

2. Install Docker#

If Docker isn't installed, follow the first step of the Docker guide (the official script installs the Compose plugin too).

3. Check ports 80 and 443#

First make sure the ports are free:

terminal
ss -ltnp 'sport = :80'
ss -ltnp 'sport = :443'

Then make sure the ports are reachable from outside:

  • Your hosting provider's firewall (Hetzner, DigitalOcean, AWS “security group”, etc.): allow incoming 80/TCP, 443/TCP and optionally 443/UDP (HTTP/3).
  • If you use ufw (ufw status says “Status: active”), add the rules; don't forget SSH:
terminal
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 443/udp

4. Create the folder#

terminal
mkdir -p /opt/bekci
cd /opt/bekci

Run all later commands from inside this folder. If you close the terminal and reconnect, type cd /opt/bekci first.

5. Create the three files#

You can create the files in one of two ways. Downloading is quicker and rules out copy-paste mistakes.

Create by hand

For each file, open an empty file with nano, paste the content below, save with Ctrl+O and Enter, and exit with Ctrl+X.

docker-compose.yml — nano /opt/bekci/docker-compose.yml

/opt/bekci/docker-compose.ymlDownload
# Bekci + Caddy: install with automatic HTTPS (Let's Encrypt).
# Step-by-step guide: https://bekci.app/en/docs/install/caddy/
#
# You don't need to change this file: the domain, email and version are read
# from the .env file. All three files live in the same folder (e.g. /opt/bekci):
#   docker-compose.yml   this file
#   Caddyfile            Caddy configuration
#   .env                 your domain and email
#
# Start:     docker compose up -d
# Logs:      docker compose logs -f bekci      (for Caddy: caddy)
# Update:    docker compose pull && docker compose up -d
# Backups, version pinning, rollback: https://bekci.app/en/docs/updates-backups/

name: bekci

services:
  bekci:
    # UPTIME_TAG: latest (SQLite, recommended) | postgres (embedded PostgreSQL)
    # or a fixed version: 1.0.0 / 1.0.0-postgres. Data can't be moved between
    # the two kinds: pick one from the start.
    image: kadirsungurlu/bekci:${UPTIME_TAG:-latest}
    restart: unless-stopped
    # Give the app (and embedded PostgreSQL) time to shut down cleanly.
    stop_grace_period: 30s
    environment:
      BASE_URL: https://${UPTIME_DOMAIN:?UPTIME_DOMAIN must be set in the .env file}
      TZ: ${TZ:-Europe/Istanbul}
      LOG_LEVEL: ${LOG_LEVEL:-info}
      MAX_CONCURRENT_CHECKS: ${MAX_CONCURRENT_CHECKS:-50}
    volumes:
      - bekci-data:/data
    # Not published directly; only reachable through Caddy.
    expose:
      - "8080"
    logging:
      driver: json-file
      options:
        max-size: 10m
        max-file: "3"

  caddy:
    # caddy:2-alpine (pinned by digest)
    image: caddy:2-alpine@sha256:6aeddd44c3078b0f9a35206472a11420648a79c184603ef95957d0a20044cb2b
    restart: unless-stopped
    depends_on:
      bekci:
        condition: service_healthy
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp" # HTTP/3
    environment:
      UPTIME_DOMAIN: ${UPTIME_DOMAIN}
      ACME_EMAIL: ${ACME_EMAIL:?ACME_EMAIL must be set in the .env file}
      STATUS_DOMAIN: ${STATUS_DOMAIN:-}
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
    logging:
      driver: json-file
      options:
        max-size: 10m
        max-file: "3"

volumes:
  bekci-data:
  caddy-data:
  caddy-config:

Caddyfile — nano /opt/bekci/Caddyfile (the file name is exactly Caddyfile: no extension, capital C)

/opt/bekci/CaddyfileDownload
# Caddy: obtains the HTTPS certificate automatically and forwards to Bekci.
# The domains come from the .env file; you don't need to change this file.
# Guide: https://bekci.app/en/docs/install/caddy/

{
	email {$ACME_EMAIL}
}

{$UPTIME_DOMAIN} {
	encode zstd gzip
	# Browsers should only use HTTPS for this domain for one year.
	# ">" means: if the app sends the same header, keep only this one.
	header >Strict-Transport-Security "max-age=31536000"
	reverse_proxy bekci:8080 {
		# Pass the live stream (Server-Sent Events) through without buffering.
		flush_interval -1
	}
}

# Optional: a separate domain for the status page (e.g. status.example.com).
# Set STATUS_DOMAIN in .env and remove the # signs at the start of the block
# below; then enter the same name in the status page's "Custom domain" field
# in the panel. That address only serves the status page, never the admin panel.
#
# {$STATUS_DOMAIN} {
# 	encode zstd gzip
# 	header >Strict-Transport-Security "max-age=31536000"
# 	reverse_proxy bekci:8080
# }

.env — nano /opt/bekci/.env (you'll edit its content in the next step)

/opt/bekci/.envDownload
# Bekci + Caddy settings. On the server this file must be named ".env" and sit
# in the same folder as docker-compose.yml (e.g. /opt/bekci/.env).
# Guide: https://bekci.app/en/docs/install/caddy/

# The panel's domain (without http:// and /). Its DNS A record must point here.
UPTIME_DOMAIN=bekci.example.com

# Your email address for Let's Encrypt certificate notices.
ACME_EMAIL=[email protected]

# latest: SQLite (recommended) | postgres: embedded PostgreSQL
# To pin a version: 1.0.0 (SQLite) or 1.0.0-postgres.
UPTIME_TAG=latest

# Time zone for daily summaries and the nightly backup.
TZ=Europe/Istanbul
LOG_LEVEL=info

# Maximum number of checks at the same time (raise it for hundreds of monitors).
MAX_CONCURRENT_CHECKS=50

# Optional: a separate domain for the status page (also enable the block in the Caddyfile).
# STATUS_DOMAIN=status.example.com

6. Put your domain in .env#

terminal
nano /opt/bekci/.env

Change just two lines; the rest can stay as they are:

/opt/bekci/.env (the lines you change)
UPTIME_DOMAIN=bekci.example.com
ACME_EMAIL=[email protected]
  • UPTIME_DOMAIN: the domain you created the DNS record for in step 1. Don't add https:// in front or / at the end.
  • ACME_EMAIL: your email address, where Let's Encrypt sends certificate notices.

The other lines: UPTIME_TAG is the Bekci version to run (version pinning), TZ is the time zone and MAX_CONCURRENT_CHECKS the maximum number of checks at the same time (all variables). Set TZ to your own zone, e.g. Europe/Berlin or UTC.

Save and exit (Ctrl+O, Enter, Ctrl+X).

7. Start it#

terminal
cd /opt/bekci
docker compose up -d

The first time, the images are downloaded. Caddy waits until Bekci is healthy, then starts and requests the certificate.

8. Verify the certificate#

Caddy's log tells you whether it obtained the certificate:

terminal
docker compose logs caddy | grep -i "certificate obtained"

If the line isn't there, wait a minute and check again. If it still isn't there, the "level":"error" lines in docker compose logs caddy tell you why; see common problems.

9. Create the admin account#

Open https://bekci.example.com. On the Welcome screen enter a username and a password of at least 8 characters, then click Create account.

In this setup BASE_URL is built from the domain in .env automatically (https:// + UPTIME_DOMAIN); you don't need to set it separately.

Common problems#

Port 80 or 443 is in use#

docker compose up -d stops with one of these errors:

Bind for 0.0.0.0:80 failed: port is already allocated
failed to bind host port 0.0.0.0:80/tcp: address already in use

The first means another container uses the port; docker ps --filter publish=80 shows which one (on servers running Coolify it's coolify-proxy). The second means a program running directly on the server (usually Nginx or Apache) uses it; ss -ltnp 'sport = :80' shows which one.

  • If you use that web server, put Bekci behind it instead of installing Caddy: behind a reverse proxy. With Coolify: Coolify.
  • If you don't use it, stop and disable it, e.g. systemctl disable --now nginx (for Apache: apache2), then run docker compose up -d again.

DNS hasn't propagated yet or is wrong#

Caddy's log shows errors about obtaining the certificate and the browser shows a security warning. If the getent command from step 1 doesn't print your server's IP, DNS isn't ready yet. Caddy retries on its own; once DNS points to the right address you can speed things up with:

terminal
docker compose restart caddy

I use Cloudflare#

  • For the first install the record must be DNS only (grey cloud); otherwise Let's Encrypt's validation can get stuck at Cloudflare.
  • If you turn the proxy (orange cloud) on after the certificate has been issued, set Cloudflare's SSL/TLS mode to Full (strict). Flexible mode causes an endless redirect loop (ERR_TOO_MANY_REDIRECTS).
  • Live updates work behind Cloudflare too: Bekci sends a keep-alive signal every 25 seconds to keep the connection open.
  • If certificate renewal causes trouble later, temporarily switch the record back to DNS only.

The browser says “not secure”#

The certificate hasn't been issued yet. Do the check from step 8. After many failed attempts Let's Encrypt refuses new requests for a while, so once you've fixed DNS and the ports, watch the log instead of running docker compose up -d over and over: docker compose logs -f caddy.

A change in .env had no effect#

After editing .env, run docker compose up -d; Compose recreates the changed containers. If you edited the Caddyfile, you also need docker compose restart caddy.

A separate domain for the status page (optional)#

You can publish your public status page at a separate address such as status.example.com. That address only serves the status page, never the admin panel.

  1. Add an A record named status pointing to the same server (like step 1).

  2. In .env, remove the # at the start of the last line and fill in the domain: STATUS_DOMAIN=status.example.com

  3. In the Caddyfile, remove the # signs at the start of the lines of the last block. The block should look like this:

    /opt/bekci/Caddyfile (end of the file)
    {$STATUS_DOMAIN} {
        encode zstd gzip
        header >Strict-Transport-Security "max-age=31536000"
        reverse_proxy bekci:8080
    }
  4. Apply it:

    terminal
    cd /opt/bekci
    docker compose up -d --force-recreate caddy
  5. In the panel, open your page under Status pages, enter status.example.com in the Custom domain field and save.

Next steps#

  • First steps: your first monitor, notification channel and status page.
  • Updates, backups and rollback: updating is as simple as cd /opt/bekci && docker compose pull && docker compose up -d, but learn how to back up first.