Skip to content

Self-hosting

L1nkZip is distributed as a Docker image, although the full code can be found on the Github repository in case you want to use it directly or contribute. It can be run as a Docker container or deployed to a Kubernetes cluster. Litestream is not required to run L1nkZip, but it is strongly encouraged if you are going to use sqlite.

Requirements

L1nkZip would not be possible without the amazings FastApi and PonyORM projects. The only requirement is Python 3.12+ if you use sqlite. Otherwise, you must install the database driver for your database of choice.

The official Docker image comes with sqlite and postgresql support, but you can extend it to support other databases.

  • mysql: MySQLdb
  • oracle: cx_oracle

Example from a Dockerfile extending the Docker image to support MySQL:

FROM dorogoy/l1nkzip:latest

RUN uv pip install --system --no-cache-dir MySQL-python

For more information check the PonyORM documentation.

Configuration

Required environment variables

  • API_DOMAIN: Domain of the API. This is the domain of the shortened URLs.
  • DB_TYPE: Database type. Supported values are inmemory, sqlite and postgresql. Other databases like mysql, oracle, and cockroachdb are also supported thanks to PonyORM, but require additional drivers.
  • DB_NAME: Database name. Used for sqlite and postgresql.
  • TOKEN: Token used to authenticate some administrative actions to the API. This is a secret value and should not be shared.
  • GENERATOR_STRING: String used to generate the shortened URLs. This is a secret value and should not be shared. You can shuffle uppercase, lowercase letters and/or numbers without repeating them.

