Skip to content

Self-Hosting

Deploy dTax on your own Linux server in under 15 minutes using Docker Compose.

Prerequisites

  • Linux server (Ubuntu 22.04+ recommended), 2+ GB RAM
  • Docker Engine 24+ and Docker Compose v2
  • A domain name pointed to your server’s IP
  • Ports 80 and 443 open in your firewall

Quick Start

  1. Clone the repository

    Terminal window
    git clone https://github.com/dTaxLab/dtax.git
    cd dtax
  2. Configure environment

    Terminal window
    cp .env.production.example .env

    Edit .env and set all required values:

    VariableDescription
    POSTGRES_PASSWORDStrong database password
    JWT_SECRETRandom string, at least 32 characters
    ENCRYPTION_KEY64-character hex string — see note below
    CORS_ORIGINYour domain, e.g. https://tax.example.com
    APP_URLSame domain (used in password-reset emails)
    NEXT_PUBLIC_API_URLhttps://tax.example.com/api (build-time, must be set before docker compose build)
  3. Start services

    This starts PostgreSQL, Redis, API, Web, and nginx. The migrate service runs database migrations automatically.

    Verify everything is running:

    Terminal window
    docker compose ps
    curl http://localhost/api/health

Architecture

┌──────────┐
:80/:443 ────│ nginx │
└────┬─────┘
┌────┴─────┐
┌─────│ routes │─────┐
│ └──────────┘ │
┌────┴───┐ ┌─────┴────┐
│ API │ │ Web │
│ :3001 │ │ :3000 │
│ │
│ Sync │ (embedded in API process)
│ Worker │ polls DB every 5s
│ ├── WALLET_SYNC jobs
│ └── PRICE_ENRICH jobs → auto tax calc → email
└────┬───┘
┌────┴───┐ ┌───────┐
│Postgres│ │ Redis │
└────────┘ └───────┘

The Sync Worker runs inside the API process and polls PostgreSQL for pending jobs every 5 seconds. It handles two job types:

  • WALLET_SYNC — fetches on-chain or exchange transactions
  • PRICE_ENRICH — backfills missing USD prices, then automatically calculates FIFO tax reports and sends a summary email

No separate worker container is needed. Configure RESEND_API_KEY + FROM_EMAIL + APP_URL to enable tax-ready email notifications.

TLS with Let’s Encrypt

  1. Update nginx config

    Edit docker/nginx/nginx.conf and replace server_name _; with your domain:

    server_name tax.example.com;
  2. Obtain certificate

    Terminal window
    docker compose up -d nginx
    docker run --rm \
    -v dtax_letsencrypt:/etc/letsencrypt \
    -v dtax_certbot-data:/var/www/certbot \
    certbot/certbot certonly \
    --webroot -w /var/www/certbot \
    -d tax.example.com \
    --agree-tos --email you@example.com
  3. Enable HTTPS

    Add an HTTPS server block to docker/nginx/nginx.conf:

    server {
    listen 443 ssl http2;
    server_name tax.example.com;
    ssl_certificate /etc/letsencrypt/live/tax.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/tax.example.com/privkey.pem;
    include /etc/nginx/snippets/ssl-params.conf;
    location /api/ {
    proxy_pass http://api/api/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 300s;
    }
    location / {
    proxy_pass http://web;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    }
    }
    Terminal window
    docker compose restart nginx
  4. Auto-renew certificates

    Terminal window
    echo "0 3 * * * docker run --rm \
    -v dtax_letsencrypt:/etc/letsencrypt \
    -v dtax_certbot-data:/var/www/certbot \
    certbot/certbot renew --quiet \
    && docker compose restart nginx" | crontab -

First Admin User

  1. Register at https://yourdomain.com/register
  2. Promote your account to admin:
Terminal window
docker compose exec postgres psql -U dtax -c \
"UPDATE \"User\" SET role='ADMIN' WHERE email='your@email.com';"

Updates

Terminal window
git pull
docker compose build
docker compose up -d

The migrate service runs automatically to apply new database migrations.

Backups

Terminal window
# Run backup
chmod +x docker/scripts/backup.sh
./docker/scripts/backup.sh ./backups
# Schedule daily at 2am
echo "0 2 * * * /path/to/dtax/docker/scripts/backup.sh /path/to/dtax/backups" | crontab -
# Restore from backup
gunzip -c backups/dtax_20260313_020000.sql.gz | \
docker compose exec -T postgres psql -U dtax dtax

Optional Services

ServiceEnvironment VariablesPurpose
ResendRESEND_API_KEY, FROM_EMAILEmail verification, password reset, and automatic tax-ready notifications — without this, password reset, email verification, and post-enrichment tax report emails are silently skipped
StripeSTRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRO_PRICE_ID, STRIPE_CPA_PRICE_IDSubscription billing
AnthropicANTHROPIC_API_KEYAI transaction classification & chat
EtherscanETHERSCAN_API_KEYEVM blockchain indexing (Ethereum, Polygon, BSC, Arbitrum, Optimism)
SolscanSOLSCAN_API_KEYSolana blockchain indexing
PostHogNEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_POSTHOG_HOSTProduct analytics
SentrySENTRY_DSNError tracking

All optional services degrade gracefully when not configured.

Troubleshooting

Services fail to start

Terminal window
docker compose logs api # Check API logs
docker compose logs web # Check Web logs
docker compose logs nginx # Check nginx logs

API crashes immediately

Check for FATAL: ENCRYPTION_KEY in the API logs — this means your ENCRYPTION_KEY is not a valid 64-character hex string. Regenerate with openssl rand -hex 32.

Database connection errors

Ensure POSTGRES_PASSWORD in .env matches the value used when the postgres volume was first created:

Terminal window
docker compose ps postgres
docker compose logs postgres

Port conflicts

If ports 80 or 443 are already in use, edit docker-compose.yml to change the nginx port mappings.

Out of disk space

Terminal window
docker system prune -f # Remove unused images/containers

Reset everything

Terminal window
docker compose down -v
docker compose up -d