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
- What you need
- The files you'll end up with
- 1. Add the DNS record
- 2. Install Docker
- 3. Check ports 80 and 443
- 4. Create the folder
- 5. Create the three files
- 6. Put your domain in .env
- 7. Start it
- 8. Verify the certificate
- 9. Create the admin account
- Common problems
- Port 80 or 443 is in use
- DNS hasn't propagated yet or is wrong
- I use Cloudflare
- The browser says “not secure”
- A change in .env had no effect
- A separate domain for the status page (optional)
- Next steps
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):
/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:
| Field | Value |
|---|---|
| Type | A |
| Name / Host | bekci — for bekci.example.com. For the domain itself (example.com) use @ |
| Value / IPv4 address | Your server's public IPv4 address, e.g. 203.0.113.10 |
| TTL | Auto 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:
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 statussays “Status: active”), add the rules; don't forget SSH:
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 443/udp4. Create the folder#
mkdir -p /opt/bekci
cd /opt/bekciRun 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.
Download (recommended)
cd /opt/bekci
curl -fsSL -o docker-compose.yml https://bekci.app/indir/en/caddy/docker-compose.yml
curl -fsSL -o Caddyfile https://bekci.app/indir/en/caddy/Caddyfile
curl -fsSL -o .env https://bekci.app/indir/en/caddy/env.example
ls -laThe third command saves the example settings file under the name .env; the leading dot matters.
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
# 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)
# 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)
# 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.com6. Put your domain in .env#
nano /opt/bekci/.envChange just two lines; the rest can stay as they are:
UPTIME_DOMAIN=bekci.example.com
ACME_EMAIL=[email protected]UPTIME_DOMAIN: the domain you created the DNS record for in step 1. Don't addhttps://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#
cd /opt/bekci
docker compose up -dThe 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:
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 allocatedfailed to bind host port 0.0.0.0:80/tcp: address already in useThe 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 rundocker compose up -dagain.
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:
docker compose restart caddyI 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.
Add an A record named
statuspointing to the same server (like step 1).In
.env, remove the#at the start of the last line and fill in the domain:STATUS_DOMAIN=status.example.comIn 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 }Apply it:
terminalcd /opt/bekci docker compose up -d --force-recreate caddyIn the panel, open your page under Status pages, enter
status.example.comin 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.
Something missing or wrong on this page? Report it on GitHub or email [email protected]