Optional environment variables

  • API_NAME: Name of the API. Used in the OpenAPI documentation. Default: L1nkZip
  • DB_HOST: Database host. Used for postgresql.
  • DB_USER: Database user. Used for postgresql.
  • DB_PASSWORD: Database password. Used for postgresql.
  • DB_DSN: Database DSN. Used for Oracle.
  • SITE_URL: URL of the website. Used when the API is visited from a browser.
  • PHISHTANK: Enable protection against phishing. Default: false. The values can also be "anonymous" or your actual PhishTank key. More details below.
  • REDIS_SERVER: Redis server URL for caching (e.g., redis://localhost:6379/0). Optional.
  • REDIS_TTL: Time-to-live for cached URLs in seconds. Default: 86400 (24 hours).
  • RATE_LIMIT_CREATE: Rate limit for URL creation. Default: "10/minute".
  • RATE_LIMIT_REDIRECT: Rate limit for URL redirection. Default: "120/minute".
  • METRICS_ENABLED: Enable Prometheus metrics endpoint. Default: false.
  • LOG_LEVEL: Logging level. Default: "INFO". Options: "DEBUG", "INFO", "WARN", "ERROR".
  • LOG_FORMAT: Log format. Default: "text". Set to "json" for structured logging.

Phishtank support

L1nkZip can be configured to check if the URL to be shortened is in the PhishTank database. This is an optional feature and can be enabled by setting the PHISHTANK environment variable to anonymous or your actual PhishTank key.

To avoid overloading the service, the Phishtank database is downloaded into the local L1nkZip database. This action will be launched each time the update endpoint is contacted successfully. The update endpoint is protected by the TOKEN environment variable and should be called periodically by a cronjob or similar. Please, be respectful with the Phishtank policies. Check the API documentation for more details about the update endpoint.

Each update to the Phishtank database can add new entries and remove old ones. This will keep the size of the database under control, it is not growing all the time. When an entry is removed from the database, the shortened URL will be allowed/reactivated again. Keep in mind that L1nkZip is not a censorship tool and it is not intended to be used as such.

Docker image

The docker image is available on Docker Hub.

docker pull dorogoy/l1nkzip

Kubernetes manifest

This is an example of a StatefulSet to deploy L1nkZip to a Kubernetes cluster. The required secrets are not included and it uses a sqlite database with litestream. This example is for a S3 compatible service (idrive e2), Amazon S3 configuration for AWS is slightly different. Please, have a look at the Litestream documentation for more details.

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: litestream
data:
  litestream.yml: |
    dbs:
      - path: /data/l1nkzip.db
        replicas:
          - type: s3
            bucket: <your-bucket>
            path: l1nkzip.db
            endpoint: <your-endpoint>
            region: <your-region>
            force-path-style: true
---
apiVersion: v1
kind: Service
metadata:
  name: l1nkzip
spec:
  selector:
    app: l1nkzip
  type: ClusterIP
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 80
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: l1nkzip
spec:
  selector:
    matchLabels:
      app: l1nkzip
  serviceName: l1nkzip
  replicas: 1
  template:
    metadata:
      labels:
        app: l1nkzip
    spec:
      initContainers:
        - name: init-litestream
          image: litestream/litestream:0.3.9
          args:
            [
              "restore",
              "-if-db-not-exists",
              "-if-replica-exists",
              "-v",
              "$(DB_NAME)",
            ]
          volumeMounts:
            - name: l1nkzip
              mountPath: /data
            - name: litestream
              mountPath: /etc/litestream.yml
              subPath: litestream.yml
          env:
            - name: LITESTREAM_ACCESS_KEY_ID
              valueFrom:
                secretKeyRef:
                  name: l1nkzip-secret
                  key: LITESTREAM_ACCESS_KEY_ID
            - name: LITESTREAM_SECRET_ACCESS_KEY
              valueFrom:
                secretKeyRef:
                  name: l1nkzip-secret
                  key: LITESTREAM_SECRET_ACCESS_KEY
            - name: DB_NAME
              valueFrom:
                configMapKeyRef:
                  name: l1nkzip-config
                  key: DB_NAME
      containers:
        - name: litestream
          image: litestream/litestream:0.3.9
          args: ["replicate"]
          volumeMounts:
            - name: l1nkzip
              mountPath: /data
            - name: litestream
              mountPath: /etc/litestream.yml
              subPath: litestream.yml
          env:
            - name: LITESTREAM_ACCESS_KEY_ID
              valueFrom:
                secretKeyRef:
                  name: l1nkzip-secret
                  key: LITESTREAM_ACCESS_KEY_ID
            - name: LITESTREAM_SECRET_ACCESS_KEY
              valueFrom:
                secretKeyRef:
                  name: l1nkzip-secret
                  key: LITESTREAM_SECRET_ACCESS_KEY
          ports:
            - name: metrics
              containerPort: 9090
        - name: l1nkzip
          image: dorogoy/l1nkzip:latest
          imagePullPolicy: Always
          ports:
            - name: http
              containerPort: 80
          volumeMounts:
            - name: l1nkzip
              mountPath: /data
            - name: litestream
              mountPath: /etc/litestream.yml
              subPath: litestream.yml
          env:
            - name: TOKEN
              valueFrom:
                secretKeyRef:
                  name: l1nkzip-secret
                  key: TOKEN
            - name: GENERATOR_STRING
              valueFrom:
                secretKeyRef:
                  name: l1nkzip-secret
                  key: GENERATOR_STRING
            - name: PHISHTANK
              value: "anonymous"
            - name: DB_TYPE
              value: sqlite
            - name: DB_NAME
              valueFrom:
                configMapKeyRef:
                  name: l1nkzip-config
                  key: DB_NAME
            - name: API_DOMAIN
              valueFrom:
                configMapKeyRef:
                  name: l1nkzip-config
                  key: API_DOMAIN
            - name: SITE_URL
              valueFrom:
                configMapKeyRef:
                  name: l1nkzip-config
                  key: SITE_URL
      volumes:
        - name: litestream
          configMap:
            name: litestream
        - name: l1nkzip
          persistentVolumeClaim:
            claimName: l1nkzip
  volumeClaimTemplates:
    - metadata:
        name: l1nkzip
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 1Gi

Docker Compose Example

This example shows how to run L1nkZip with PostgreSQL and Redis using Docker Compose:

version: '3.8'

services:
  postgres:
    image: postgres:17-alpine
    environment:
      POSTGRES_DB: l1nkzip
      POSTGRES_USER: l1nkzip
      POSTGRES_PASSWORD: your-postgres-password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U l1nkzip -d l1nkzip"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  l1nkzip:
    image: dorogoy/l1nkzip:latest
    ports:
      - "8000:80"
    environment:
      # Required environment variables
      API_DOMAIN: "http://localhost:8000"
      DB_TYPE: "postgres"
      DB_NAME: "l1nkzip"
      DB_HOST: "postgres"
      DB_PORT: "5432"
      DB_USER: "l1nkzip"
      DB_PASSWORD: "your-postgres-password"
      TOKEN: "your-secret-admin-token"
      GENERATOR_STRING: "your-custom-alphabet-here"

      # Optional environment variables
      PHISHTANK: "anonymous"
      REDIS_SERVER: "redis://redis:6379/0"
      REDIS_TTL: "86400"
      RATE_LIMIT_CREATE: "10/minute"
      RATE_LIMIT_REDIRECT: "120/minute"
      METRICS_ENABLED: "true"
      LOG_LEVEL: "INFO"
      LOG_FORMAT: "json"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:

To use this configuration:

  1. Create a docker-compose.yml file with the above content
  2. Replace the placeholder values:
  3. your-postgres-password: A strong password for PostgreSQL
  4. your-secret-admin-token: A secure token for admin operations
  5. your-custom-alphabet: A shuffled string of characters for URL generation (e.g., "aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789")
  6. Run: docker-compose up -d

The API will be available at http://localhost:8000 with: - PostgreSQL database for persistent storage - Redis caching for improved performance - JSON structured logging - Prometheus metrics at /metrics - Health checks at /health

Monitoring and Observability

L1nkZip supports comprehensive monitoring through Prometheus metrics and structured logging.

Metrics Endpoint

When METRICS_ENABLED=true is set, L1nkZip exposes a Prometheus metrics endpoint at /metrics. This includes:

  • HTTP request metrics (count, latency, error rates)
  • Cache performance metrics (hit/miss rates)
  • Business metrics (URLs created, redirects, phishing blocks)
  • Database connection metrics

Structured Logging

Set LOG_FORMAT=json to enable JSON-formatted structured logging with fields including: - Request tracing IDs - Response times - Error details - Cache operations - Phishing detection events

Example Monitoring Configuration

# Add to your Kubernetes deployment env vars
- name: METRICS_ENABLED
  value: "true"
- name: LOG_FORMAT
  value: "json"
- name: LOG_LEVEL
  value: "INFO"

Grafana Dashboards

Pre-built Grafana dashboards are available for: - API performance monitoring - Cache efficiency analysis - Business metrics tracking - Error rate and latency monitoring

Alerting

Configure Prometheus alert rules for: - High error rates (>5% for 5 minutes) - Service availability (container down) - High latency (95th percentile >1 second) - Low cache hit rate (<70%) - High phishing block rates

Health Checks

The health endpoint at /health provides: - Database connectivity status - Service availability - Basic dependency checks