# How to Configure a Self-Hosted Zakirullin Files Sync Server with TLS

> Set up a self-hosted Zakirullin Files sync server with TLS encryption. Easily enable HTTPS using Let's Encrypt and Go autocert with minimal environment variables. Secure your file sync today.

- Repository: [Artem Zakirullin/files.md](https://github.com/zakirullin/files.md)
- Tags: how-to-guide
- Published: 2026-05-21

---

**Zakirullin Files provides automatic TLS via Let's Encrypt using the Go `autocert` package, requiring only the `CERT_DIR`, `API_URL`, and `APP_URL` environment variables to enable HTTPS on ports 80 and 443.**

Zakirullin Files is an open-source markdown-based file synchronization platform that ships with built-in TLS support. Configuring a **self-hosted Zakirullin Files sync server with TLS** requires understanding how the Go-based server handles certificate automation through environment variables and the `autocert` package. The implementation automatically manages Let's Encrypt certificates while providing HTTP-to-HTTPS redirection without manual certificate handling.

## Built-in TLS Architecture

The server implements a dual-listener architecture that separates ACME challenge handling from secure API traffic. This design eliminates manual certificate management while ensuring encrypted connections by default.

### Automatic Certificate Provisioning

The core TLS logic resides in [`server/sync/autocert.go`](https://github.com/zakirullin/files.md/blob/main/server/sync/autocert.go), which creates an `autocert.Manager` configured with a `HostPolicy` restricting certificates to specific hostnames. The manager’s `GetCertificate` hook is wired into the TLS configuration in [`server/sync/webserver.go`](https://github.com/zakirullin/files.md/blob/main/server/sync/webserver.go) (lines 75-94), allowing the server to obtain and renew **Let's Encrypt** certificates on-the-fly. When `ListenAndServeTLS("", "")` is called, the manager supplies certificates dynamically rather than loading static key files from disk.

### HTTP Challenge and Redirect Handling

Port **80** serves two purposes: handling the ACME "http-01" challenge and redirecting all other traffic to HTTPS. The `certServer()` function in [`server/sync/autocert.go`](https://github.com/zakirullin/files.md/blob/main/server/sync/autocert.go) spawns this plain-HTTP server, which uses `autocertManager.HTTPHandler(nil)` to intercept validation requests while sending all standard traffic to the HTTPS port. Port **443** exclusively serves the API and Progressive Web App (PWA) using the TLS configuration that delegates to the `autocert` manager.

## Core Configuration Components

### Environment Variables in server/config/config.go

The `config.LoadBotConfig()` function reads environment variables that control TLS behavior. Three variables are critical for certificate automation:

- **`CERT_DIR`** (default: `/tmp`): Specifies the writable directory where `autocert` caches issued certificates, preventing rate-limit hits across restarts.
- **`API_URL`**: The full HTTPS URL (e.g., `https://api.files.md`) for the API endpoint; the hostname is extracted via `hostOf()` and fed to `autocert.HostWhitelist`.
- **`APP_URL`**: The full HTTPS URL for the PWA frontend, also validated against the host whitelist.

Both URLs must include the `https://` scheme and resolve to the server's public IP address.

### Certificate Manager Setup in server/sync/autocert.go

This file defines the `autocert.Manager` with strict hostname validation. The `HostWhitelist` is populated from `config.ServerCfg.APIHost()` and `config.ServerCfg.AppHost()`, ensuring certificates are only requested for explicitly configured domains. The manager handles all ACME protocol negotiations with Let's Encrypt, including the HTTP-01 challenge responses served on port 80.

### TLS Server Initialization in server/sync/webserver.go

When `sync.Serve()` detects a non-empty `certDir`, it constructs a `tls.Config` that references the manager’s `GetCertificate` method. The HTTPS server starts with `ListenAndServeTLS("", "")`, signaling Go to use the dynamic certificate provider. If `certDir` is empty, the server falls back to plain HTTP on port **8080** (lines 61-73), which is unsuitable for production but useful for local development.

## Step-by-Step TLS Configuration

Follow these steps to deploy a production-ready TLS-enabled server:

1. **Configure DNS**: Create A records pointing your domains (e.g., `api.example.com` and `app.example.com`) to your server's public IP address.

2. **Set Environment Variables**: Create an `.env` file or export variables directly:

   ```dotenv
   API_URL=https://api.example.com
   APP_URL=https://app.example.com
   CERT_DIR=/opt/filesmd/certs
   LOG_FILE=/var/log/filesmd/server.log
   ```

3. **Open Firewall Ports**: Allow inbound TCP traffic on ports 80 and 443. The server must be reachable from the internet for Let's Encrypt validation.

4. **Deploy the Service**: Run the initialization and deployment commands as documented in the repository. The first startup will trigger certificate issuance; subsequent restarts load cached certificates from `CERT_DIR`.

5. **Verify Installation**: Navigate to `https://api.example.com` and `https://app.example.com`. The browser should display valid Let's Encrypt certificates without warnings.

## Implementing Custom Certificates

For environments requiring corporate PKI certificates instead of Let's Encrypt, bypass `autocert` by providing a custom `GetCertificate` implementation:

```go
tlsCfg := &tls.Config{
    GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
        cert, err := tls.LoadX509KeyPair("/etc/ssl/certs/server.crt", "/etc/ssl/private/server.key")
        if err != nil {
            return nil, err
        }
        return &cert, nil
    },
}

srv := &http.Server{
    Addr:      ":443",
    TLSConfig: tlsCfg,
    Handler:   sync.Router(),
}

log.Fatal(srv.ListenAndServeTLS("", ""))

```

To use this approach, omit the `CERT_DIR` environment variable or set it to an empty string when calling `sync.Serve()`, forcing the server to skip automatic certificate management.

## Summary

- **Automatic TLS** is provided via the `autocert` Go package in [`server/sync/autocert.go`](https://github.com/zakirullin/files.md/blob/main/server/sync/autocert.go), handling Let's Encrypt issuance and renewal without manual intervention.
- **Port 80** handles ACME challenges and HTTP-to-HTTPS redirects, while **port 443** serves encrypted API and PWA traffic using certificates supplied by the `autocert.Manager`.
- **Configuration** requires setting `CERT_DIR` (writable path), `API_URL`, and `APP_URL` in [`server/config/config.go`](https://github.com/zakirullin/files.md/blob/main/server/config/config.go) before calling `sync.Serve()`.
- **Certificate persistence** relies on the `CERT_DIR` cache; ensure this directory is backed up to avoid hitting Let's Encrypt rate limits during restarts.
- **Custom certificates** are supported by implementing a custom `tls.Config.GetCertificate` function and omitting the `CERT_DIR` parameter.

## Frequently Asked Questions

### What ports must be open for the TLS setup?

Ports **80** and **443** must be accessible from the internet. Port 80 handles the ACME HTTP-01 challenge required for Let's Encrypt validation and redirects all other traffic to HTTPS. Port 443 serves the actual encrypted API and PWA traffic. Firewall rules and NAT configurations must permit inbound TCP connections on both ports.

### How does the server handle certificate persistence?

The `autocert.Manager` caches issued certificates in the directory specified by the `CERT_DIR` environment variable (defaulting to `/tmp`). This persistent storage allows the server to restart without re-requesting certificates from Let's Encrypt, preventing unnecessary load on ACME servers and avoiding rate limits. Ensure the process has write permissions to this directory.

### Can I use custom certificates instead of Let's Encrypt?

Yes. To use corporate or self-signed certificates, omit the `CERT_DIR` environment variable and implement a custom `GetCertificate` function in your TLS configuration that loads PEM-encoded key pairs using `tls.LoadX509KeyPair()`. When `certDir` is empty, the server logic in [`server/sync/webserver.go`](https://github.com/zakirullin/files.md/blob/main/server/sync/webserver.go) defaults to HTTP on port 8080, so you must manually construct and start the HTTPS server with your custom `tls.Config`.

### What happens if the certificate directory is not writable?

If the process lacks write permissions to `CERT_DIR`, the `autocert.Manager` cannot cache certificates, causing it to request new certificates on every restart. This rapidly exhausts the Let's Encrypt rate limits (currently 50 certificates per domain per week), resulting in failed TLS handshakes and service interruption. Always ensure `CERT_DIR` points to a writable, persistent volume.